UNPKG

@wordpress/block-editor

Version:
172 lines (164 loc) 6.49 kB
/** * External dependencies */ import * as parsel from 'parsel-js'; import Processor from 'postcss/lib/processor'; import CssSyntaxError from 'postcss/lib/css-syntax-error'; import prefixSelector from 'postcss-prefix-selector'; import rebaseUrl from 'postcss-urlrebase'; const cacheByWrapperSelector = new Map(); const ROOT_SELECTOR_TOKENS = [{ type: 'type', content: 'body' }, { type: 'type', content: 'html' }, { type: 'pseudo-class', content: ':root' }, { type: 'pseudo-class', content: ':where(body)' }, { type: 'pseudo-class', content: ':where(:root)' }, { type: 'pseudo-class', content: ':where(html)' }]; /** * Prefixes root selectors in a way that ensures consistent specificity. * This requires special handling, since prefixing a classname before * html, body, or :root will generally result in an invalid selector. * * Some libraries will simply replace the root selector with the prefix * instead, but this results in inconsistent specificity. * * This function instead inserts the prefix after the root tags but before * any other part of the selector. This results in consistent specificity: * - If a `:where()` selector is used for the prefix, all selectors output * by `transformStyles` will have no specificity increase. * - If a classname, id, or something else is used as the prefix, all selectors * will have the same specificity bump when transformed. * * @param {string} prefix The prefix. * @param {string} selector The selector. * * @return {string} The prefixed root selector. */ function prefixRootSelector(prefix, selector) { // Use a tokenizer, since regular expressions are unreliable. const tokenized = parsel.tokenize(selector); // Find the last token that contains a root selector by walking back // through the tokens. const lastRootIndex = tokenized.findLastIndex(({ content, type }) => { return ROOT_SELECTOR_TOKENS.some(rootSelector => content === rootSelector.content && type === rootSelector.type); }); // Walk forwards to find the combinator after the last root. // This is where the root ends and the rest of the selector begins, // and the index to insert before. // Doing it this way takes into account that a root selector like // 'body' may have additional id/class/pseudo-class/attribute-selector // parts chained to it, which is difficult to quantify using a regex. let insertionPoint = -1; for (let i = lastRootIndex + 1; i < tokenized.length; i++) { if (tokenized[i].type === 'combinator') { insertionPoint = i; break; } } // Tokenize and insert the prefix with a ' ' combinator before it. const tokenizedPrefix = parsel.tokenize(prefix); tokenized.splice( // Insert at the insertion point, or the end. insertionPoint === -1 ? tokenized.length : insertionPoint, 0, { type: 'combinator', content: ' ' }, ...tokenizedPrefix); return parsel.stringify(tokenized); } function transformStyle({ css, ignoredSelectors = [], baseURL }, wrapperSelector = '', transformOptions) { // When there is no wrapper selector and no base URL, there is no need // to transform the CSS. This is most cases because in the default // iframed editor, no wrapping is needed, and not many styles // provide a base URL. if (!wrapperSelector && !baseURL) { return css; } try { var _transformOptions$ign; const excludedSelectors = [...ignoredSelectors, ...((_transformOptions$ign = transformOptions?.ignoredSelectors) !== null && _transformOptions$ign !== void 0 ? _transformOptions$ign : []), wrapperSelector]; return new Processor([wrapperSelector && prefixSelector({ prefix: wrapperSelector, transform(prefix, selector, prefixedSelector) { // For backwards compatibility, don't use the `exclude` option // of postcss-prefix-selector, instead handle it here to match // the behavior of the old library (postcss-prefix-wrap) that // `transformStyle` previously used. if (excludedSelectors.some(excludedSelector => excludedSelector instanceof RegExp ? selector.match(excludedSelector) : selector.includes(excludedSelector))) { return selector; } const hasRootSelector = ROOT_SELECTOR_TOKENS.some(rootSelector => selector.startsWith(rootSelector.content)); // Reorganize root selectors such that the root part comes before the prefix, // but the prefix still comes before the remaining part of the selector. if (hasRootSelector) { return prefixRootSelector(prefix, selector); } return prefixedSelector; } }), baseURL && rebaseUrl({ rootUrl: baseURL })].filter(Boolean)).process(css, {}).css; // use sync PostCSS API } catch (error) { if (error instanceof CssSyntaxError) { // eslint-disable-next-line no-console console.warn('wp.blockEditor.transformStyles Failed to transform CSS.', error.message + '\n' + error.showSourceCode(false)); } else { // eslint-disable-next-line no-console console.warn('wp.blockEditor.transformStyles Failed to transform CSS.', error); } return null; } } /** * @typedef {Object} EditorStyle * @property {string} css the CSS block(s), as a single string. * @property {?string} baseURL the base URL to be used as the reference when rewriting urls. * @property {?string[]} ignoredSelectors the selectors not to wrap. */ /** * @typedef {Object} TransformOptions * @property {?string[]} ignoredSelectors the selectors not to wrap. */ /** * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. * * @param {EditorStyle[]} styles CSS rules. * @param {string} wrapperSelector Wrapper selector. * @param {TransformOptions} transformOptions Additional options for style transformation. * @return {Array} converted rules. */ const transformStyles = (styles, wrapperSelector = '', transformOptions) => { let cache = cacheByWrapperSelector.get(wrapperSelector); if (!cache) { cache = new WeakMap(); cacheByWrapperSelector.set(wrapperSelector, cache); } return styles.map(style => { let css = cache.get(style); if (!css) { css = transformStyle(style, wrapperSelector, transformOptions); cache.set(style, css); } return css; }); }; export default transformStyles; //# sourceMappingURL=index.js.map