Skip to content

Instantly share code, notes, and snippets.

@erquhart
Last active July 11, 2019 15:48
Show Gist options
  • Save erquhart/b4a1920a2d5b5a311291d2fc8d9ffb4d to your computer and use it in GitHub Desktop.
Save erquhart/b4a1920a2d5b5a311291d2fc8d9ffb4d to your computer and use it in GitHub Desktop.
Parse stylesheets over time, Eg. for detecting CodeMirror themes dynamically.
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