Last active
July 11, 2019 15:48
-
-
Save erquhart/b4a1920a2d5b5a311291d2fc8d9ffb4d to your computer and use it in GitHub Desktop.
Parse stylesheets over time, Eg. for detecting CodeMirror themes dynamically.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { uniq, reduce, map, filter, debounce, sortBy, difference } from 'lodash'; | |
/** | |
* Watches for new stylesheets for a period of time, returns a unique list of | |
* pattern match captures from CSS selectors. Designed to handle a single | |
* capture group per match, so the resulting array of matches will always be | |
* strings. The observer is a singleton, callers should pass callbacks that will | |
* be called when new matches are found. Uses `MutationObserver` to watch for | |
* stylesheets in the document head. Accepts RegExp `pattern` and an options | |
* hash for `debounceLength`/`lifespan` in milliseconds, defaulting to 5000 and | |
* 20000 respectively, and `matchIndex`, defaulting to 1. | |
* | |
* Designed to detect CodeMirror themes by checking for a telltale selector | |
* across all stylesheets and parsing the unique theme name from the selector. | |
* Note that I ultimately never used it, as observing and processing style | |
* changes is costly for performance, and the potential for problems wasn't | |
* worth it. | |
* | |
* Regexp for parsing CodeMirror themes in case its useful: | |
* /^\.cm-s-([\w-]+)\.CodeMirror$/ | |
* | |
* To use with a React app, call **once per component** that requires the data: | |
* | |
* ```js | |
* componentDidMount() { | |
* const cb = themes => this.setState({ themes }); | |
* const pattern = /^\.cm-s-([\w-]+)\.CodeMirror$/; | |
* observeStyles(cb, pattern); | |
* } | |
* | |
* Once the observer expires (20s from init by default), further calls to | |
* observeStyles are safe and will do nothing. | |
*/ | |
let headObserver; | |
let matches = []; | |
const headObserverCallbacks = []; | |
export default function observeStyles(cb, pattern, opts = {}) { | |
const { debounceLength = 5000, lifespan = 20000, matchIndex = 1 } = opts; | |
if (headObserver === undefined) { | |
headObserver = new MutationObserver(debounce( | |
() => onObserveStyles(pattern), | |
debounceLength, | |
{ leading: true }, | |
)); | |
headObserver.observe(document.head, { childList: true }); | |
setTimeout(() => { | |
headObserver.disconnect(); | |
headObserver = null; | |
}, lifespan); | |
} else if (headObserver !== null) { | |
if (matches.length > 0) { | |
cb(matches); | |
} | |
headObserverCallbacks.push(cb); | |
} else { | |
cb(matches); | |
} | |
} | |
function onObserveStyles(pattern, matchIndex) { | |
const parsedMatches = parseMatchesFromStyles(pattern, matchIndex); | |
if (difference(parsedMatches, matches).length > 0) { | |
matches = parsedMatches; | |
headObserverCallbacks.forEach(cb => cb(matches)); | |
} | |
} | |
function parseMatchesFromStyles(pattern, matchIndex) { | |
const matches = reduce(document.styleSheets, (acc, { cssRules }) => { | |
const matches = map(cssRules, ({ selectorText }) => { | |
return selectorText?.match(pattern)?.[matchIndex]; | |
}); | |
return acc.concat(matches); | |
}, []); | |
return sortBy(uniq(filter(matches, v => v))); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment