From f8fa7cdae80e0d48cfb8b43a919dc6fdd00ce865 Mon Sep 17 00:00:00 2001 From: Darin Dimitrov Date: Fri, 8 Nov 2019 10:22:11 +0200 Subject: [PATCH 1/3] feat(css): Added optional css-tree parser --- nativescript-core/css/css-tree-parser.ts | 121 ++++++++++++++++++++ nativescript-core/package.json | 1 + nativescript-core/ui/styling/style-scope.ts | 17 ++- package.json | 2 +- 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 nativescript-core/css/css-tree-parser.ts diff --git a/nativescript-core/css/css-tree-parser.ts b/nativescript-core/css/css-tree-parser.ts new file mode 100644 index 0000000000..6a5cb33e29 --- /dev/null +++ b/nativescript-core/css/css-tree-parser.ts @@ -0,0 +1,121 @@ +import { + parse +} from "css-tree"; + +function mapSelectors(selector: string): string[] { + if (!selector) { + return []; + } + + return selector.split(/\s*(?![^(]*\)),\s*/).map(s => s.replace(/\u200C/g, ",")); +} + +function mapPosition(node) { + return { + start: { + line: node.loc.start.line, + column: node.loc.start.column + }, + end: { + line: node.loc.end.line, + column: node.loc.end.column + } + }; +} + +function transformAst(node) { + if (!node) { + return; + } + + if (node.type === "StyleSheet") { + return { + type: "stylesheet", + stylesheet: { + source: node.loc.source, + rules: node.children.toArray().map(child => transformAst(child)), + parsingErrors: [] + } + }; + } + + if (node.type === "Atrule") { + let atrule: any = { + type: node.name, + }; + + if (node.name === "supports" || node.name === "media") { + atrule[node.name] = node.prelude.value; + atrule.rules = transformAst(node.block); + } else if (node.name === "page") { + atrule.selectors = node.prelude ? mapSelectors(node.prelude.value) : []; + atrule.declarations = transformAst(node.block); + } else if (node.name === "document") { + atrule.document = node.prelude ? node.prelude.value : ""; + atrule.vendor = ""; + atrule.rules = transformAst(node.block); + } else if (node.name === "font-face") { + atrule.declarations = transformAst(node.block); + } else if (node.name === "import" || node.name === "charset" || node.name === "namespace") { + atrule[node.name] = node.prelude ? node.prelude.value : ""; + } else { + atrule.rules = transformAst(node.block); + } + + return atrule; + } + + if (node.type === "Block") { + return node.children.toArray().map(child => transformAst(child)); + } + + if (node.type === "Rule") { + let value = node.prelude.value; + + return { + type: "rule", + selectors: mapSelectors(value), + declarations: transformAst(node.block), + position: mapPosition(node) + }; + } + + if (node.type === "Comment") { + return { + type: "comment", + comment: node.value, + position: mapPosition(node) + }; + } + + if (node.type === "Declaration") { + return { + type: "declaration", + property: node.property, + value: node.value.value, + position: mapPosition(node) + }; + } + + throw Error(`Unknown node type ${node.type}`); +} + +export function cssTreeParse(css, source): any { + let errors = []; + let ast = parse(css, { + parseValue: false, + parseAtrulePrelude: false, + parseRulePrelude: false, + positions: true, + filename: source, + onParseError: error => { + errors.push(`${source}:${error.line}:${error.column}: ${error.formattedMessage}`); + } + }); + + if (errors.length > 0) { + throw new Error(errors[0]); + } + + return transformAst(ast); +} \ No newline at end of file diff --git a/nativescript-core/package.json b/nativescript-core/package.json index a0441e28b7..5ff92805f8 100644 --- a/nativescript-core/package.json +++ b/nativescript-core/package.json @@ -20,6 +20,7 @@ "dependencies": { "nativescript-hook": "0.2.5", "reduce-css-calc": "^2.1.6", + "css-tree": "^1.0.0-alpha.37", "semver": "6.3.0", "tns-core-modules-widgets": "next", "tslib": "1.10.0" diff --git a/nativescript-core/ui/styling/style-scope.ts b/nativescript-core/ui/styling/style-scope.ts index ed585227d1..ea4087ccdd 100644 --- a/nativescript-core/ui/styling/style-scope.ts +++ b/nativescript-core/ui/styling/style-scope.ts @@ -12,6 +12,9 @@ import { CSS3Parser, CSSNativeScript } from "../../css/parser"; +import { + cssTreeParse +} from "../../css/css-tree-parser"; import { RuleSet, @@ -50,11 +53,15 @@ function ensureCssAnimationParserModule() { } } -let parser: "rework" | "nativescript" = "rework"; +let parser: "rework" | "nativescript" | "css-tree" = "rework"; try { const appConfig = require("~/package.json"); - if (appConfig && appConfig.cssParser === "nativescript") { - parser = "nativescript"; + if (appConfig) { + if (appConfig.cssParser === "css-tree") { + parser = "css-tree"; + } else if (appConfig.cssParser === "nativescript") { + parser = "nativescript"; + } } } catch (e) { // @@ -220,6 +227,10 @@ class CSSSource { private parseCSSAst() { if (this._source) { switch (parser) { + case "css-tree": + this._ast = cssTreeParse(this._source, this._file); + + return; case "nativescript": const cssparser = new CSS3Parser(this._source); const stylesheet = cssparser.parseAStylesheet(); diff --git a/package.json b/package.json index e68f0c5068..72ecee0d91 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@types/node": "~10.12.18", "chai": "^4.1.2", "css": "^2.2.1", - "css-tree": "^1.0.0-alpha24", + "css-tree": "^1.0.0-alpha.37", "gonzales": "^1.0.7", "madge": "^2.2.0", "markdown-snippet-injector": "0.2.2", From 879c106dfea86827488c01b8e3ebaafdc74c43ae Mon Sep 17 00:00:00 2001 From: vakrilov Date: Mon, 2 Dec 2019 14:21:32 +0200 Subject: [PATCH 2/3] test: css-tree parser compat tests --- unit-tests/css-tree-parser/css-tree-parser.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 unit-tests/css-tree-parser/css-tree-parser.ts diff --git a/unit-tests/css-tree-parser/css-tree-parser.ts b/unit-tests/css-tree-parser/css-tree-parser.ts new file mode 100644 index 0000000000..72c8a83ff6 --- /dev/null +++ b/unit-tests/css-tree-parser/css-tree-parser.ts @@ -0,0 +1,21 @@ +import { cssTreeParse } from "@nativescript/core/css/css-tree-parser"; +import { parse as reworkCssParse } from "@nativescript/core/css"; +import { assert } from "chai"; + +describe("css-tree parser compatible with rework ", () => { + it("basic selector", () => { + const testCase = ".test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(reworkAST, cssTreeAST); + }); + + it("@keyframes", () => { + const testCase = ".test { animation-name: test; } @keyframes test { from { background-color: red; } to { background-color: blue; } }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(reworkAST, cssTreeAST); + }); +}); \ No newline at end of file From edd0cbcaa92761fd1f02a60db37c9cc422e8dfd5 Mon Sep 17 00:00:00 2001 From: Darin Dimitrov Date: Tue, 3 Dec 2019 10:16:39 +0200 Subject: [PATCH 3/3] test: more css-tree compat tests --- nativescript-core/css/css-tree-parser.ts | 61 ++++++++++------ tests/app/ui/styling/style-tests.ts | 2 +- unit-tests/css-tree-parser/css-tree-parser.ts | 70 ++++++++++++++++++- 3 files changed, 108 insertions(+), 25 deletions(-) diff --git a/nativescript-core/css/css-tree-parser.ts b/nativescript-core/css/css-tree-parser.ts index 6a5cb33e29..b7aa1413f3 100644 --- a/nativescript-core/css/css-tree-parser.ts +++ b/nativescript-core/css/css-tree-parser.ts @@ -10,8 +10,8 @@ function mapSelectors(selector: string): string[] { return selector.split(/\s*(?![^(]*\)),\s*/).map(s => s.replace(/\u200C/g, ",")); } -function mapPosition(node) { - return { +function mapPosition(node, css) { + let res: any = { start: { line: node.loc.start.line, column: node.loc.start.column @@ -19,11 +19,18 @@ function mapPosition(node) { end: { line: node.loc.end.line, column: node.loc.end.column - } + }, + content: css }; + + if (node.loc.source && node.loc.source !== "") { + res.source = node.loc.source; + } + + return res; } -function transformAst(node) { +function transformAst(node, css, type = null) { if (!node) { return; } @@ -32,8 +39,7 @@ function transformAst(node) { return { type: "stylesheet", stylesheet: { - source: node.loc.source, - rules: node.children.toArray().map(child => transformAst(child)), + rules: node.children.map(child => transformAst(child, css)).toArray(), parsingErrors: [] } }; @@ -46,45 +52,58 @@ function transformAst(node) { if (node.name === "supports" || node.name === "media") { atrule[node.name] = node.prelude.value; - atrule.rules = transformAst(node.block); + atrule.rules = transformAst(node.block, css); } else if (node.name === "page") { atrule.selectors = node.prelude ? mapSelectors(node.prelude.value) : []; - atrule.declarations = transformAst(node.block); + atrule.declarations = transformAst(node.block, css); } else if (node.name === "document") { atrule.document = node.prelude ? node.prelude.value : ""; atrule.vendor = ""; - atrule.rules = transformAst(node.block); + atrule.rules = transformAst(node.block, css); } else if (node.name === "font-face") { - atrule.declarations = transformAst(node.block); + atrule.declarations = transformAst(node.block, css); } else if (node.name === "import" || node.name === "charset" || node.name === "namespace") { atrule[node.name] = node.prelude ? node.prelude.value : ""; + } else if (node.name === "keyframes") { + atrule.name = node.prelude ? node.prelude.value : ""; + atrule.keyframes = transformAst(node.block, css, "keyframe"); + atrule.vendor = undefined; } else { - atrule.rules = transformAst(node.block); + atrule.rules = transformAst(node.block, css); } + atrule.position = mapPosition(node, css); + return atrule; } if (node.type === "Block") { - return node.children.toArray().map(child => transformAst(child)); + return node.children.map(child => transformAst(child, css, type)).toArray(); } if (node.type === "Rule") { let value = node.prelude.value; - return { - type: "rule", - selectors: mapSelectors(value), - declarations: transformAst(node.block), - position: mapPosition(node) + let res: any = { + type: type != null ? type : "rule", + declarations: transformAst(node.block, css), + position: mapPosition(node, css) }; + + if (type === "keyframe") { + res.values = mapSelectors(value); + } else { + res.selectors = mapSelectors(value); + } + + return res; } if (node.type === "Comment") { return { type: "comment", comment: node.value, - position: mapPosition(node) + position: mapPosition(node, css) }; } @@ -92,8 +111,8 @@ function transformAst(node) { return { type: "declaration", property: node.property, - value: node.value.value, - position: mapPosition(node) + value: node.value.value ? node.value.value.trim() : "", + position: mapPosition(node, css) }; } @@ -117,5 +136,5 @@ export function cssTreeParse(css, source): any { throw new Error(errors[0]); } - return transformAst(ast); + return transformAst(ast, css); } \ No newline at end of file diff --git a/tests/app/ui/styling/style-tests.ts b/tests/app/ui/styling/style-tests.ts index 37835cbb4f..ee4b834432 100644 --- a/tests/app/ui/styling/style-tests.ts +++ b/tests/app/ui/styling/style-tests.ts @@ -1584,7 +1584,7 @@ export function test_nested_css_calc() { stack.className = "wide"; TKUnit.assertEqual(stack.width as any, 125, "Stack - width === 125"); - (stack as any).style = `width: calc(100% * calc(1 / 2)`; + (stack as any).style = `width: calc(100% * calc(1 / 2))`; TKUnit.assertDeepEqual(stack.width, { unit: "%", value: 0.5 }, "Stack - width === 50%"); } diff --git a/unit-tests/css-tree-parser/css-tree-parser.ts b/unit-tests/css-tree-parser/css-tree-parser.ts index 72c8a83ff6..c72e7f0984 100644 --- a/unit-tests/css-tree-parser/css-tree-parser.ts +++ b/unit-tests/css-tree-parser/css-tree-parser.ts @@ -8,14 +8,78 @@ describe("css-tree parser compatible with rework ", () => { const reworkAST = reworkCssParse(testCase, { source: "file.css" }); const cssTreeAST = cssTreeParse(testCase, "file.css"); - assert.deepEqual(reworkAST, cssTreeAST); + assert.deepEqual(cssTreeAST, reworkAST); }); it("@keyframes", () => { - const testCase = ".test { animation-name: test; } @keyframes test { from { background-color: red; } to { background-color: blue; } }"; + const testCase = ".test { animation-name: test; } @keyframes test { from { background-color: red; } to { background-color: blue; } } .test { color: red; }"; const reworkAST = reworkCssParse(testCase, { source: "file.css" }); const cssTreeAST = cssTreeParse(testCase, "file.css"); - assert.deepEqual(reworkAST, cssTreeAST); + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@media", () => { + const testCase = "@media screen and (max-width: 600px) { body { background-color: olive; } } .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@supports", () => { + const testCase = "@supports not (display: grid) { div { float: right; } } .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@page", () => { + const testCase = "@page :first { margin: 2cm; } .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@document", () => { + const testCase = "@document url(\"https://www.example.com/\") { h1 { color: green; } } .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@font-face", () => { + const testCase = "@font-face { font-family: \"Open Sans\"; src: url(\"/fonts/OpenSans-Regular-webfont.woff2\") format(\"woff2\"), url(\"/fonts/OpenSans-Regular-webfont.woff\") format(\"woff\"); } .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@import", () => { + const testCase = "@import url('landscape.css') screen and (orientation:landscape); @import url(\"fineprint.css\") print; .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@charset", () => { + const testCase = "@charset \"utf-8\"; .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); + }); + + it("@namespace", () => { + const testCase = "@namespace svg url(http://www.w3.org/2000/svg); .test { color: red; }"; + const reworkAST = reworkCssParse(testCase, { source: "file.css" }); + const cssTreeAST = cssTreeParse(testCase, "file.css"); + + assert.deepEqual(cssTreeAST, reworkAST); }); }); \ No newline at end of file