From 2300c23ad447f32b435322cbb694133fea4b53d5 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Sat, 9 Sep 2023 17:06:09 +0400 Subject: [PATCH] Add support for container CSS at rule. Closes #5372 --- src/parser/model/BrowserParserCss.ts | 94 +++++++++++++++++++--------- src/parser/model/ParserCss.ts | 14 ++--- test/specs/parser/model/ParserCss.ts | 27 ++++++++ 3 files changed, 99 insertions(+), 36 deletions(-) diff --git a/src/parser/model/BrowserParserCss.ts b/src/parser/model/BrowserParserCss.ts index cf5cd60f7..6f4287740 100644 --- a/src/parser/model/BrowserParserCss.ts +++ b/src/parser/model/BrowserParserCss.ts @@ -1,22 +1,53 @@ import { keys } from 'underscore'; -import { CssRuleJSON, CssRuleProperties } from '../../css_composer/model/CssRule'; - -// At-rules -// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants -const atRules: Record = { - 4: 'media', - 5: 'font-face', - 6: 'page', - 7: 'keyframes', - 11: 'counter-style', - 12: 'supports', - 13: 'document', - 14: 'font-feature-values', - 15: 'viewport', +import { CssRuleJSON } from '../../css_composer/model/CssRule'; +import { ObjectStrings } from '../../common'; + +/** @see https://developer.mozilla.org/en-US/docs/Web/API/CSSRule/type */ +const CSS_RULE_TYPES = { + STYLE_RULE: 1, + CHARSET_RULE: 2, + IMPORT_RULE: 3, + MEDIA_RULE: 4, + FONT_FACE_RULE: 5, + PAGE_RULE: 6, + KEYFRAMES_RULE: 7, + KEYFRAME_RULE: 8, + NAMESPACE_RULE: 10, + COUNTER_STYLE_RULE: 11, + SUPPORTS_RULE: 12, + DOCUMENT_RULE: 13, + FONT_FEATURE_VALUES_RULE: 14, + VIEWPORT_RULE: 15, + REGION_STYLE_RULE: 16, }; -const atRuleKeys = keys(atRules); -const singleAtRules = ['5', '6', '11', '15']; -const singleAtRulesNames = ['font-face', 'page', 'counter-style', 'viewport']; + +/** @see https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule */ +const AT_RULE_NAMES: ObjectStrings = { + [CSS_RULE_TYPES.MEDIA_RULE]: 'media', + [CSS_RULE_TYPES.FONT_FACE_RULE]: 'font-face', + [CSS_RULE_TYPES.PAGE_RULE]: 'page', + [CSS_RULE_TYPES.KEYFRAMES_RULE]: 'keyframes', + [CSS_RULE_TYPES.COUNTER_STYLE_RULE]: 'counter-style', + [CSS_RULE_TYPES.SUPPORTS_RULE]: 'supports', + [CSS_RULE_TYPES.DOCUMENT_RULE]: 'document', + [CSS_RULE_TYPES.FONT_FEATURE_VALUES_RULE]: 'font-feature-values', + [CSS_RULE_TYPES.VIEWPORT_RULE]: 'viewport', +}; + +const AT_RULE_KEYS = keys(AT_RULE_NAMES); + +const SINGLE_AT_RULE_TYPES = [ + CSS_RULE_TYPES.FONT_FACE_RULE, + CSS_RULE_TYPES.PAGE_RULE, + CSS_RULE_TYPES.COUNTER_STYLE_RULE, + CSS_RULE_TYPES.VIEWPORT_RULE, +]; + +const NESTABLE_AT_RULE_NAMES = AT_RULE_KEYS.filter(i => SINGLE_AT_RULE_TYPES.indexOf(Number(i)) < 0) + .map(i => AT_RULE_NAMES[i]) + .concat(['container', 'layer']); + +const SINGLE_AT_RULE_NAMES = SINGLE_AT_RULE_TYPES.map(n => AT_RULE_NAMES[n]); /** * Parse selector string to array. @@ -100,7 +131,7 @@ export const createNode = (selectors: string[], style = {}, opts = {}): CssRuleJ const state = stateArr[1]; // @ts-ignore const { atRule, selectorsAdd, mediaText } = opts; - const singleAtRule = singleAtRulesNames.indexOf(atRule) >= 0; + const singleAtRule = SINGLE_AT_RULE_NAMES.indexOf(atRule) >= 0; singleAtRule && (node.singleAtRule = true); atRule && (node.atRuleType = atRule); selectorsAdd && (node.selectorsAdd = selectorsAdd); @@ -119,6 +150,11 @@ export const createNode = (selectors: string[], style = {}, opts = {}): CssRuleJ return node as CssRuleJSON; }; +export const getNestableAtRule = (node: CSSRule) => { + const { cssText = '' } = node; + return NESTABLE_AT_RULE_NAMES.find(name => cssText.indexOf(`@${name}`) === 0); +}; + /** * Fetch data from node * @param {StyleSheet|CSSRule} el @@ -129,28 +165,28 @@ export const parseNode = (el: CSSStyleSheet | CSSRule) => { const nodes = (el as CSSStyleSheet).cssRules || []; for (let i = 0, len = nodes.length; i < len; i++) { - const node: CSSRule = nodes[i]; - const type = node.type.toString(); + const node = nodes[i]; + const { type } = node; let singleAtRule = false; let atRuleType = ''; let condition = ''; - // @ts-ignore keyText is for CSSKeyframeRule - let sels: string = node.selectorText || node.keyText; - const isSingleAtRule = singleAtRules.indexOf(type) >= 0; + const sels = (node as CSSStyleRule).selectorText || (node as CSSKeyframeRule).keyText || ''; + const isSingleAtRule = SINGLE_AT_RULE_TYPES.indexOf(type) >= 0; // Check if the node is an at-rule if (isSingleAtRule) { singleAtRule = true; - atRuleType = atRules[type]; + atRuleType = AT_RULE_NAMES[type]; condition = parseCondition(node); - } else if (atRuleKeys.indexOf(type) >= 0) { + } else if (AT_RULE_KEYS.indexOf(`${type}`) >= 0 || (!type && getNestableAtRule(node))) { const subRules = parseNode(node); + const subAtRuleType = AT_RULE_NAMES[type] || getNestableAtRule(node); condition = parseCondition(node); for (let s = 0, lens = subRules.length; s < lens; s++) { const subRule = subRules[s]; condition && (subRule.mediaText = condition); - subRule.atRuleType = atRules[type]; + subRule.atRuleType = subAtRuleType; } result = result.concat(subRules); } @@ -166,7 +202,7 @@ export const parseNode = (el: CSSStyleSheet | CSSRule) => { // For each group of selectors for (let k = 0, len3 = selsArr.length; k < len3; k++) { const model = createNode(selsArr[k], style, { - atRule: atRules[type], + atRule: AT_RULE_NAMES[type], }); result.push(model); lastRule = model; @@ -206,8 +242,8 @@ export default (str: string) => { // There is no .sheet before adding it to the document.head.appendChild(el); - const sheet = el.sheet!; + const sheet = el.sheet; document.head.removeChild(el); - return parseNode(sheet); + return sheet ? parseNode(sheet) : []; }; diff --git a/src/parser/model/ParserCss.ts b/src/parser/model/ParserCss.ts index 24bd4c51c..7d4dc497d 100644 --- a/src/parser/model/ParserCss.ts +++ b/src/parser/model/ParserCss.ts @@ -7,18 +7,18 @@ import BrowserCssParser, { parseSelector, createNode } from './BrowserParserCss' const ParserCss = (em?: EditorModel, config: ParserConfig = {}) => ({ /** * Parse CSS string to a desired model object - * @param {String} str CSS string + * @param {String} input CSS string * @return {Array} */ - parse(str: string) { - let result: CssRuleJSON[] = []; + parse(input: string) { + let output: CssRuleJSON[] = []; const { parserCss } = config; const editor = em?.Editor; - const nodes = parserCss ? parserCss(str, editor!) : BrowserCssParser(str); - nodes.forEach(node => (result = result.concat(this.checkNode(node)))); - em?.trigger('parse:css', { input: str, output: result }); + const nodes = parserCss ? parserCss(input, editor!) : BrowserCssParser(input); + nodes.forEach(node => (output = output.concat(this.checkNode(node)))); + em?.trigger('parse:css', { input, output, nodes }); - return result; + return output; }, /** diff --git a/test/specs/parser/model/ParserCss.ts b/test/specs/parser/model/ParserCss.ts index 2eae73fa8..dbc8ff8af 100644 --- a/test/specs/parser/model/ParserCss.ts +++ b/test/specs/parser/model/ParserCss.ts @@ -288,6 +288,33 @@ describe('ParserCss', () => { expect(obj.parse(str)).toEqual([result]); }); + // Unsupported by CSSOM parser + test.skip('Parse rule with @container at-rule', () => { + const atRuleType = 'container'; + const atRuleText = 'somename (min-width: 300px)'; + const input = `@${atRuleType} ${atRuleText} { + .cls1{ height: 100px } + .cls2{ height: 200px } + }`; + const output = [ + { + selectors: ['cls1'], + selectorsAdd: '', + atRuleType, + mediaText: atRuleText, + style: { height: '100px' }, + }, + { + selectors: ['cls2'], + selectorsAdd: '', + atRuleType, + mediaText: atRuleText, + style: { height: '200px' }, + }, + ]; + expect(obj.parse(input)).toEqual(output); + }); + test('Parses multiple font-face at-rules', () => { const str = ` @font-face {