Files
Panayot Cankov f7a3a36b9c Housekeeping node tests, renamed to unit-tests (#4936)
Add parsers for the background css shorthand property, make ViewBase unit testable in node environment

Add background parser and linear-gradient parser

Use sticky regexes

Simplify some types, introduce generic Parsed<T> instead of & TokenRange

Apply each parser to return a { start, end, value } object

Move the css selector parser to the css/parser and unify types

Add the first steps toward building homegrown css parser

Add somewhat standards compliant tokenizer, add baseline, rework and shady css parsers

Enable all tests again, skip flaky perf test

Improve css parser tokenizer by converting some char token types to simple string

Implement 'parse a stylesheet'

Add gonzales css-parser

Add parseLib and css-tree perf

Add a thin parser layer that will convert CSS3 tokens to values, for now output is compatible with rework

Make root tsc green

Return the requires of tns-core-modules to use relative paths for webpack to work

Implement support for '@import 'url-string';

Fix function parser, function-token is no-longer neglected

Make the style-scope be able to load from "css" and "css-ast" modules

Add a loadAppCss event so theme can be added to snapshot separately from loaded
2017-10-20 10:42:07 +03:00

462 lines
25 KiB
TypeScript

import { assert } from "chai";
import {
parseURL,
parseColor,
parsePercentageOrLength,
parseBackgroundPosition,
parseBackground,
parseSelector,
AttributeSelectorTest,
CSS3Parser,
TokenObjectType,
CSSNativeScript,
} from "tns-core-modules/css/parser";
import {
parse
} from "tns-core-modules/css";
import * as fs from "fs";
import * as shadyCss from 'shady-css-parser';
import * as reworkCss from 'css';
const parseCss: any = require('parse-css');
const gonzales: any = require('gonzales');
const parserlib: any = require("parserlib");
const csstree: any = require('css-tree');
describe("css", () => {
describe("parser", () => {
function test<T>(parse: (value: string, lastIndex?: number) => T, value: string, expected: T);
function test<T>(parse: (value: string, lastIndex?: number) => T, value: string, lastIndex: number, expected: T);
function test<T>(parse: (value: string, lastIndex?: number) => T, value: string, lastIndexOrExpected: number | T, expected?: T) {
if (arguments.length === 3) {
it(`${lastIndexOrExpected ? "can parse " : "can not parse "}"${value}"`, () => {
const result = parse(value);
assert.deepEqual(result, lastIndexOrExpected);
});
} else {
it(`${expected ? "can parse " : "can not parse "}"${value}" starting at index ${lastIndexOrExpected}`, () => {
const result = parse(value, <number>lastIndexOrExpected);
assert.deepEqual(result, expected);
});
}
}
describe("values", () => {
describe("url", () => {
test(parseURL, "url('smiley.gif') ", { start: 0, end: 19, value: "smiley.gif" });
test(parseURL, ' url("frown.gif") ', { start: 0, end: 19, value: "frown.gif" });
test(parseURL, " url(lucky.gif)", { start: 0, end: 16, value: "lucky.gif" });
test(parseURL, "url(lucky.gif) #FF0000", 15, null);
test(parseURL, "repeat url(lucky.gif) #FF0000", 6, { start: 6, end: 22, value: "lucky.gif" });
});
describe("color", () => {
test(parseColor, " #369 ", { start: 0, end: 7, value: 0xFF336699 });
test(parseColor, " #456789 ", { start: 0, end: 10, value: 0xFF456789 });
test(parseColor, " #85456789 ", { start: 0, end: 12, value: 0x85456789 });
test(parseColor, " rgb(255, 8, 128) ", { start: 0, end: 18, value: 0xFFFF0880 });
test(parseColor, " rgba(255, 8, 128, 0.5) ", { start: 0, end: 24, value: 0x80FF0880 });
test(parseColor, "#FF0000 url(lucky.gif)", 8, null);
test(parseColor, "url(lucky.gif) #FF0000 repeat", 15, { start: 15, end: 23, value: 0xFFFF0000 });
});
describe("units", () => {
test(parsePercentageOrLength, " 100% ", { start: 0, end: 6, value: { value: 1, unit: "%" }});
test(parsePercentageOrLength, " 100px ", { start: 0, end: 7, value: { value: 100, unit: "px" }});
test(parsePercentageOrLength, " 0.5px ", { start: 0, end: 7, value: { value: 0.5, unit: "px" }});
test(parsePercentageOrLength, " 100dip ", { start: 0, end: 8, value: { value: 100, unit: "dip" }});
test(parsePercentageOrLength, " 100 ", { start: 0, end: 5, value: { value: 100, unit: "dip" }});
test(parsePercentageOrLength, " 100 ", { start: 0, end: 5, value: { value: 100, unit: "dip" }});
test(parsePercentageOrLength, " +-12.2 ", null);
});
describe("position", () => {
test(parseBackgroundPosition, "left", { start: 0, end: 4, value: { x: "left", y: "center" }});
test(parseBackgroundPosition, "center", { start: 0, end: 6, value: { x: "center", y: "center" }});
test(parseBackgroundPosition, "right", { start: 0, end: 5, value: { x: "right", y: "center" }});
test(parseBackgroundPosition, "top", { start: 0, end: 3, value: { x: "center", y: "top" }});
test(parseBackgroundPosition, "bottom", { start: 0, end: 6, value: { x: "center", y: "bottom" }});
test(parseBackgroundPosition, "top 75px left 100px", { start: 0, end: 19, value: {
x: { align: "left", offset: { value: 100, unit: "px" }},
y: { align: "top", offset: { value: 75, unit: "px" }}
}});
test(parseBackgroundPosition, "left 100px top 75px", { start: 0, end: 19, value: {
x: { align: "left", offset: { value: 100, unit: "px" }},
y: { align: "top", offset: { value: 75, unit: "px" }}
}});
test(parseBackgroundPosition, "right center", { start: 0, end: 12, value: { x: "right", y: "center" }});
test(parseBackgroundPosition, "center left 100%", { start: 0, end: 16, value: { x: { align: "left", offset: { value: 1, unit: "%" }}, y: "center" }});
test(parseBackgroundPosition, "top 50% left 100%", { start: 0, end: 17, value: { x: { align: "left", offset: { value: 1, unit: "%" }}, y: { align: "top", offset: { value: 0.5, unit: "%" }}}});
test(parseBackgroundPosition, "bottom left 25%", { start: 0, end: 15, value: { x: { align: "left", offset: { value: 0.25, unit: "%" }}, y: "bottom" }});
test(parseBackgroundPosition, "top 100% left 25%", { start: 0, end: 17, value: { x: { align: "left", offset: { value: 0.25, unit: "%" }}, y: { align: "top", offset: { value: 1, unit: "%" }}}});
});
describe("background", () => {
test(parseBackground, " #996633 ", { start: 0, end: 12, value: { color: 0xFF996633 }});
test(parseBackground, ' #00ff00 url("smiley.gif") repeat-y ', { start: 0, end: 37, value: { color: 0xFF00FF00, image: "smiley.gif", repeat: "repeat-y" }});
test(parseBackground, ' url(smiley.gif) no-repeat top 50% left 100% #00ff00', { start: 0, end: 56, value: {
color: 0xFF00FF00,
image: "smiley.gif",
repeat: "no-repeat",
position: {
x: { align: "left", offset: { value: 1, unit: "%" }},
y: { align: "top", offset: { value: 0.5, unit: "%" }}
}
}});
test(parseBackground, ' url(smiley.gif) no-repeat top 50% left 100% / 100px 100px #00ff00', { start: 0, end: 70, value: {
color: 0xFF00FF00,
image: "smiley.gif",
repeat: "no-repeat",
position: {
x: { align: "left", offset: { value: 1, unit: "%" }},
y: { align: "top", offset: { value: 0.5, unit: "%" }}
},
size: { x: { value: 100, unit: "px" }, y: { value: 100, unit: "px" }}
}});
test(parseBackground, ' linear-gradient(to right top) ', { start: 0, end: 32, value: {
image: {
angle: Math.PI * 1/4,
colors: []
}
}});
test(parseBackground, ' linear-gradient(45deg, #0000FF, #00FF00) ', { start: 0, end: 43, value: {
image: {
angle: Math.PI * 1/4,
colors: [
{ argb: 0xFF0000FF },
{ argb: 0xFF00FF00 }
]
}
}});
test(parseBackground, 'linear-gradient(0deg, blue, green 40%, red)', { start: 0, end: 43, value: {
image: {
angle: Math.PI * 0/4,
colors: [
{ argb: 0xFF0000FF },
{ argb: 0xFF008000, offset: { value: 0.4, unit: "%" }},
{ argb: 0xFFFF0000 }
]
}
}});
});
});
describe("selectors", () => {
test(parseSelector, ` listview#products.mark gridlayout:selected[row="2"] a> b > c >d>e *[src] `, {
start: 0, end: 79, value: [
[
{ type: "", identifier: "listview" },
{ type: "#", identifier: "products" },
{ type: ".", identifier: "mark" }
],
" ",
[
{ type: "", identifier: "gridlayout" },
{ type: ":", identifier: "selected" },
{ type: "[]", property: "row", test: "=", value: "2" }
],
" ",
[{ type: "", identifier: "a"}],
">",
[{ type: "", identifier: "b"}],
">",
[{ type: "", identifier: "c"}],
">",
[{ type: "", identifier: "d"}],
">",
[{ type: "", identifier: "e"}],
" ",
[
{ type: "*" },
{ type: "[]", property: "src" }
],
undefined
],
});
test(parseSelector, "*", { start: 0, end: 1, value: [[{ type: "*" }], undefined ]});
test(parseSelector, "button", { start: 0, end: 6, value: [[{ type: "", identifier: "button" }], undefined]});
test(parseSelector, ".login", { start: 0, end: 6, value: [[{ type: ".", identifier: "login" }], undefined]});
test(parseSelector, "#login", { start: 0, end: 6, value: [[{ type: "#", identifier: "login" }], undefined]});
test(parseSelector, ":hover", { start: 0, end: 6, value: [[{ type: ":", identifier: "hover" }], undefined]});
test(parseSelector, "[src]", { start: 0, end: 5, value: [[{ type: "[]", property: "src" }], undefined]});
test(parseSelector, `[src = "res://"]`, { start: 0, end: 16, value: [[{ type: "[]", property: "src", test: "=", value: `res://`}], undefined]});
(<AttributeSelectorTest[]>["=", "^=", "$=", "*=", "=", "~=", "|="]).forEach(attributeTest => {
test(parseSelector, `[src ${attributeTest} "val"]`, { start: 0, end: 12 + attributeTest.length, value: [[{ type: "[]", property: "src", test: attributeTest, value: "val"}], undefined]});
});
test(parseSelector, "listview > .image", { start: 0, end: 17, value: [[{ type: "", identifier: "listview"}], ">", [{ type: ".", identifier: "image"}], undefined]});
test(parseSelector, "listview .image", { start: 0, end: 16, value: [[{ type: "", identifier: "listview"}], " ", [{ type: ".", identifier: "image"}], undefined]});
test(parseSelector, "button:hover", { start: 0, end: 12, value: [[{ type: "", identifier: "button" }, { type: ":", identifier: "hover"}], undefined]});
test(parseSelector, "listview>:selected image.product", { start: 0, end: 32, value: [
[{ type: "", identifier: "listview" }],
">",
[{ type: ":", identifier: "selected" }],
" ",
[
{ type: "", identifier: "image" },
{ type: ".", identifier: "product" }
],
undefined
]});
test(parseSelector, "button[testAttr]", { start: 0, end: 16, value: [
[
{ type: "", identifier: "button" },
{ type: "[]", property: "testAttr" },
],
undefined
]});
test(parseSelector, "button#login[user][pass]:focused:hovered", { start: 0, end: 40, value: [
[
{ type: "", identifier: "button" },
{ type: "#", identifier: "login" },
{ type: "[]", property: "user" },
{ type: "[]", property: "pass" },
{ type: ":", identifier: "focused" },
{ type: ":", identifier: "hovered" }
],
undefined
]});
});
describe("css3", () => {
let themeCoreLightIos: string;
let whatIsNewIos: string;
before("Read the core.light.css file", () => {
themeCoreLightIos = fs.readFileSync(`${__dirname}/assets/core.light.css`).toString();
whatIsNewIos = fs.readFileSync(`${__dirname}/assets/what-is-new.ios.css`).toString();
});
describe("tokenizer", () => {
it("the tokenizer roundtrips the core.light.css theme", () => {
const cssparser = new CSS3Parser(themeCoreLightIos);
const stylesheet = cssparser.tokenize();
let original = themeCoreLightIos.replace(/\/\*([^\/]|\/[^\*])*\*\//g, "").replace(/\n/g, " ");
let roundtrip = stylesheet.map(m => {
if (!m) return "";
if (typeof m === "string") return m;
return m.text;
}).join("");
let lastIndex = Math.min(original.length, roundtrip.length);
for(var i = 0; i < lastIndex; i++)
if (original[i] != roundtrip[i])
assert.equal(roundtrip.substr(i, 50), original.substr(i, 50), "Round-tripped CSS string differ at index: " + i);
assert.equal(roundtrip.length, original.length, "Expected round-tripped string lengths to match.");
});
it("test what-is-new.ios.css from nativescript-marketplace-demo", () => {
const parser = new CSS3Parser(whatIsNewIos);
const tokens = parser.tokenize();
assert.deepEqual(tokens, [
{ type: TokenObjectType.atKeyword, text: "import" },
" ",
{ type: TokenObjectType.url, text: "url('~/views/what-is-new-common.css')" },
";", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "news-card" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "margin" },
":", " ",
{ type: TokenObjectType.number, text: "12" },
" ",
{ type: TokenObjectType.number, text: "12" },
" ",
{ type: TokenObjectType.number, text: "0" },
" ",
{ type: TokenObjectType.number, text: "12" },
";", " ", "}", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "title" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "font-size" },
":", " ",
{ type: TokenObjectType.number, text: "14" },
";", " ", "}", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "body" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "font-size" },
":", " ",
{ type: TokenObjectType.number, text: "14" },
";", " ", "}", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "learn-more" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "font-size" },
":", " ",
{ type: TokenObjectType.number, text: "14" },
";", " ", "}", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "date" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "font-size" },
":", " ",
{ type: TokenObjectType.number, text: "12" },
";", " ", "}", " ",
{ type: TokenObjectType.delim, text: "." },
{ type: TokenObjectType.ident, text: "empty-placeholder" },
" ", "{", " ",
{ type: TokenObjectType.ident, text: "vertical-align" },
":", " ",
{ type: TokenObjectType.ident, text: "center" },
";", " ",
{ type: TokenObjectType.ident, text: "text-align" },
":", " ",
{ type: TokenObjectType.ident, text: "center" },
";", " ", "}",
undefined // EOF
]);
});
});
describe("parser", () => {
it("test what-is-new.ios.css from nativescript-marketplace-demo", () => {
const parser = new CSS3Parser(whatIsNewIos);
const stylesheet = parser.parseAStylesheet();
// console.log(JSON.stringify(stylesheet, null, "\t"));
// TODO: Assert...
});
it(".btn-primary{border-color:rgba(255,0,0,0)}", () => {
const parser = new CSS3Parser(".btn-primary{border-color:rgba(255,0,0,0)}");
const stylesheet = parser.parseAStylesheet();
assert.deepEqual(stylesheet, {rules:[
{
type: "qualified-rule",
prelude: [{ type: 2, text: "." }, { type: 6, text: "btn-primary" }],
block: { type: 9, text: "{border-color:rgba(255,0,0,0)}", associatedToken: "{", values: [
{ type: 6, text: "border-color" },
":",
{ type: 14, name: "rgba", text: "rgba(255,0,0,0)", components: [
{ type: 3, text: "255" }, ",",
{ type: 3, text: "0"}, ",",
{ type: 3, text: "0"}, ",",
{ type: 3, text: "0"}
]}
]}
}]},
"NativeScript parsed AST doesn't match.");
const cssToNS = new CSSNativeScript();
const nativescriptAst = cssToNS.parseStylesheet(stylesheet);
assert.deepEqual(nativescriptAst, { type: "stylesheet", stylesheet: { rules: [
{
type: "rule",
selectors: [".btn-primary"],
declarations: [{
"type": "declaration",
"property": "border-color",
"value": "rgba(255,0,0,0)"
}]
}]}},
"NativeScript AST mapped to rework doesn't match.");
});
});
it("serialization", () => {
const reworkAst = reworkCss.parse(themeCoreLightIos, { source: "nativescript-theme-core/css/core.light.css" });
fs.writeFileSync("unit-tests/css/out/rework.css.json", JSON.stringify(reworkAst, (k, v) => k === "position" ? undefined : v, " "));
const nsParser = new CSS3Parser(themeCoreLightIos);
const nativescriptStylesheet = nsParser.parseAStylesheet();
const cssToNS = new CSSNativeScript();
const nativescriptAst = cssToNS.parseStylesheet(nativescriptStylesheet);
fs.writeFileSync("unit-tests/css/out/nativescript.css.json", JSON.stringify(nativescriptAst, null, " "));
});
it.skip("our parser is fast (this test is flaky, gc, opts.)", () => {
function trapDuration(action: () => void) {
const [startSec, startMSec] = process.hrtime();
action();
const [endSec, endMSec] = process.hrtime();
return (endSec - startSec) * 1000 + (endMSec - startMSec) / 1000000;
}
const charCodeByCharCodeDuration = trapDuration(() => {
let count = 0;
for (let i = 0; i < themeCoreLightIos.length; i++) {
count += themeCoreLightIos.charCodeAt(i);
}
assert.equal(count, 1218711);
});
const charByCharDuration = trapDuration(() => {
let char;
for (let i = 0; i < themeCoreLightIos.length; i++) {
char = themeCoreLightIos.charAt(i);
}
assert.equal(char, "\n");
});
const compareCharIfDuration = trapDuration(() => {
let char;
let c = 0;
for (let i = 0; i < themeCoreLightIos.length; i++) {
const char = themeCoreLightIos[i];
if ((char >= "a" && char <= "z") || (char >= "A" && char <= "Z") || char === "_") {
c++;
}
}
assert.equal(c, 8774);
});
const compareCharRegEx = /[a-zA-Z_]/;
const compareCharRegExDuration = trapDuration(() => {
let char;
let c = 0;
for (let i = 0; i < themeCoreLightIos.length; i++) {
const char = themeCoreLightIos[i];
if (compareCharRegEx.test(char)) {
c++;
}
}
assert.equal(c, 8774);
});
const indexerDuration = trapDuration(() => {
let char;
for (let i = 0; i < themeCoreLightIos.length; i++) {
char = themeCoreLightIos[i];
}
assert.equal(char, "\n");
});
const reworkDuration = trapDuration(() => {
const ast = reworkCss.parse(themeCoreLightIos, { source: "nativescript-theme-core/css/core.light.css" });
// fs.writeFileSync("rework.css.json", JSON.stringify(ast, null, "\t"));
});
const shadyDuration = trapDuration(() => {
const shadyParser = new shadyCss.Parser();
const ast = shadyParser.parse(themeCoreLightIos);
// fs.writeFileSync("shady.css.json", JSON.stringify(ast, null, "\t"));
});
const parseCssDuration = trapDuration(() => {
const tokens = parseCss.tokenize(themeCoreLightIos);
const ast = parseCss.parseAStylesheet(tokens);
// fs.writeFileSync("parse.css.json", JSON.stringify(ast, null, "\t"));
});
const gonzalesDuration = trapDuration(() => {
const ast = gonzales.srcToCSSP(themeCoreLightIos);
});
const parserlibDuration = trapDuration(() => {
const parser = new parserlib.css.Parser({ starHack: true, underscoreHack: true });
const ast = parser.parse(themeCoreLightIos);
});
const csstreeDuration = trapDuration(() => {
const ast = csstree.parse(themeCoreLightIos);
});
const nativescriptToReworkAstDuration = trapDuration(() => {
const cssparser = new CSS3Parser(themeCoreLightIos);
const stylesheet = cssparser.parseAStylesheet();
const cssNS = new CSSNativeScript();
const ast = cssNS.parseStylesheet(stylesheet);
});
const nativescriptParseDuration = trapDuration(() => {
const cssparser = new CSS3Parser(themeCoreLightIos);
const stylesheet = cssparser.parseAStylesheet();
});
console.log(` * Baseline perf: .charCodeAt: ${charCodeByCharCodeDuration}ms. .charAt: ${charByCharDuration}ms. []:${indexerDuration}ms. compareCharIf: ${compareCharIfDuration} compareCharRegEx: ${compareCharRegExDuration}`);
console.log(` * Parsers perf: rework: ${reworkDuration}ms. shady: ${shadyDuration}ms. parse-css: ${parseCssDuration}ms. gonzalesDuration: ${gonzalesDuration} parserlib: ${parserlibDuration} csstree: ${csstreeDuration} nativescript-parse: ${nativescriptParseDuration}ms. nativescriptToReworkAst: ${nativescriptToReworkAstDuration}`);
assert.isAtMost(nativescriptParseDuration, reworkDuration / 3);
assert.isAtMost(nativescriptParseDuration, shadyDuration / 1.5);
});
});
});
});