diff --git a/packages/css-scope/__tests__/index.spec.ts b/packages/css-scope/__tests__/index.spec.ts index 9c2133994..24d5fa76b 100644 --- a/packages/css-scope/__tests__/index.spec.ts +++ b/packages/css-scope/__tests__/index.spec.ts @@ -1,3 +1,95 @@ +import { parse, stringify } from '../src'; + describe('Css scope', () => { - it('needs tests', () => {}); + it('should parse @container syntax correctly', () => { + const css = ` + .card { + container-type: inline-size; + } + + @container (min-width: 400px) { + .card-content { + font-size: 18px; + } + } + `; + + const ast = parse(css); + const scopedCss = stringify(ast, 'MyApp'); + expect(scopedCss).toContain('@container (min-width: 400px)'); + expect(scopedCss).toContain('#MyApp .card-content'); + expect(scopedCss).toContain('font-size: 18px'); + }); + + it('should parse @keyframes syntax correctly', () => { + const css = ` + .\@1_0_0_2444__aTWceUPNFbTpZsnHPM94_semi-icon-spinning { + animation: \@1_0_0_2444__XXl2sOw3Pp3KVSQS9PFl_semi-icon-animation-rotate .6s linear infinite; + animation-fill-mode: forwards; + } + + @keyframes \@1_0_0_2444__XXl2sOw3Pp3KVSQS9PFl_semi-icon-animation-rotate { + 0% { + transform: rotate(0) + } + to { + transform: rotate(1turn) + } + } + `; + + const ast = parse(css); + const scopedCss = stringify(ast, 'MyApp'); + console.log(scopedCss); + expect(scopedCss).toContain( + 'MyApp-@1_0_0_2444__XXl2sOw3Pp3KVSQS9PFl_semi-icon-animation-rotate', + ); + }); + + it('should parse incorrect syntax @dark-text with silent mode', () => { + const css = ` + body { + --data-1: var(--arcoblue-5); + } + @keyframes arco-msg-fade { + 0% { + opacity: 0; + } + to { + opacity: 1; + } + } + @keyframes arco-msg-scale { + 0% { + transform: scale(0); + } + to { + transform: scale(1); + } + } + @keyframes force-loading-circle { + to { + transform: rotate(1turn); + } + } + @keyframes arco-loading-circle { + to { + transform: rotate(1turn); + } + } + @dark-text: arco-theme= 'dark'; + `; + + // Use silent mode to ignore syntax errors + const ast = parse(css, { silent: true }); + const scopedCss = stringify(ast, 'MyApp'); + + // Verify that valid CSS rules are still processed correctly + expect(scopedCss).toContain('#MyApp [__garfishmockbody__]'); + + console.log(ast.stylesheet.parsingErrors); + // Verify that parsing errors are captured + expect(ast.stylesheet.parsingErrors).toBeDefined(); + expect(ast.stylesheet.parsingErrors.length).toBeGreaterThan(0); + }); }); diff --git a/packages/css-scope/src/animationParser.ts b/packages/css-scope/src/animationParser.ts index 63c980959..65d08f25b 100644 --- a/packages/css-scope/src/animationParser.ts +++ b/packages/css-scope/src/animationParser.ts @@ -208,7 +208,7 @@ function parse(tokens: Array): Array { type Props = Array; function stringify(tree: Array, prefix: string) { let output = ''; - const splice = (p) => (isName(p) ? `${p}-${prefix}` : p); + const splice = (p) => (isName(p) ? `${prefix}-${p}` : p); const child = (ps: Array) => { let buf = ''; diff --git a/packages/css-scope/src/cssParser.ts b/packages/css-scope/src/cssParser.ts index 406136290..d089bbb94 100644 --- a/packages/css-scope/src/cssParser.ts +++ b/packages/css-scope/src/cssParser.ts @@ -19,6 +19,7 @@ import { KeyframesNode, StylesheetNode, CustomMediaNode, + ContainerNode, } from './globalTypes'; const commentre = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g; @@ -267,7 +268,9 @@ export function parse(css: string, options: CssParserOptions = {}) { const vendor = m[1]; // identifier - m = match(/^([-\w]+)\s*/); + // m = match(/^([-\w]+)\s*/); + m = match(/^([-@\w\\]+)\s*/); + if (!m) return error('@keyframes missing name'); const name = m[1]; @@ -342,6 +345,25 @@ export function parse(css: string, options: CssParserOptions = {}) { }) as MediaNode; } + // Parse container. + function atcontainer() { + const pos = position(); + const m = match(/^@container *([^{]+)/); + + if (!m) return; + const container = trim(m[1]); + + if (!open()) return error("@container missing '{'"); + const style = comments().concat(rules()); + if (!close()) return error("@container missing '}'"); + + return pos({ + type: 'container', + container: container, + rules: style, + }) as ContainerNode; + } + // Parse custom-media. function atcustommedia() { const pos = position(); @@ -450,6 +472,7 @@ export function parse(css: string, options: CssParserOptions = {}) { return ( atkeyframes() || atmedia() || + atcontainer() || atcustommedia() || atsupports() || atimport() || diff --git a/packages/css-scope/src/cssStringify.ts b/packages/css-scope/src/cssStringify.ts index 61149575e..fb849ea2b 100644 --- a/packages/css-scope/src/cssStringify.ts +++ b/packages/css-scope/src/cssStringify.ts @@ -17,6 +17,7 @@ import { NamespaceNode, StylesheetNode, CustomMediaNode, + ContainerNode, } from './globalTypes'; import { processAnimation } from './animationParser'; @@ -112,6 +113,15 @@ class Compiler { ); } + container(node: ContainerNode) { + return ( + this.emit(`@container ${node.container}`, node.position) + + this.emit(` {\n${this.indent(1)}`) + + this.mapVisit(node.rules, '\n\n') + + this.emit(`${this.indent(-1)}\n}`) + ); + } + document(node: DocumentNode) { const doc = `@${node.vendor || ''}document ${node.document}`; return ( diff --git a/packages/css-scope/src/globalTypes.ts b/packages/css-scope/src/globalTypes.ts index 371e2d944..72ea0269f 100644 --- a/packages/css-scope/src/globalTypes.ts +++ b/packages/css-scope/src/globalTypes.ts @@ -94,12 +94,18 @@ export interface StylesheetNode extends BaseNode<'stylesheet'> { }; } +export interface ContainerNode extends BaseNode<'container'> { + container: string; + rules: Array; +} + export type Node = | DeclNode | PageNode | HostNode | RuleNode | MediaNode + | ContainerNode | ImportNode | CharsetNode | CommentNode