From f7a3a36b9cdd47042ce145de2e94af6eea63bba1 Mon Sep 17 00:00:00 2001 From: Panayot Cankov Date: Fri, 20 Oct 2017 10:42:07 +0300 Subject: [PATCH] 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 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 --- .vscode/launch.json | 9 + .vscode/tasks.json | 20 +- apps/references.d.ts | 2 - gruntfile.js | 18 +- node-tests/definitions/chai.d.ts | 285 --- node-tests/definitions/mocha.d.ts | 112 -- package.json | 22 +- tests/app/testRunner.ts | 6 - tests/app/ui/styling/css-selector-parser.ts | 141 -- tests/app/ui/styling/css-selector.ts | 269 --- tests/references.d.ts | 2 - .../application/application-common.ts | 6 +- tns-core-modules/application/application.d.ts | 12 + .../application/application.ios.ts | 11 +- tns-core-modules/console/console.ts | 4 +- tns-core-modules/css/parser.ts | 1544 +++++++++++++++++ tns-core-modules/file-system/file-system.ts | 122 +- tns-core-modules/globals/globals.ts | 45 +- .../image-asset/image-asset-common.ts | 21 +- .../image-asset/image-asset.android.ts | 10 + .../image-asset/image-asset.ios.ts | 10 + tns-core-modules/module.d.ts | 21 + tns-core-modules/package.json | 1 + tns-core-modules/profiling/profiling.ts | 51 +- tns-core-modules/ui/builder/builder.ts | 2 +- .../component-builder/component-builder.ts | 2 +- .../ui/core/properties/properties.d.ts | 2 +- .../ui/core/view-base/view-base.ts | 5 +- tns-core-modules/ui/frame/frame-common.ts | 2 +- tns-core-modules/ui/frame/frame.android.ts | 1 + tns-core-modules/ui/frame/frame.ios.ts | 2 +- tns-core-modules/ui/page/page.ios.ts | 2 +- .../ui/styling/background.android.ts | 2 +- .../ui/styling/css-selector-parser.d.ts | 42 - .../ui/styling/css-selector-parser.ts | 125 -- .../ui/styling/css-selector/css-selector.ts | 102 +- tns-core-modules/ui/styling/font-common.ts | 16 +- tns-core-modules/ui/styling/font.android.ts | 2 +- tns-core-modules/ui/styling/font.ios.ts | 2 +- .../ui/styling/style-properties.ts | 4 +- tns-core-modules/ui/styling/style-scope.ts | 122 +- tns-core-modules/ui/styling/style/style.d.ts | 1 + tns-core-modules/ui/styling/style/style.ts | 1 + tns-core-modules/utils/debug.android.ts | 22 - tns-core-modules/utils/debug.ios.ts | 22 - .../utils/{debug-common.ts => debug.ts} | 21 + tsconfig.json | 10 +- tsconfig.node-tests.json | 16 - tsconfig.shared.json | 10 +- tsconfig.unit-tests.json | 18 + unit-tests/common-types.d.ts | 33 + unit-tests/css/assets/core.light.css | 6 + unit-tests/css/assets/what-is-new.ios.css | 23 + unit-tests/css/out/.gitignore | 3 + unit-tests/css/out/README.md | 2 + unit-tests/css/parser.ts | 461 +++++ unit-tests/mocha.opts | 4 + unit-tests/package.json | 3 + unit-tests/polyfills/file-system-access.ts | 7 + unit-tests/polyfills/platform.ts | 0 unit-tests/runtime.ts | 20 + unit-tests/ui/styling/css-selector.ts | 288 +++ .../xml}/test-angular-xml.ts | 3 +- {node-tests => unit-tests/xml}/test-xml.ts | 3 +- 64 files changed, 2875 insertions(+), 1281 deletions(-) delete mode 100644 node-tests/definitions/chai.d.ts delete mode 100644 node-tests/definitions/mocha.d.ts delete mode 100644 tests/app/ui/styling/css-selector-parser.ts delete mode 100644 tests/app/ui/styling/css-selector.ts create mode 100644 tns-core-modules/css/parser.ts delete mode 100644 tns-core-modules/ui/styling/css-selector-parser.d.ts delete mode 100644 tns-core-modules/ui/styling/css-selector-parser.ts delete mode 100644 tns-core-modules/utils/debug.android.ts delete mode 100644 tns-core-modules/utils/debug.ios.ts rename tns-core-modules/utils/{debug-common.ts => debug.ts} (63%) delete mode 100644 tsconfig.node-tests.json create mode 100644 tsconfig.unit-tests.json create mode 100644 unit-tests/common-types.d.ts create mode 100644 unit-tests/css/assets/core.light.css create mode 100644 unit-tests/css/assets/what-is-new.ios.css create mode 100644 unit-tests/css/out/.gitignore create mode 100644 unit-tests/css/out/README.md create mode 100644 unit-tests/css/parser.ts create mode 100644 unit-tests/mocha.opts create mode 100644 unit-tests/package.json create mode 100644 unit-tests/polyfills/file-system-access.ts create mode 100644 unit-tests/polyfills/platform.ts create mode 100644 unit-tests/runtime.ts create mode 100644 unit-tests/ui/styling/css-selector.ts rename {node-tests => unit-tests/xml}/test-angular-xml.ts (95%) rename {node-tests => unit-tests/xml}/test-xml.ts (92%) diff --git a/.vscode/launch.json b/.vscode/launch.json index abcaa7450..cec715adc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,15 @@ { "version": "0.2.0", "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Unit Tests", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "args": [ "--timeout", "999999", "--opts", "unit-tests/mocha.opts" ], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "tsc-unit-tests" + }, { "name": "Launch on iOS", "type": "nativescript", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3dcb37998..7dc223a06 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,5 +6,23 @@ "isShellCommand": true, "args": ["-p", "."], "showOutput": "always", - "problemMatcher": "$tsc" + "problemMatcher": "$tsc", + "tasks": [ + { + "taskName": "tsc-unit-tests", + "problemMatcher": "$tsc", + "command": "./node_modules/.bin/tsc", + "args": [ "-p", "tsconfig.unit-tests.json" ] + }, + { + "taskName": "unit-tests", + "command": "npm", + "args": ["run", "unit-test"] + }, + { + "taskName": "unit-tests-watch", + "command": "npm", + "args": ["run", "unit-test-watch"] + } + ] } \ No newline at end of file diff --git a/apps/references.d.ts b/apps/references.d.ts index e6228175d..81882673c 100644 --- a/apps/references.d.ts +++ b/apps/references.d.ts @@ -1,4 +1,2 @@ /// /// - -/// diff --git a/gruntfile.js b/gruntfile.js index ebb5b5437..f396f7bfb 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -109,7 +109,7 @@ module.exports = function(grunt) { var nodeTestEnv = JSON.parse(JSON.stringify(process.env)); nodeTestEnv.NODE_PATH = localCfg.outTnsCoreModules; - localCfg.nodeTestsDir = path.join(localCfg.outDir, 'node-tests'); + localCfg.nodeTestsDir = path.join(localCfg.outDir, 'unit-tests'); localCfg.mainPackageContent = grunt.file.readJSON(localCfg.packageJsonFilePath); localCfg.packageVersion = getPackageVersion(localCfg.packageJsonFilePath); localCfg.commitSHA = getCommitSha(); @@ -202,7 +202,7 @@ module.exports = function(grunt) { src: [ "**/*.d.ts", //Exclude the d.ts files in the apps folder - these are part of the apps and are already packed there! - "!node-tests/**", + "!unit-tests/**", "!org.nativescript.widgets.d.ts", "!android17.d.ts", "!**/*.android.d.ts", @@ -255,7 +255,7 @@ module.exports = function(grunt) { '**/*', '!*.md', '!node_modules/**/*', - '!node-tests/**/*', + '!unit-tests/**/*', ], cwd: localCfg.outDir, dest: "<%= grunt.option('path') %>/node_modules/tns-core-modules/", @@ -279,7 +279,7 @@ module.exports = function(grunt) { }, compileAll: "npm run compile-all", setupLinks: "npm run setup", - compileNodeTests: "npm run compile-node-tests", + runUnitTests: "npm run unit-test", tslint: "npm run tslint", }, simplemocha: { @@ -368,7 +368,7 @@ module.exports = function(grunt) { //aliasing pack-modules for backwards compatibility grunt.registerTask("pack-modules", [ "compile-modules", - "node-tests", + "run-unit-test", "copy:modulesPackageDef", "exec:packModules" ]); @@ -384,13 +384,9 @@ module.exports = function(grunt) { "copy:jsLibs", ]); - grunt.registerTask("node-tests", [ + grunt.registerTask("run-unit-test", [ "clean:nodeTests", - "shell:compileNodeTests", - "copy:childPackageFiles", - "copy:jsLibs", - "env:nodeTests", - "exec:mochaNode", //spawn a new process to use the new NODE_PATH + "shell:runUnitTests" ]); grunt.registerTask("apiref", [ diff --git a/node-tests/definitions/chai.d.ts b/node-tests/definitions/chai.d.ts deleted file mode 100644 index fae15cd05..000000000 --- a/node-tests/definitions/chai.d.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* tslint:disable */ - -// Type definitions for chai 1.7.2 -// Project: http://chaijs.com/ -// Definitions by: Jed Hunsaker , Bart van der Schoor -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -declare module chai { - export class AssertionError { - constructor(message: string, _props?: any, ssf?: Function); - name: string; - message: string; - showDiff: boolean; - stack: string; - } - - function expect(target: any, message?: string): Expect; - - export var assert: Assert; - export var config: Config; - - export interface Config { - includeStack: boolean; - } - - // Provides a way to extend the internals of Chai - function use(fn: (chai: any, utils: any) => void): any; - - interface ExpectStatic { - (target: any): Expect; - } - - interface Assertions { - attr(name: string, value?: string): any; - css(name: string, value?: string): any; - data(name: string, value?: string): any; - class(className: string): any; - id(id: string): any; - html(html: string): any; - text(text: string): any; - value(value: string): any; - visible: any; - hidden: any; - selected: any; - checked: any; - disabled: any; - empty: any; - exist: any; - } - - interface Expect extends LanguageChains, NumericComparison, TypeComparison, Assertions { - not: Expect; - deep: Deep; - a: TypeComparison; - an: TypeComparison; - include: Include; - contain: Include; - ok: Expect; - true: Expect; - false: Expect; - null: Expect; - undefined: Expect; - exist: Expect; - empty: Expect; - arguments: Expect; - Arguments: Expect; - equal: Equal; - equals: Equal; - eq: Equal; - eql: Equal; - eqls: Equal; - property: Property; - ownProperty: OwnProperty; - haveOwnProperty: OwnProperty; - length: Length; - lengthOf: Length; - match(RegularExpression: RegExp, message?: string): Expect; - string(string: string, message?: string): Expect; - keys: Keys; - key(string: string): Expect; - throw: Throw; - throws: Throw; - Throw: Throw; - respondTo(method: string, message?: string): Expect; - itself: Expect; - satisfy(matcher: Function, message?: string): Expect; - closeTo(expected: number, delta: number, message?: string): Expect; - members: Members; - } - - interface LanguageChains { - to: Expect; - be: Expect; - been: Expect; - is: Expect; - that: Expect; - and: Expect; - have: Expect; - with: Expect; - at: Expect; - of: Expect; - same: Expect; - } - - interface NumericComparison { - above: NumberComparer; - gt: NumberComparer; - greaterThan: NumberComparer; - least: NumberComparer; - gte: NumberComparer; - below: NumberComparer; - lt: NumberComparer; - lessThan: NumberComparer; - most: NumberComparer; - lte: NumberComparer; - within(start: number, finish: number, message?: string): Expect; - } - - interface NumberComparer { - (value: number, message?: string): Expect; - } - - interface TypeComparison { - (type: string, message?: string): Expect; - instanceof: InstanceOf; - instanceOf: InstanceOf; - } - - interface InstanceOf { - (constructor: Object, message?: string): Expect; - } - - interface Deep { - equal: Equal; - property: Property; - } - - interface Equal { - (value: any, message?: string): Expect; - } - - interface Property { - (name: string, value?: any, message?: string): Expect; - } - - interface OwnProperty { - (name: string, message?: string): Expect; - } - - interface Length extends LanguageChains, NumericComparison { - (length: number, message?: string): Expect; - } - - interface Include { - (value: Object, message?: string): Expect; - (value: string, message?: string): Expect; - (value: number, message?: string): Expect; - keys: Keys; - members: Members; - } - - interface Keys { - (...keys: string[]): Expect; - (keys: any[]): Expect; - } - - interface Members { - (set: any[], message?: string): Expect; - } - - interface Throw { - (): Expect; - (expected: string, message?: string): Expect; - (expected: RegExp, message?: string): Expect; - (constructor: Error, expected?: string, message?: string): Expect; - (constructor: Error, expected?: RegExp, message?: string): Expect; - (constructor: Function, expected?: string, message?: string): Expect; - (constructor: Function, expected?: RegExp, message?: string): Expect; - } - - export interface Assert { - (express: any, msg?: string):void; - - fail(actual?: any, expected?: any, msg?: string, operator?: string):void; - - ok(val: any, msg?: string):void; - notOk(val: any, msg?: string):void; - - equal(act: any, exp: any, msg?: string):void; - notEqual(act: any, exp: any, msg?: string):void; - - strictEqual(act: any, exp: any, msg?: string):void; - notStrictEqual(act: any, exp: any, msg?: string):void; - - deepEqual(act: any, exp: any, msg?: string):void; - notDeepEqual(act: any, exp: any, msg?: string):void; - - isTrue(val: any, msg?: string):void; - isFalse(val: any, msg?: string):void; - - isNull(val: any, msg?: string):void; - isNotNull(val: any, msg?: string):void; - - isUndefined(val: any, msg?: string):void; - isDefined(val: any, msg?: string):void; - - isFunction(val: any, msg?: string):void; - isNotFunction(val: any, msg?: string):void; - - isObject(val: any, msg?: string):void; - isNotObject(val: any, msg?: string):void; - - isArray(val: any, msg?: string):void; - isNotArray(val: any, msg?: string):void; - - isString(val: any, msg?: string):void; - isNotString(val: any, msg?: string):void; - - isNumber(val: any, msg?: string):void; - isNotNumber(val: any, msg?: string):void; - - isBoolean(val: any, msg?: string):void; - isNotBoolean(val: any, msg?: string):void; - - typeOf(val: any, type: string, msg?: string):void; - notTypeOf(val: any, type: string, msg?: string):void; - - instanceOf(val: any, type: Function, msg?: string):void; - notInstanceOf(val: any, type: Function, msg?: string):void; - - include(exp: string, inc: any, msg?: string):void; - include(exp: any[], inc: any, msg?: string):void; - - notInclude(exp: string, inc: any, msg?: string):void; - notInclude(exp: any[], inc: any, msg?: string):void; - - match(exp: any, re: RegExp, msg?: string):void; - notMatch(exp: any, re: RegExp, msg?: string):void; - - property(obj: Object, prop: string, msg?: string):void; - notProperty(obj: Object, prop: string, msg?: string):void; - deepProperty(obj: Object, prop: string, msg?: string):void; - notDeepProperty(obj: Object, prop: string, msg?: string):void; - - propertyVal(obj: Object, prop: string, val: any, msg?: string):void; - propertyNotVal(obj: Object, prop: string, val: any, msg?: string):void; - - deepPropertyVal(obj: Object, prop: string, val: any, msg?: string):void; - deepPropertyNotVal(obj: Object, prop: string, val: any, msg?: string):void; - - lengthOf(exp: any, len: number, msg?: string):void; - //alias frenzy - throw(fn: Function, msg?: string):void; - throw(fn: Function, regExp: RegExp):void; - throw(fn: Function, errType: Function, msg?: string):void; - throw(fn: Function, errType: Function, regExp: RegExp):void; - - throws(fn: Function, msg?: string):void; - throws(fn: Function, regExp: RegExp):void; - throws(fn: Function, errType: Function, msg?: string):void; - throws(fn: Function, errType: Function, regExp: RegExp):void; - - Throw(fn: Function, msg?: string):void; - Throw(fn: Function, regExp: RegExp):void; - Throw(fn: Function, errType: Function, msg?: string):void; - Throw(fn: Function, errType: Function, regExp: RegExp):void; - - doesNotThrow(fn: Function, msg?: string):void; - doesNotThrow(fn: Function, regExp: RegExp):void; - doesNotThrow(fn: Function, errType: Function, msg?: string):void; - doesNotThrow(fn: Function, errType: Function, regExp: RegExp):void; - - operator(val: any, operator: string, val2: any, msg?: string):void; - closeTo(act: number, exp: number, delta: number, msg?: string):void; - - sameMembers(set1: any[], set2: any[], msg?: string):void; - includeMembers(set1: any[], set2: any[], msg?: string):void; - - ifError(val: any, msg?: string):void; - } -} - -declare module "chai" { -export = chai; -} diff --git a/node-tests/definitions/mocha.d.ts b/node-tests/definitions/mocha.d.ts deleted file mode 100644 index 7cd5abcae..000000000 --- a/node-tests/definitions/mocha.d.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* tslint:disable */ - -// Type definitions for mocha 1.17.1 -// Project: http://visionmedia.github.io/mocha/ -// Definitions by: Kazi Manzur Rashid , otiai10 -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -interface Mocha { - // Setup mocha with the given setting options. - setup(options: MochaSetupOptions): Mocha; - - //Run tests and invoke `fn()` when complete. - run(callback?: () => void): void; - - // Set reporter as function - reporter(reporter: () => void): Mocha; - - // Set reporter, defaults to "dot" - reporter(reporter: string): Mocha; - - // Enable growl support. - growl(): Mocha -} - -interface MochaSetupOptions { - //milliseconds to wait before considering a test slow - slow?: number; - - // timeout in milliseconds - timeout?: number; - - // ui name "bdd", "tdd", "exports" etc - ui?: string; - - //array of accepted globals - globals?: any[]; - - // reporter instance (function or string), defaults to `mocha.reporters.Dot` - reporter?: any; - - // bail on the first test failure - bail?: Boolean; - - // ignore global leaks - ignoreLeaks?: Boolean; - - // grep string or regexp to filter tests with - grep?: any; -} - -interface MochaDone { - (error?: Error): void; -} - -declare var mocha: Mocha; - -declare var describe : { - (description: string, spec: () => void): void; - only(description: string, spec: () => void): void; - skip(description: string, spec: () => void): void; - timeout(ms: number): void; -} - -// alias for `describe` -declare var context : { - (contextTitle: string, spec: () => void): void; - only(contextTitle: string, spec: () => void): void; - skip(contextTitle: string, spec: () => void): void; - timeout(ms: number): void; -} - -declare var it: { - (expectation: string, assertion?: () => void): void; - (expectation: string, assertion?: (done: MochaDone) => void): void; - only(expectation: string, assertion?: () => void): void; - only(expectation: string, assertion?: (done: MochaDone) => void): void; - skip(expectation: string, assertion?: () => void): void; - skip(expectation: string, assertion?: (done: MochaDone) => void): void; - timeout(ms: number): void; -}; - -declare function before(action: () => void): void; - -declare function before(action: (done: MochaDone) => void): void; - -declare function setup(action: () => void): void; - -declare function setup(action: (done: MochaDone) => void): void; - -declare function after(action: () => void): void; - -declare function after(action: (done: MochaDone) => void): void; - -declare function teardown(action: () => void): void; - -declare function teardown(action: (done: MochaDone) => void): void; - -declare function beforeEach(action: () => void): void; - -declare function beforeEach(action: (done: MochaDone) => void): void; - -declare function suiteSetup(action: () => void): void; - -declare function suiteSetup(action: (done: MochaDone) => void): void; - -declare function afterEach(action: () => void): void; - -declare function afterEach(action: (done: MochaDone) => void): void; - -declare function suiteTeardown(action: () => void): void; - -declare function suiteTeardown(action: (done: MochaDone) => void): void; diff --git a/package.json b/package.json index 836e5849e..391a43358 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,14 @@ }, "license": "Apache-2.0", "devDependencies": { + "@types/chai": "^4.0.4", + "@types/mocha": "^2.2.42", "@types/node": "^7.0.5", - "chai": "3.2.0", + "chai": "^4.1.2", "concurrently": "^2.1.0", + "css": "^2.2.1", + "css-tree": "^1.0.0-alpha24", + "gonzales": "^1.0.7", "grunt": "1.0.1", "grunt-contrib-clean": "1.0.0", "grunt-contrib-copy": "git://github.com/NativeScript/grunt-contrib-copy.git#master", @@ -24,14 +29,21 @@ "http-server": "^0.9.0", "madge": "^2.0.0", "markdown-snippet-injector": "0.2.2", - "mocha": "2.2.5", + "mocha": "^3.5.0", + "mocha-typescript": "^1.1.9", + "module-alias": "^2.0.1", "nativescript-typedoc-theme": "git://github.com/NativeScript/nativescript-typedoc-theme.git#master", + "parse-css": "git+https://github.com/tabatkins/parse-css.git", + "parserlib": "^1.1.1", + "shady-css-parser": "^0.1.0", "shelljs": "^0.7.0", + "source-map-support": "^0.4.17", "time-grunt": "1.3.0", + "tslib": "^1.7.1", "tslint": "^5.4.3", "typedoc": "^0.5.10", "typedoc-plugin-external-module-name": "git://github.com/PanayotCankov/typedoc-plugin-external-module-name.git#with-js", - "typescript": "^2.4.1" + "typescript": "^2.5.2" }, "scripts": { "setup": "npm run dev-link-tns-platform-declarations && npm run dev-link-tns-core-modules && npm run dev-link-tests && npm run dev-link-apps", @@ -39,10 +51,12 @@ "ci": "tsc && npm run tslint && npm run ci-apps && npm run ci-tests", "ci-apps": "cd apps && npm i ../tns-core-modules ../tns-platform-declarations --save", "ci-tests": "cd tests && npm i ../tns-core-modules ../tns-platform-declarations --save", + "unit-test": "tsc -p tsconfig.unit-tests.json && mocha --opts unit-tests/mocha.opts", + "unit-test-watch": "mocha-typescript-watch -p tsconfig.unit-tests.json --opts unit-tests/mocha.opts", "compile-all": "npm run tsc -- -p tsconfig.json --outDir bin/dist", "compile-modules": "npm run tsc -- -p tsconfig-modules.json --outDir bin/dist", "compile-check-base-dts": "npm run tsc -- -p tsconfig.base-dts.json", - "compile-node-tests": "npm run tsc -- -p tsconfig.node-tests.json --outDir bin/dist/node-tests", + "compile-unit-tests": "npm run tsc -- -p tsconfig.unit-tests.json --outDir bin/dist/unit-tests", "compile-check-combined-dts": "npm run tsc -- -p tsconfig.combined-dts.json", "tsc-w": "npm run tsc -- --skipLibCheck -w", "dev-tsc-tns-platform-declarations": "npm run tsc -- -p tns-platform-declarations", diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index e13ae4fce..0213aea44 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -148,12 +148,6 @@ allTests["VISUAL-STATE"] = visualStateTests; import * as valueSourceTests from "./ui/styling/value-source-tests"; allTests["VALUE-SOURCE"] = valueSourceTests; -import * as cssSelectorParserTests from "./ui/styling/css-selector-parser"; -allTests["CSS-SELECTOR-PARSER"] = cssSelectorParserTests; - -import * as cssSelectorTests from "./ui/styling/css-selector"; -allTests["CSS-SELECTOR"] = cssSelectorTests; - import * as buttonTests from "./ui/button/button-tests"; allTests["BUTTON"] = buttonTests; diff --git a/tests/app/ui/styling/css-selector-parser.ts b/tests/app/ui/styling/css-selector-parser.ts deleted file mode 100644 index b64eef195..000000000 --- a/tests/app/ui/styling/css-selector-parser.ts +++ /dev/null @@ -1,141 +0,0 @@ -import * as parser from "tns-core-modules/ui/styling/css-selector-parser"; -import * as TKUnit from "../../TKUnit"; - -function test(css: string, expected: {}): void { - let result = parser.parse(css); - TKUnit.assertDeepEqual(result, expected); -} - -export function test_fairly_complex_selector(): void { - test(` listview#products.mark gridlayout:selected[row="2"] a> b > c >d>e *[src] `, [ - { pos: 2, type: "", ident: "listview" }, - { pos: 10, type: "#", ident: "products" }, - { pos: 19, type: ".", ident: "mark", comb: " " }, - { pos: 25, type: "", ident: "gridlayout" }, - { pos: 35, type: ":", ident: "selected" }, - { pos: 44, type: "[]", prop: "row", test: "=", value: "2", comb: " " }, - { pos: 54, type: "", ident: "a", comb: ">" }, - { pos: 57, type: "", ident: "b", comb: ">" }, - { pos: 63, type: "", ident: "c", comb: ">" }, - { pos: 66, type: "", ident: "d", comb: ">" }, - { pos: 68, type: "", ident: "e", comb: " " }, - { pos: 70, type: "*" }, - { pos: 71, type: "[]", prop: "src" } - ]); -} - -export function test_typeguard_isUniversal(): void { - let selector = parser.parse("*")[0]; - TKUnit.assertTrue(parser.isUniversal(selector)); - TKUnit.assertFalse(parser.isType(selector)); - TKUnit.assertFalse(parser.isClass(selector)); - TKUnit.assertFalse(parser.isId(selector)); - TKUnit.assertFalse(parser.isPseudo(selector)); - TKUnit.assertFalse(parser.isAttribute(selector)); -} -export function test_typeguard_isType(): void { - let selector = parser.parse("button")[0]; - TKUnit.assertFalse(parser.isUniversal(selector)); - TKUnit.assertTrue(parser.isType(selector)); - TKUnit.assertFalse(parser.isClass(selector)); - TKUnit.assertFalse(parser.isId(selector)); - TKUnit.assertFalse(parser.isPseudo(selector)); - TKUnit.assertFalse(parser.isAttribute(selector)); -} -export function test_typeguard_isClass(): void { - let selector = parser.parse(".login")[0]; - TKUnit.assertFalse(parser.isUniversal(selector)); - TKUnit.assertFalse(parser.isType(selector)); - TKUnit.assertTrue(parser.isClass(selector)); - TKUnit.assertFalse(parser.isId(selector)); - TKUnit.assertFalse(parser.isPseudo(selector)); - TKUnit.assertFalse(parser.isAttribute(selector)); -} -export function test_typeguard_isId(): void { - let selector = parser.parse("#login")[0]; - TKUnit.assertFalse(parser.isUniversal(selector)); - TKUnit.assertFalse(parser.isType(selector)); - TKUnit.assertFalse(parser.isClass(selector)); - TKUnit.assertTrue(parser.isId(selector)); - TKUnit.assertFalse(parser.isPseudo(selector)); - TKUnit.assertFalse(parser.isAttribute(selector)); -} -export function test_typeguard_isPseudo(): void { - let selector = parser.parse(":hover")[0]; - TKUnit.assertFalse(parser.isUniversal(selector)); - TKUnit.assertFalse(parser.isType(selector)); - TKUnit.assertFalse(parser.isClass(selector)); - TKUnit.assertFalse(parser.isId(selector)); - TKUnit.assertTrue(parser.isPseudo(selector)); - TKUnit.assertFalse(parser.isAttribute(selector)); -} -export function test_typeguard_isAttribute(): void { - let selector = parser.parse("[src]")[0]; - TKUnit.assertFalse(parser.isUniversal(selector)); - TKUnit.assertFalse(parser.isType(selector)); - TKUnit.assertFalse(parser.isClass(selector)); - TKUnit.assertFalse(parser.isId(selector)); - TKUnit.assertFalse(parser.isPseudo(selector)); - TKUnit.assertTrue(parser.isAttribute(selector)); -} - -export function test_universal_selector(): void { - test(`*`, [{ pos: 0, type: "*" }]); -} -export function test_type_selector(): void { - test(`button`, [{ pos: 0, type: "", ident: "button" }]); -} -export function test_class_selector(): void { - test(`.red`, [{ pos: 0, type: ".", ident: "red" }]); -} -export function test_id_selector(): void { - test(`#login`, [{ pos: 0, type: "#", ident: "login" }]); -} -export function test_pseudoClass(): void { - test(`:hover`, [{ pos: 0, type: ":", ident: "hover" }]); -} -export function test_attribute_no_value(): void { - test(`[src]`, [{ pos: 0, type: "[]", prop: "src" }]); -} -export function test_attribute_equal(): void { - test(`[src = "res://"]`, [{ pos: 0, type: "[]", prop: "src", test: "=", value: `res://` }]); -} -export function test_attribute_all_tests(): void { - ["=", "^=", "$=", "*=", "=", "~=", "|="].forEach(t => test(`[src ${t} "val"]`, [{ pos: 0, type: "[]", prop: "src", test: t, value: "val"}])); -} -export function test_direct_parent_comb(): void { - test(`listview > .image`, [ - { pos: 0, type: "", ident: "listview", comb: ">" }, - { pos: 11, type: ".", ident: "image" } - ]); -} -export function test_ancestor_comb(): void { - test(`listview .image`, [ - { pos: 0, type: "", ident: "listview", comb: " " }, - { pos: 10, type: ".", ident: "image" } - ]); -} -export function test_single_sequence(): void { - test(`button:hover`, [ - { pos: 0, type: "", ident: "button" }, - { pos: 6, type: ":", ident: "hover" } - ]); -} -export function test_multiple_sequences(): void { - test(`listview>:selected image.product`, [ - { pos: 0, type: "", ident: "listview", comb: ">" }, - { pos: 9, type: ":", ident: "selected", comb: " " }, - { pos: 19, type: "", ident: "image" }, - { pos: 24, type: ".", ident: "product" } - ]); -} -export function test_multiple_attribute_and_pseudo_classes(): void { - test(`button#login[user][pass]:focused:hovered`, [ - { pos: 0, type: "", ident: "button" }, - { pos: 6, type: "#", ident: "login" }, - { pos: 12, type: "[]", prop: "user" }, - { pos: 18, type: "[]", prop: "pass" }, - { pos: 24, type: ":", ident: "focused" }, - { pos: 32, type: ":", ident: "hovered" } - ]); -} diff --git a/tests/app/ui/styling/css-selector.ts b/tests/app/ui/styling/css-selector.ts deleted file mode 100644 index c5f4467b8..000000000 --- a/tests/app/ui/styling/css-selector.ts +++ /dev/null @@ -1,269 +0,0 @@ -import * as selector from "tns-core-modules/ui/styling/css-selector"; -import * as parser from "tns-core-modules/css"; -import * as TKUnit from "../../TKUnit"; - -function create(css: string, source: string = "css-selectors.ts@test"): { rules: selector.RuleSet[], map: selector.SelectorsMap } { - let parse = parser.parse(css, { source }); - let rulesAst = parse.stylesheet.rules.filter(n => n.type === "rule"); - let rules = selector.fromAstNodes(rulesAst); - let map = new selector.SelectorsMap(rules); - return { rules, map }; -} - -function createOne(css: string, source: string = "css-selectors.ts@test"): selector.RuleSet { - let {rules} = create(css, source); - TKUnit.assertEqual(rules.length, 1); - return rules[0]; -} - -export function test_single_selector() { - let rule = createOne(`* { color: red; }`); - TKUnit.assertTrue(rule.selectors[0].match({ cssType: "button" })); - TKUnit.assertTrue(rule.selectors[0].match({ cssType: "image" })); -} - -export function test_two_selectors() { - let rule = createOne(`button, image { color: red; }`); - TKUnit.assertTrue(rule.selectors[0].match({ cssType: "button" })); - TKUnit.assertTrue(rule.selectors[1].match({ cssType: "image" })); - TKUnit.assertFalse(rule.selectors[0].match({ cssType: "stacklayout" })); - TKUnit.assertFalse(rule.selectors[1].match({ cssType: "stacklayout" })); -} - -export function test_narrow_selection() { - let {map} = create(` - .login { color: blue; } - button { color: red; } - image { color: green; } - `); - - let buttonQuerry = map.query({ cssType: "button" }).selectors; - TKUnit.assertEqual(buttonQuerry.length, 1); - TKUnit.assertDeepSuperset(buttonQuerry[0].ruleset.declarations, [ - { property: "color", value: "red" } - ]); - - let imageQuerry = map.query({ cssType: "image", cssClasses: new Set(["login"]) }).selectors; - TKUnit.assertEqual(imageQuerry.length, 2); - // Note class before type - TKUnit.assertDeepSuperset(imageQuerry[0].ruleset.declarations, [ - { property: "color", value: "green" } - ]); - TKUnit.assertDeepSuperset(imageQuerry[1].ruleset.declarations, [ - { property: "color", value: "blue" } - ]); -} - -let positiveMatches = { - "*": (view) => true, - "type": (view) => view.cssType === "type", - "#id": (view) => view.id === "id", - ".class": (view) => view.cssClasses.has("class"), - ":pseudo": (view) => view.cssPseudoClasses.has("pseudo"), - "[src1]": (view) => "src1" in view, - "[src2='src-value']": (view) => view['src2'] === 'src-value' -} - -let positivelyMatchingView = { - cssType: "type", - id: "id", - cssClasses: new Set(["class"]), - cssPseudoClasses: new Set(["pseudo"]), - "src1": "src", - "src2": "src-value" -} - -let negativelyMatchingView = { - cssType: "nottype", - id: "notid", - cssClasses: new Set(["notclass"]), - cssPseudoClasses: new Set(["notpseudo"]), - // Has no "src1" - "src2": "not-src-value" -} - -export function test_simple_selectors_match() { - for (let sel in positiveMatches) { - let css = sel + " { color: red; }"; - let rule = createOne(css); - TKUnit.assertTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css); - if (sel !== "*") { - TKUnit.assertFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css); - } - } -} - -export function test_two_selector_sequence_positive_match() { - for (let firstStr in positiveMatches) { - for (let secondStr in positiveMatches) { - if (secondStr !== firstStr && secondStr !== "*" && secondStr !== "type") { - let css = firstStr + secondStr + " { color: red; }"; - let rule = createOne(css); - TKUnit.assertTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css); - if (firstStr !== "*") { - TKUnit.assertFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css); - } - } - } - } -} - -export function test_direct_parent_combinator() { - let rule = createOne(`listview > item:selected { color: red; }`); - TKUnit.assertTrue(rule.selectors[0].match({ - cssType: "item", - cssPseudoClasses: new Set(["selected"]), - parent: { - cssType: "listview" - } - }), "Item in list view expected to match"); - TKUnit.assertFalse(rule.selectors[0].match({ - cssType: "item", - cssPseudoClasses: new Set(["selected"]), - parent: { - cssType: "stacklayout", - parent: { - cssType: "listview" - } - } - }), "Item in stack in list view NOT expected to match."); -} - -export function test_ancestor_combinator() { - let rule = createOne(`listview item:selected { color: red; }`); - TKUnit.assertTrue(rule.selectors[0].match({ - cssType: "item", - cssPseudoClasses: new Set(["selected"]), - parent: { - cssType: "listview" - } - }), "Item in list view expected to match"); - TKUnit.assertTrue(rule.selectors[0].match({ - cssType: "item", - cssPseudoClasses: new Set(["selected"]), - parent: { - cssType: "stacklayout", - parent: { - cssType: "listview" - } - } - }), "Item in stack in list view expected to match."); - TKUnit.assertFalse(rule.selectors[0].match({ - cssType: "item", - cssPseudoClasses: new Set(["selected"]), - parent: { - cssType: "stacklayout", - parent: { - cssType: "page" - } - } - }), "Item in stack in page NOT expected to match."); -} - -export function test_backtracking_css_selector() { - let sel = createOne(`a>b c { color: red; }`).selectors[0]; - let child = { - cssType: "c", - parent: { - cssType: "b", - parent: { - cssType: "fail", - parent: { - cssType: "b", - parent: { - cssType: "a" - } - } - } - } - } - TKUnit.assertTrue(sel.match(child)); -} - -function toString() { return this.cssType; } - -export function test_simple_query_match() { - let {map} = create(`list grid[promotion] button:highlighted { color: red; }`); - - let list, grid, button; - - button = { - cssType: "button", - cssPseudoClasses: new Set(["highlighted"]), - toString, - parent: grid = { - cssType: "grid", - promotion: true, - toString, - parent: list = { - cssType: "list", - toString - } - } - } - - let match = map.query(button); - TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector."); - - let expected = new Map() - .set(grid, { attributes: new Set(["promotion"]) }) - .set(button, { pseudoClasses: new Set(["highlighted"]) }); - - TKUnit.assertDeepEqual(match.changeMap, expected); -} - -export function test_query_match_one_child_group() { - let {map} = create(`#prod[special] > gridlayout { color: red; }`); - let gridlayout, prod; - - gridlayout = { - cssType: "gridlayout", - toString, - parent: prod = { - id: "prod", - cssType: "listview", - toString - } - }; - - let match = map.query(gridlayout); - TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector."); - - let expected = new Map().set(prod, { attributes: new Set(["special"])} ); - TKUnit.assertDeepEqual(match.changeMap, expected); -} - -export function test_query_match_one_sibling_group() { - let {map} = create(`list button:highlighted+button:disabled { color: red; }`); - let list, button, disabledButton; - - list = { - cssType: "list", - toString, - getChildIndex: () => 1, - getChildAt: () => button - }; - - button = { - cssType: "button", - cssPseudoClasses: new Set(["highlighted"]), - toString, - parent: list - }; - - disabledButton = { - cssType: "button", - cssPseudoClasses: new Set(["disabled"]), - toString, - parent: list - }; - - let match = map.query(disabledButton); - TKUnit.assertEqual(match.selectors.length, 1, "Expected match to have one selector."); - - let expected = new Map() - .set(button, { pseudoClasses: new Set(["highlighted"]) }) - .set(disabledButton, { pseudoClasses: new Set(["disabled"]) }); - - TKUnit.assertDeepEqual(match.changeMap, expected); -} diff --git a/tests/references.d.ts b/tests/references.d.ts index e6228175d..81882673c 100644 --- a/tests/references.d.ts +++ b/tests/references.d.ts @@ -1,4 +1,2 @@ /// /// - -/// diff --git a/tns-core-modules/application/application-common.ts b/tns-core-modules/application/application-common.ts index 970f9287a..fe92b36aa 100644 --- a/tns-core-modules/application/application-common.ts +++ b/tns-core-modules/application/application-common.ts @@ -17,7 +17,7 @@ export function hasLaunched(): boolean { export { Observable }; -import { UnhandledErrorEventData, iOSApplication, AndroidApplication, CssChangedEventData } from "."; +import { UnhandledErrorEventData, iOSApplication, AndroidApplication, CssChangedEventData, LoadAppCSSEventData } from "."; export const launchEvent = "launch"; export const suspendEvent = "suspend"; @@ -66,6 +66,10 @@ export function getCssFileName(): string { return cssFile; } +export function loadAppCss(): void { + events.notify({ eventName: "loadAppCss", object: app, cssFile: getCssFileName() }); +} + export function addCss(cssText: string): void { events.notify({ eventName: "cssChanged", object: app, cssText: cssText }); } diff --git a/tns-core-modules/application/application.d.ts b/tns-core-modules/application/application.d.ts index 8f1f3bf9a..d20b00969 100644 --- a/tns-core-modules/application/application.d.ts +++ b/tns-core-modules/application/application.d.ts @@ -141,6 +141,14 @@ export function setCssFileName(cssFile: string): void; */ export function getCssFileName(): string; +/** + * Loads immediately the app.css. + * By default the app.css file is loaded shortly after "loaded". + * For the Android snapshot the CSS can be parsed during the snapshot generation, + * as the CSS does not depend on runtime APIs, and loadAppCss will be called explicitly. + */ +export function loadAppCss(); + export function addCss(cssText: string): void; /** @@ -553,3 +561,7 @@ export function getNativeApplication(): any; * Indicates if the application is allready launched. See also the `application.on("launch", handler)` event. */ export function hasLaunched(): boolean; + +export interface LoadAppCSSEventData extends EventData { + cssFile: string; +} \ No newline at end of file diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts index f02562686..01961488a 100644 --- a/tns-core-modules/application/application.ios.ts +++ b/tns-core-modules/application/application.ios.ts @@ -1,8 +1,14 @@ -import { iOSApplication as IOSApplicationDefinition, LaunchEventData, ApplicationEventData, OrientationChangedEventData } from "."; +import { + iOSApplication as IOSApplicationDefinition, + LaunchEventData, + ApplicationEventData, + OrientationChangedEventData, + LoadAppCSSEventData +} from "."; import { notify, launchEvent, resumeEvent, suspendEvent, exitEvent, lowMemoryEvent, - orientationChangedEvent, setApplication, livesync, displayedEvent + orientationChangedEvent, setApplication, livesync, displayedEvent, getCssFileName } from "./application-common"; // First reexport so that app module is initialized. @@ -117,6 +123,7 @@ class IOSApplication implements IOSApplicationDefinition { }; notify(args); + notify({ eventName: "loadAppCss", object: this, cssFile: getCssFileName() }); let rootView = createRootView(args.root); this._window.content = rootView; diff --git a/tns-core-modules/console/console.ts b/tns-core-modules/console/console.ts index 2557710be..f32858cc3 100644 --- a/tns-core-modules/console/console.ts +++ b/tns-core-modules/console/console.ts @@ -5,6 +5,8 @@ const enum MessageType { error = 3 } +declare function __time(): number; + function __message(message: any, level: string) { if ((global).__consoleMessage) { (global).__consoleMessage(message, level); @@ -227,7 +229,7 @@ export class Console { } private timeMillis() { - return java.lang.System.nanoTime() / 1000000; // 1 ms = 1000000 ns + return __time(); // 1 ms = 1000000 ns } public time(reportName: string): void { diff --git a/tns-core-modules/css/parser.ts b/tns-core-modules/css/parser.ts new file mode 100644 index 000000000..f067f5654 --- /dev/null +++ b/tns-core-modules/css/parser.ts @@ -0,0 +1,1544 @@ +export type Parsed = { start: number, end: number, value: V }; + +import * as reworkcss from "./reworkcss"; + +// Values +export type ARGB = number; +export type URL = string; +export type Angle = number; +export interface Unit { + value: number; + unit: string; +} +export type Length = Unit<"px" | "dip">; +export type Percentage = Unit<"%">; +export type LengthPercentage = Length | Percentage; +export type Keyword = string; +export interface ColorStop { + argb: ARGB; + offset?: LengthPercentage; +} +export interface LinearGradient { + angle: number; + colors: ColorStop[]; +} +export interface Background { + readonly color?: number; + readonly image?: URL | LinearGradient; + readonly repeat?: BackgroundRepeat; + readonly position?: BackgroundPosition; + readonly size?: BackgroundSize; +} +export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat"; +export type BackgroundSize = "auto" | "cover" | "contain" | { + x: LengthPercentage, + y: "auto" | LengthPercentage +} +export type HorizontalAlign = "left" | "center" | "right"; +export type VerticalAlign = "top" | "center" | "bottom"; +export interface HorizontalAlignWithOffset { + readonly align: "left" | "right"; + readonly offset: LengthPercentage; +} +export interface VerticalAlignWithOffset { + readonly align: "top" | "bottom"; + readonly offset: LengthPercentage +} +export interface BackgroundPosition { + readonly x: HorizontalAlign | HorizontalAlignWithOffset; + readonly y: VerticalAlign | VerticalAlignWithOffset; +} + +const urlRegEx = /\s*url\((?:('|")([^\1]*)\1|([^\)]*))\)\s*/gy; +export function parseURL(text: string, start: number = 0): Parsed { + urlRegEx.lastIndex = start; + const result = urlRegEx.exec(text); + if (!result) { + return null; + } + const end = urlRegEx.lastIndex; + const value: URL = result[2] || result[3]; + return { start, end, value }; +} + +const hexColorRegEx = /\s*#((?:[0-9A-F]{8})|(?:[0-9A-F]{6})|(?:[0-9A-F]{3}))\s*/giy; +export function parseHexColor(text: string, start: number = 0): Parsed { + hexColorRegEx.lastIndex = start; + const result = hexColorRegEx.exec(text); + if (!result) { + return null; + } + const end = hexColorRegEx.lastIndex; + let hex = result[1]; + let argb; + if (hex.length === 8) { + argb = parseInt("0x" + hex); + } else if (hex.length === 6) { + argb = parseInt("0xFF" + hex); + } else if (hex.length === 3) { + argb = parseInt("0xFF" + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]); + } + return { start, end, value: argb }; +} + +function rgbaToArgbNumber(r: number, g: number, b: number, a: number = 1): number | undefined { + if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255 && a >= 0 && a <= 1) { + return (Math.round(a * 0xFF) * 0x01000000) + (r * 0x010000) + (g * 0x000100) + (b * 0x000001); + } else { + return null; + } +} + +const rgbColorRegEx = /\s*(rgb\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*\))/gy; +export function parseRGBColor(text: string, start: number = 0): Parsed { + rgbColorRegEx.lastIndex = start; + const result = rgbColorRegEx.exec(text); + if (!result) { + return null; + } + const end = rgbColorRegEx.lastIndex; + const value = result[1] && rgbaToArgbNumber(parseInt(result[2]), parseInt(result[3]), parseInt(result[4])); + return { start, end, value }; +} + +const rgbaColorRegEx = /\s*(rgba\(\s*(\d*)\s*,\s*(\d*)\s*,\s*(\d*)\s*,\s*([01]?\.?\d*)\s*\))/gy; +export function parseRGBAColor(text: string, start: number = 0): Parsed { + rgbaColorRegEx.lastIndex = start; + const result = rgbaColorRegEx.exec(text); + if (!result) { + return null; + } + const end = rgbaColorRegEx.lastIndex; + const value = rgbaToArgbNumber(parseInt(result[2]), parseInt(result[3]), parseInt(result[4]), parseFloat(result[5])); + return { start, end, value }; +} + +export enum colors { + transparent = 0x00000000, + aliceblue = 0xFFF0F8FF, + antiquewhite = 0xFFFAEBD7, + aqua = 0xFF00FFFF, + aquamarine = 0xFF7FFFD4, + azure = 0xFFF0FFFF, + beige = 0xFFF5F5DC, + bisque = 0xFFFFE4C4, + black = 0xFF000000, + blanchedalmond = 0xFFFFEBCD, + blue = 0xFF0000FF, + blueviolet = 0xFF8A2BE2, + brown = 0xFFA52A2A, + burlywood = 0xFFDEB887, + cadetblue = 0xFF5F9EA0, + chartreuse = 0xFF7FFF00, + chocolate = 0xFFD2691E, + coral = 0xFFFF7F50, + cornflowerblue = 0xFF6495ED, + cornsilk = 0xFFFFF8DC, + crimson = 0xFFDC143C, + cyan = 0xFF00FFFF, + darkblue = 0xFF00008B, + darkcyan = 0xFF008B8B, + darkgoldenrod = 0xFFB8860B, + darkgray = 0xFFA9A9A9, + darkgreen = 0xFF006400, + darkgrey = 0xFFA9A9A9, + darkkhaki = 0xFFBDB76B, + darkmagenta = 0xFF8B008B, + darkolivegreen = 0xFF556B2F, + darkorange = 0xFFFF8C00, + darkorchid = 0xFF9932CC, + darkred = 0xFF8B0000, + darksalmon = 0xFFE9967A, + darkseagreen = 0xFF8FBC8F, + darkslateblue = 0xFF483D8B, + darkslategray = 0xFF2F4F4F, + darkslategrey = 0xFF2F4F4F, + darkturquoise = 0xFF00CED1, + darkviolet = 0xFF9400D3, + deeppink = 0xFFFF1493, + deepskyblue = 0xFF00BFFF, + dimgray = 0xFF696969, + dimgrey = 0xFF696969, + dodgerblue = 0xFF1E90FF, + firebrick = 0xFFB22222, + floralwhite = 0xFFFFFAF0, + forestgreen = 0xFF228B22, + fuchsia = 0xFFFF00FF, + gainsboro = 0xFFDCDCDC, + ghostwhite = 0xFFF8F8FF, + gold = 0xFFFFD700, + goldenrod = 0xFFDAA520, + gray = 0xFF808080, + green = 0xFF008000, + greenyellow = 0xFFADFF2F, + grey = 0xFF808080, + honeydew = 0xFFF0FFF0, + hotpink = 0xFFFF69B4, + indianred = 0xFFCD5C5C, + indigo = 0xFF4B0082, + ivory = 0xFFFFFFF0, + khaki = 0xFFF0E68C, + lavender = 0xFFE6E6FA, + lavenderblush = 0xFFFFF0F5, + lawngreen = 0xFF7CFC00, + lemonchiffon = 0xFFFFFACD, + lightblue = 0xFFADD8E6, + lightcoral = 0xFFF08080, + lightcyan = 0xFFE0FFFF, + lightgoldenrodyellow = 0xFFFAFAD2, + lightgray = 0xFFD3D3D3, + lightgreen = 0xFF90EE90, + lightgrey = 0xFFD3D3D3, + lightpink = 0xFFFFB6C1, + lightsalmon = 0xFFFFA07A, + lightseagreen = 0xFF20B2AA, + lightskyblue = 0xFF87CEFA, + lightslategray = 0xFF778899, + lightslategrey = 0xFF778899, + lightsteelblue = 0xFFB0C4DE, + lightyellow = 0xFFFFFFE0, + lime = 0xFF00FF00, + limegreen = 0xFF32CD32, + linen = 0xFFFAF0E6, + magenta = 0xFFFF00FF, + maroon = 0xFF800000, + mediumaquamarine = 0xFF66CDAA, + mediumblue = 0xFF0000CD, + mediumorchid = 0xFFBA55D3, + mediumpurple = 0xFF9370DB, + mediumseagreen = 0xFF3CB371, + mediumslateblue = 0xFF7B68EE, + mediumspringgreen = 0xFF00FA9A, + mediumturquoise = 0xFF48D1CC, + mediumvioletred = 0xFFC71585, + midnightblue = 0xFF191970, + mintcream = 0xFFF5FFFA, + mistyrose = 0xFFFFE4E1, + moccasin = 0xFFFFE4B5, + navajowhite = 0xFFFFDEAD, + navy = 0xFF000080, + oldlace = 0xFFFDF5E6, + olive = 0xFF808000, + olivedrab = 0xFF6B8E23, + orange = 0xFFFFA500, + orangered = 0xFFFF4500, + orchid = 0xFFDA70D6, + palegoldenrod = 0xFFEEE8AA, + palegreen = 0xFF98FB98, + paleturquoise = 0xFFAFEEEE, + palevioletred = 0xFFDB7093, + papayawhip = 0xFFFFEFD5, + peachpuff = 0xFFFFDAB9, + peru = 0xFFCD853F, + pink = 0xFFFFC0CB, + plum = 0xFFDDA0DD, + powderblue = 0xFFB0E0E6, + purple = 0xFF800080, + red = 0xFFFF0000, + rosybrown = 0xFFBC8F8F, + royalblue = 0xFF4169E1, + saddlebrown = 0xFF8B4513, + salmon = 0xFFFA8072, + sandybrown = 0xFFF4A460, + seagreen = 0xFF2E8B57, + seashell = 0xFFFFF5EE, + sienna = 0xFFA0522D, + silver = 0xFFC0C0C0, + skyblue = 0xFF87CEEB, + slateblue = 0xFF6A5ACD, + slategray = 0xFF708090, + slategrey = 0xFF708090, + snow = 0xFFFFFAFA, + springgreen = 0xFF00FF7F, + steelblue = 0xFF4682B4, + tan = 0xFFD2B48C, + teal = 0xFF008080, + thistle = 0xFFD8BFD8, + tomato = 0xFFFF6347, + turquoise = 0xFF40E0D0, + violet = 0xFFEE82EE, + wheat = 0xFFF5DEB3, + white = 0xFFFFFFFF, + whitesmoke = 0xFFF5F5F5, + yellow = 0xFFFFFF00, + yellowgreen = 0xFF9ACD32 +}; + +export function parseColorKeyword(value, start: number, keyword = parseKeyword(value, start)): Parsed { + if (keyword && keyword.value in colors) { + const end = keyword.end; + const value = colors[keyword.value]; + return { start, end, value }; + } + return null; +} + +export function parseColor(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed { + return parseHexColor(value, start) || parseColorKeyword(value, start, keyword) || parseRGBColor(value, start) || parseRGBAColor(value, start); +} + +const keywordRegEx = /\s*([a-z][\w\-]*)\s*/giy; +function parseKeyword(text: string, start: number = 0): Parsed { + keywordRegEx.lastIndex = start; + const result = keywordRegEx.exec(text); + if (!result) { + return null; + } + const end = keywordRegEx.lastIndex; + const value = result[1]; + return { start, end, value } +} + +const backgroundRepeatKeywords = new Set([ "repeat", "repeat-x", "repeat-y", "no-repeat" ]); +export function parseRepeat(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed { + if (keyword && backgroundRepeatKeywords.has(keyword.value)) { + const end = keyword.end; + const value = keyword.value; + return { start, end, value }; + } + return null; +} + +const unitRegEx = /\s*([\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?)([a-zA-Z]+|%)?\s*/gy; +export function parseUnit(text: string, start: number = 0): Parsed> { + unitRegEx.lastIndex = start; + const result = unitRegEx.exec(text); + if (!result) { + return null; + } + const end = unitRegEx.lastIndex; + const value = parseFloat(result[1]); + const unit = result[2] || "dip"; + return { start, end, value: { value, unit }}; +} + +export function parsePercentageOrLength(text: string, start: number = 0): Parsed { + const unitResult = parseUnit(text, start); + if (unitResult) { + const { start, end } = unitResult; + const value = unitResult.value; + if (value.unit === "%") { + value.value /= 100; + } else if (!value.unit) { + value.unit = "dip"; + } else if (value.unit === "px" || value.unit === "dip") { + // same + } else { + return null; + } + return { start, end, value }; + } + return null; +} + +const angleUnitsToRadMap: { [unit: string]: (start: number, end: number, value: number) => Parsed } = { + "deg": (start: number, end: number, deg: number) => ({ start, end, value: deg / 180 * Math.PI }), + "rad": (start: number, end: number, rad: number) => ({ start, end, value: rad }), + "grad": (start: number, end: number, grad: number) => ({ start, end, value: grad / 200 * Math.PI }), + "turn": (start: number, end: number, turn: number) => ({ start, end, value: turn * Math.PI * 2 }) +} +export function parseAngle(value: string, start: number = 0): Parsed { + const angleResult = parseUnit(value, start); + if (angleResult) { + const { start, end, value } = angleResult; + return (angleUnitsToRadMap[value.unit] || (() => null))(start, end, value.value); + } + return null; +} + +const backgroundSizeKeywords = new Set(["auto", "contain", "cover"]); +export function parseBackgroundSize(value: string, start: number = 0, keyword = parseKeyword(value, start)): Parsed { + let end = start; + if (keyword && backgroundSizeKeywords.has(keyword.value)) { + end = keyword.end; + const value = <"auto" | "cover" | "contain">keyword.value; + return { start, end, value }; + } + + // Parse one or two lengths... the other will be "auto" + const firstLength = parsePercentageOrLength(value, end); + if (firstLength) { + end = firstLength.end; + const secondLength = parsePercentageOrLength(value, firstLength.end); + if (secondLength) { + end = secondLength.end; + return { start, end, value: { x: firstLength.value, y: secondLength.value }}; + } else { + return { start, end, value: { x: firstLength.value, y: "auto" }}; + } + } + return null; +} + +const backgroundPositionKeywords = Object.freeze(new Set([ "left", "right", "top", "bottom", "center" ])); +const backgroundPositionKeywordsDirection: {[align: string]: "x" | "center" | "y" } = { + "left": "x", + "right": "x", + "center": "center", + "top": "y", + "bottom": "y" +} +export function parseBackgroundPosition(text: string, start: number = 0, keyword = parseKeyword(text, start)): Parsed { + function formatH(align: Parsed, offset: Parsed) { + if (align.value === "center") { + return "center"; + } + if (offset && offset.value.value !== 0) { + return { align: align.value, offset: offset.value }; + } + return align.value; + } + function formatV(align: Parsed, offset: Parsed) { + if (align.value === "center") { + return "center"; + } + if (offset && offset.value.value !== 0) { + return { align: align.value, offset: offset.value }; + } + return align.value; + } + let end = start; + if (keyword && backgroundPositionKeywords.has(keyword.value)) { + end = keyword.end; + let firstDirection = backgroundPositionKeywordsDirection[keyword.value]; + + const firstLength = firstDirection !== "center" && parsePercentageOrLength(text, end); + if (firstLength) { + end = firstLength.end; + } + + const secondKeyword = parseKeyword(text, end); + if (secondKeyword && backgroundPositionKeywords.has(secondKeyword.value)) { + end = secondKeyword.end; + let secondDirection = backgroundPositionKeywordsDirection[secondKeyword.end]; + + if (firstDirection === secondDirection && firstDirection !== "center") { + return null; // Reject pair of both horizontal or both vertical alignments. + } + + const secondLength = secondDirection !== "center" && parsePercentageOrLength(text, end); + if (secondLength) { + end = secondLength.end; + } + + if ((firstDirection === secondDirection && secondDirection === "center") || (firstDirection === "x" || secondDirection === "y")) { + return { start, end, value: { + x: formatH(>keyword, firstLength), + y: formatV(>secondKeyword, secondLength) + }}; + } else { + return { start, end, value: { + x: formatH(>secondKeyword, secondLength), + y: formatV(>keyword, firstLength), + }}; + } + } else { + if (firstDirection === "center") { + return { start, end, value: { x: "center", y: "center" }}; + } else if (firstDirection === "x") { + return { start, end, value: { x: formatH(>keyword, firstLength), y: "center" }}; + } else { + return { start, end, value: { x: "center", y: formatV(>keyword, firstLength) }}; + } + } + } else { + const firstLength = parsePercentageOrLength(text, end); + if (firstLength) { + end = firstLength.end; + const secondLength = parsePercentageOrLength(text, end); + if (secondLength) { + end = secondLength.end; + return { start, end, value: { x: { align: "left", offset: firstLength.value }, y: { align: "top", offset: secondLength.value }}}; + } else { + return { start, end, value: { x: { align: "left", offset: firstLength.value }, y: "center" }}; + } + } else { + return null; + } + } +} + +const directionRegEx = /\s*to\s*(left|right|top|bottom)\s*(left|right|top|bottom)?\s*/gy; +const sideDirections = { + top: Math.PI * 0/2, + right: Math.PI * 1/2, + bottom: Math.PI * 2/2, + left: Math.PI * 3/2 +} +const cornerDirections = { + top: { + right: Math.PI * 1/4, + left: Math.PI * 7/4 + }, + right: { + top: Math.PI * 1/4, + bottom: Math.PI * 3/4 + }, + bottom: { + right: Math.PI * 3/4, + left: Math.PI * 5/4 + }, + left: { + top: Math.PI * 7/4, + bottom: Math.PI * 5/4 + } +} +function parseDirection(text: string, start: number = 0): Parsed { + directionRegEx.lastIndex = start; + const result = directionRegEx.exec(text); + if (!result) { + return null; + } + const end = directionRegEx.lastIndex; + const firstDirection = result[1]; + if (result[2]) { + const secondDirection = result[2]; + const value = cornerDirections[firstDirection][secondDirection]; + return value === undefined ? null : { start, end, value }; + } else { + return { start, end, value: sideDirections[firstDirection] } + } +} + +const openingBracketRegEx = /\s*\(\s*/gy; +const closingBracketRegEx = /\s*\)\s*/gy; +const closingBracketOrCommaRegEx = /\s*(\)|,)\s*/gy; +function parseArgumentsList(text: string, start: number, argument: (value: string, lastIndex: number, index: number) => Parsed): Parsed[]> { + openingBracketRegEx.lastIndex = start; + const openingBracket = openingBracketRegEx.exec(text); + if (!openingBracket) { + return null; + } + let end = openingBracketRegEx.lastIndex; + const value: Parsed[] = []; + + closingBracketRegEx.lastIndex = end; + const closingBracket = closingBracketRegEx.exec(text); + if (closingBracket) { + return { start, end, value }; + } + + for(var index = 0; true; index++) { + const arg = argument(text, end, index); + if (!arg) { + return null; + } + end = arg.end; + value.push(arg); + + closingBracketOrCommaRegEx.lastIndex = end; + const closingBracketOrComma = closingBracketOrCommaRegEx.exec(text); + if (closingBracketOrComma) { + end = closingBracketOrCommaRegEx.lastIndex; + if (closingBracketOrComma[1] === ",") { + continue; + } else if (closingBracketOrComma[1] === ")") { + return { start, end, value }; + } + } else { + return null; + } + } +} + +export function parseColorStop(text: string, start: number = 0): Parsed { + const color = parseColor(text, start); + if (!color) { + return null; + } + let end = color.end; + const offset = parsePercentageOrLength(text, end); + if (offset) { + end = offset.end; + return { start, end, value: { argb: color.value, offset: offset.value }}; + } + return { start, end, value: { argb: color.value }}; +} + +const linearGradientStartRegEx = /\s*linear-gradient\s*/gy; +export function parseLinearGradient(text: string, start: number = 0): Parsed { + linearGradientStartRegEx.lastIndex = start; + const lgs = linearGradientStartRegEx.exec(text); + if (!lgs) { + return null; + } + let end = linearGradientStartRegEx.lastIndex; + + let angle = Math.PI; + const colors: ColorStop[] = []; + + const parsedArgs = parseArgumentsList(text, end, (text, start, index) => { + if (index === 0) { + // First arg can be gradient direction + const angleArg = parseAngle(text, start) || parseDirection(text, start); + if (angleArg) { + angle = angleArg.value; + return angleArg; + } + } + + const colorStop = parseColorStop(text, start); + if (colorStop) { + colors.push(colorStop.value); + return colorStop; + } + + return null; + }); + if (!parsedArgs) { + return null; + } + end = parsedArgs.end; + + return { start, end, value: { angle, colors }}; +} + +const slashRegEx = /\s*(\/)\s*/gy; +function parseSlash(text: string, start: number): Parsed<"/"> { + slashRegEx.lastIndex = start; + const slash = slashRegEx.exec(text); + if (!slash) { + return null; + } + const end = slashRegEx.lastIndex; + return { start, end, value: "/" }; +} + +export function parseBackground(text: string, start: number = 0): Parsed { + const value: any = {}; + let end = start; + while(end < text.length) { + const keyword = parseKeyword(text, end); + const color = parseColor(text, end, keyword); + if (color) { + value.color = color.value; + end = color.end; + continue; + } + const repeat = parseRepeat(text, end, keyword); + if (repeat) { + value.repeat = repeat.value; + end = repeat.end; + continue; + } + const position = parseBackgroundPosition(text, end, keyword); + if (position) { + value.position = position.value; + end = position.end; + + const slash = parseSlash(text, end); + if (slash) { + end = slash.end; + const size = parseBackgroundSize(text, end); + if (!size) { + // Found / but no proper size following + return null; + } + value.size = size.value; + end = size.end; + } + continue; + } + + const url = parseURL(text, end); + if (url) { + value.image = url.value; + end = url.end; + continue; + } + const gradient = parseLinearGradient(text, end); + if (gradient) { + value.image = gradient.value; + end = gradient.end; + continue; + } + + return null; + } + return { start, end, value }; +} + +// Selectors + +export type Combinator = "+" | "~" | ">" | " "; + +export interface UniversalSelector { + type: "*"; +} +export interface TypeSelector { + type: ""; + identifier: string; +} +export interface ClassSelector { + type: "."; + identifier: string; +} +export interface IdSelector { + type: "#"; + identifier: string; +} +export interface PseudoClassSelector { + type: ":"; + identifier: string; +} +export type AttributeSelectorTest = "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|="; +export interface AttributeSelector { + type: "[]"; + property: string; + test?: AttributeSelectorTest; + value?: string; +} + +export type SimpleSelector = UniversalSelector | TypeSelector | ClassSelector | IdSelector | PseudoClassSelector | AttributeSelector; +export type SimpleSelectorSequence = SimpleSelector[]; +export type Selector = [SimpleSelectorSequence, Combinator]; + +const universalSelectorRegEx = /\*/gy; +export function parseUniversalSelector(text: string, start: number = 0): Parsed { + universalSelectorRegEx.lastIndex = start; + const result = universalSelectorRegEx.exec(text); + if (!result) { + return null; + } + const end = universalSelectorRegEx.lastIndex; + return { start, end, value: { type: "*" }}; +} + +const simpleIdentifierSelectorRegEx = /(#|\.|:|\b)([_-\w][_-\w\d]*)/gy; +export function parseSimpleIdentifierSelector(text: string, start: number = 0): Parsed { + simpleIdentifierSelectorRegEx.lastIndex = start; + const result = simpleIdentifierSelectorRegEx.exec(text); + if (!result) { + return null; + } + const end = simpleIdentifierSelectorRegEx.lastIndex; + const type = <"#" | "." | ":" | "">result[1]; + const identifier: string = result[2]; + const value = { type, identifier }; + return { start, end, value }; +} + +const attributeSelectorRegEx = /\[\s*([_-\w][_-\w\d]*)\s*(?:(=|\^=|\$=|\*=|\~=|\|=)\s*(?:([_-\w][_-\w\d]*)|"((?:[^\\"]|\\(?:"|n|r|f|\\|0-9a-f))*)"|'((?:[^\\']|\\(?:'|n|r|f|\\|0-9a-f))*)')\s*)?\]/gy; +export function parseAttributeSelector(text: string, start: number): Parsed { + attributeSelectorRegEx.lastIndex = start; + const result = attributeSelectorRegEx.exec(text); + if (!result) { + return null; + } + const end = attributeSelectorRegEx.lastIndex; + const property = result[1]; + if (result[2]) { + const test = result[2]; + const value = result[3] || result[4] || result[5]; + return { start, end, value: { type: "[]", property, test, value }}; + } + return { start, end, value: { type: "[]", property }}; +} + +export function parseSimpleSelector(text: string, start: number = 0): Parsed { + return parseUniversalSelector(text, start) || + parseSimpleIdentifierSelector(text, start) || + parseAttributeSelector(text, start); +} + +export function parseSimpleSelectorSequence(text: string, start: number): Parsed { + let simpleSelector = parseSimpleSelector(text, start); + if (!simpleSelector) { + return null; + } + let end = simpleSelector.end; + let value = []; + while(simpleSelector) { + value.push(simpleSelector.value); + end = simpleSelector.end; + simpleSelector = parseSimpleSelector(text, end); + } + return { start, end, value } +} + +const combinatorRegEx = /\s*(\+|~|>)?\s*/gy; +export function parseCombinator(text: string, start: number = 0): Parsed { + combinatorRegEx.lastIndex = start; + const result = combinatorRegEx.exec(text); + if (!result) { + return null; + } + const end = combinatorRegEx.lastIndex; + const value = result[1] || " "; + return { start, end, value } +} + +const whiteSpaceRegEx = /\s*/gy; +export function parseSelector(text: string, start: number = 0): Parsed { + let end = start; + whiteSpaceRegEx.lastIndex = end; + const leadingWhiteSpace = whiteSpaceRegEx.exec(text); + if (leadingWhiteSpace) { + end = whiteSpaceRegEx.lastIndex; + } + let value = []; + let combinator: Parsed; + let expectSimpleSelector = true; // Must have at least one + do { + const simpleSelectorSequence = parseSimpleSelectorSequence(text, end); + if (!simpleSelectorSequence) { + if (expectSimpleSelector) { + return null; + } else { + break; + } + } + end = simpleSelectorSequence.end; + if (combinator) { + value.push(combinator.value); + } + value.push(simpleSelectorSequence.value); + combinator = parseCombinator(text, end); + if (combinator) { + end = combinator.end; + } + expectSimpleSelector = combinator && combinator.value !== " "; // Simple selector must follow non trailing white space combinator + } while(combinator); + value.push(undefined); + return { start, end, value }; +} + +export interface Stylesheet { + rules: Rule[]; +} +export type Rule = QualifiedRule | AtRule; + +export interface AtRule { + type: "at-rule"; + name: string; + prelude: InputToken[]; + block: SimpleBlock; +} +export interface QualifiedRule { + type: "qualified-rule"; + prelude: InputToken[]; + block: SimpleBlock; +} + +const whitespaceRegEx = /[\s\t\n\r\f]*/gym; + +const singleQuoteStringRegEx = /'((?:[^\n\r\f\']|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?'|$)/gym; // Besides $n, parse escape +const doubleQuoteStringRegEx = /"((?:[^\n\r\f\"]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)(:?"|$)/gym; // Besides $n, parse escape + +const commentRegEx = /(\/\*(?:[^\*]|\*[^\/])*\*\/)/gym; +const numberRegEx = /[\+\-]?(?:\d+\.\d+|\d+|\.\d+)(?:[eE][\+\-]?\d+)?/gym; +const nameRegEx = /-?(?:(?:[a-zA-Z_]|[^\x00-\x7F]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))(?:[a-zA-Z_0-9\-]*|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*)/gym; +const nonQuoteURLRegEx = /(:?[^\)\s\t\n\r\f\'\"\(]|\\(?:\$|\n|[0-9a-fA-F]{1,6}\s?))*/gym; // TODO: non-printable code points omitted + +type InputToken = "(" | ")" | "{" | "}" | "[" | "]" | ":" | ";" | "," | " " | "^=" | "|=" | "$=" | "*=" | "~=" | "" | undefined /* */ | InputTokenObject | FunctionInputToken | FunctionToken | SimpleBlock | AtKeywordToken; + +export const enum TokenObjectType { + /** + * + */ + string = 1, + /** + * + */ + delim = 2, + /** + * + */ + number = 3, + /** + * + */ + percentage = 4, + /** + * + */ + dimension = 5, + /** + * + */ + ident = 6, + /** + * + */ + url = 7, + /** + * + * This is a token indicating a function's leading: ( + */ + functionToken = 8, + /** + * + */ + simpleBlock = 9, + /** + * + */ + comment = 10, + /** + * + */ + atKeyword = 11, + /** + * + */ + hash = 12, + /** + * + * This is a complete consumed function: ([ [, ]*])")" + */ + function = 14, +} + +interface InputTokenObject { + type: TokenObjectType; + text: string; +} + +/** + * This is a "(" token. + */ +interface FunctionInputToken extends InputTokenObject { + name: string; +} + +/** + * This is a completely parsed function like "([component [, component]*])". + */ +interface FunctionToken extends FunctionInputToken { + components: any[]; +} + +interface SimpleBlock extends InputTokenObject { + associatedToken: InputToken; + values: InputToken[]; +} + +interface AtKeywordToken extends InputTokenObject {} + +/** + * CSS parser following relatively close: + * CSS Syntax Module Level 3 + * https://www.w3.org/TR/css-syntax-3/ + */ +export class CSS3Parser { + private nextInputCodePointIndex = 0; + private reconsumedInputToken: InputToken; + private topLevelFlag: boolean; + + constructor(private text: string) {} + + /** + * For testing purposes. + * This method allows us to run and assert the proper working of the tokenizer. + */ + tokenize(): InputToken[] { + let tokens: InputToken[] = []; + let inputToken: InputToken; + do { + inputToken = this.consumeAToken(); + tokens.push(inputToken); + } while(inputToken); + return tokens; + } + + /** + * 4.3.1. Consume a token + * https://www.w3.org/TR/css-syntax-3/#consume-a-token + */ + private consumeAToken(): InputToken { + if (this.reconsumedInputToken) { + let result = this.reconsumedInputToken; + this.reconsumedInputToken = null; + return result; + } + const char = this.text[this.nextInputCodePointIndex]; + switch(char) { + case "\"": return this.consumeAStringToken(); + case "'": return this.consumeAStringToken(); + case "(": + case ")": + case ",": + case ":": + case ";": + case "[": + case "]": + case "{": + case "}": + this.nextInputCodePointIndex++; + return char; + case "#": return this.consumeAHashToken() || this.consumeADelimToken(); + case " ": + case "\t": + case "\n": + case "\r": + case "\f": + return this.consumeAWhitespace(); + case "@": return this.consumeAtKeyword() || this.consumeADelimToken(); + // TODO: Only if this is valid escape, otherwise it is a parse error + case "\\": return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + case "0": + case "1": + case "2": + case "3": + case "4": + case "5": + case "6": + case "7": + case "8": + case "9": + return this.consumeANumericToken(); + case "u": + case "U": + if (this.text[this.nextInputCodePointIndex + 1] === "+") { + const thirdChar = this.text[this.nextInputCodePointIndex + 2]; + if (thirdChar >= '0' && thirdChar <= '9' || thirdChar === "?") { + // TODO: Handle unicode stuff such as U+002B + throw new Error("Unicode tokens not supported!"); + } + } + return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + case "$": + case "*": + case "^": + case "|": + case "~": + return this.consumeAMatchToken() || this.consumeADelimToken(); + case "-": return this.consumeANumericToken() || this.consumeAnIdentLikeToken() || this.consumeCDC() || this.consumeADelimToken(); + case "+": + case ".": + return this.consumeANumericToken() || this.consumeADelimToken(); + case "/": return this.consumeAComment() || this.consumeADelimToken(); + case "<": return this.consumeCDO() || this.consumeADelimToken(); + case undefined: return undefined; + default: return this.consumeAnIdentLikeToken() || this.consumeADelimToken(); + } + } + + private consumeADelimToken(): InputToken { + return { type: TokenObjectType.delim, text: this.text[this.nextInputCodePointIndex++] }; + } + + private consumeAWhitespace(): InputToken { + whitespaceRegEx.lastIndex = this.nextInputCodePointIndex; + const result = whitespaceRegEx.exec(this.text); + this.nextInputCodePointIndex = whitespaceRegEx.lastIndex; + return " "; + } + + private consumeAHashToken(): InputTokenObject { + this.nextInputCodePointIndex++; + let hashName = this.consumeAName(); + if (hashName) { + return { type: TokenObjectType.hash, text: "#" + hashName.text }; + } + this.nextInputCodePointIndex--; + return null; + } + + private consumeCDO(): "" | null { + if (this.text.substr(this.nextInputCodePointIndex, 3) === "-->") { + this.nextInputCodePointIndex += 3; + return "-->"; + } + return null; + } + + private consumeAMatchToken(): "*=" | "$=" | "|=" | "~=" | "^=" | null { + if (this.text[this.nextInputCodePointIndex + 1] === "=") { + const token = this.text.substr(this.nextInputCodePointIndex, 2); + this.nextInputCodePointIndex += 2 + return <"*=" | "$=" | "|=" | "~=" | "^=">token; + } + return null; + } + + /** + * 4.3.2. Consume a numeric token + * https://www.w3.org/TR/css-syntax-3/#consume-a-numeric-token + */ + private consumeANumericToken(): InputToken { + numberRegEx.lastIndex = this.nextInputCodePointIndex; + const result = numberRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = numberRegEx.lastIndex; + if (this.text[this.nextInputCodePointIndex] === "%") { + return { type: TokenObjectType.percentage, text: result[0] }; // TODO: Push the actual number and unit here... + } + + const name = this.consumeAName(); + if (name) { + return { type: TokenObjectType.dimension, text: result[0] + name.text }; + } + + return { type: TokenObjectType.number, text: result[0] }; + } + + /** + * 4.3.3. Consume an ident-like token + * https://www.w3.org/TR/css-syntax-3/#consume-an-ident-like-token + */ + private consumeAnIdentLikeToken(): InputToken { + const name = this.consumeAName(); + if (!name) { + return null; + } + if (this.text[this.nextInputCodePointIndex] === "(") { + this.nextInputCodePointIndex++; + if (name.text.toLowerCase() === "url") { + return this.consumeAURLToken(); + } + return { type: TokenObjectType.functionToken, name: name.text, text: name.text + "(" }; + } + return name; + } + + /** + * 4.3.4. Consume a string token + * https://www.w3.org/TR/css-syntax-3/#consume-a-string-token + */ + private consumeAStringToken(): InputTokenObject { + const char = this.text[this.nextInputCodePointIndex]; + let result: RegExpExecArray; + if (char === "'") { + singleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; + result = singleQuoteStringRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = singleQuoteStringRegEx.lastIndex; + } else if (char === "\"") { + doubleQuoteStringRegEx.lastIndex = this.nextInputCodePointIndex; + result = doubleQuoteStringRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = doubleQuoteStringRegEx.lastIndex; + } + + // TODO: Handle bad-string. + // TODO: Perform string escaping. + return { type: TokenObjectType.string, text: result[0] }; + } + + /** + * 4.3.5. Consume a url token + * https://www.w3.org/TR/css-syntax-3/#consume-a-url-token + */ + private consumeAURLToken(): InputToken { + const start = this.nextInputCodePointIndex - 3 /* url */ - 1 /* ( */; + const urlToken: InputToken = { type: TokenObjectType.url, text: undefined }; + this.consumeAWhitespace(); + if (this.nextInputCodePointIndex >= this.text.length) { + return urlToken; + } + const nextInputCodePoint = this.text[this.nextInputCodePointIndex]; + if (nextInputCodePoint === "\"" || nextInputCodePoint === "'") { + const stringToken = this.consumeAStringToken(); + // TODO: Handle bad-string. + // TODO: Set value instead. + urlToken.text = stringToken.text; + this.consumeAWhitespace(); + if (this.text[this.nextInputCodePointIndex] === ")" || this.nextInputCodePointIndex >= this.text.length) { + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + urlToken.text = this.text.substring(start, end); + return urlToken; + } else { + // TODO: Handle bad-url. + return null; + } + } + + while(this.nextInputCodePointIndex < this.text.length) { + const char = this.text[this.nextInputCodePointIndex++]; + switch(char) { + case ")": return urlToken; + case " ": + case "\t": + case "\n": + case "\r": + case "\f": + this.consumeAWhitespace(); + if (this.text[this.nextInputCodePointIndex] === ")") { + this.nextInputCodePointIndex++; + return urlToken; + } else { + // TODO: Bar url! Consume remnants. + return null; + } + case "\"": + case "\'": + // TODO: Parse error! Bar url! Consume remnants. + return null; + case "\\": + // TODO: Escape! + throw new Error("Escaping not yet supported!"); + default: + // TODO: Non-printable chars - error. + urlToken.text += char; + } + } + return urlToken; + } + + /** + * 4.3.11. Consume a name + * https://www.w3.org/TR/css-syntax-3/#consume-a-name + */ + private consumeAName(): InputTokenObject { + nameRegEx.lastIndex = this.nextInputCodePointIndex; + const result = nameRegEx.exec(this.text); + if (!result) { + return null; + } + this.nextInputCodePointIndex = nameRegEx.lastIndex; + // TODO: Perform string escaping. + return { type: TokenObjectType.ident, text: result[0] }; + } + + private consumeAtKeyword(): InputTokenObject { + this.nextInputCodePointIndex++; + let name = this.consumeAName(); + if (name) { + return { type: TokenObjectType.atKeyword, text: name.text }; + } + this.nextInputCodePointIndex--; + return null; + } + + private consumeAComment(): InputToken { + if (this.text[this.nextInputCodePointIndex + 1] === "*") { + commentRegEx.lastIndex = this.nextInputCodePointIndex; + const result = commentRegEx.exec(this.text); + if (!result) { + return null; // TODO: Handle + } + this.nextInputCodePointIndex = commentRegEx.lastIndex; + // The CSS spec tokenizer does not emmit comment tokens + return this.consumeAToken(); + } + return null; + } + + private reconsumeTheCurrentInputToken(currentInputToken: InputToken) { + this.reconsumedInputToken = currentInputToken; + } + + /** + * 5.3.1. Parse a stylesheet + * https://www.w3.org/TR/css-syntax-3/#parse-a-stylesheet + */ + public parseAStylesheet(): Stylesheet { + this.topLevelFlag = true; + const stylesheet: Stylesheet = { + rules: this.consumeAListOfRules() + }; + return stylesheet; + } + + /** + * 5.4.1. Consume a list of rules + * https://www.w3.org/TR/css-syntax-3/#consume-a-list-of-rules + */ + public consumeAListOfRules(): Rule[] { + const rules: Rule[] = []; + let inputToken: InputToken; + while(inputToken = this.consumeAToken()) { + switch(inputToken) { + case " ": continue; + case "": + if (this.topLevelFlag) { + continue; + } + this.reconsumeTheCurrentInputToken(inputToken); + const atRule = this.consumeAnAtRule(); + if (atRule) { + rules.push(atRule); + } + continue; + } + if ((inputToken).type === TokenObjectType.atKeyword) { + this.reconsumeTheCurrentInputToken(inputToken); + const atRule = this.consumeAnAtRule(); + if (atRule) { + rules.push(atRule); + } + continue; + } + this.reconsumeTheCurrentInputToken(inputToken); + const qualifiedRule = this.consumeAQualifiedRule(); + if (qualifiedRule) { + rules.push(qualifiedRule); + } + } + return rules; + } + + /** + * 5.4.2. Consume an at-rule + * https://www.w3.org/TR/css-syntax-3/#consume-an-at-rule + */ + public consumeAnAtRule(): AtRule { + let inputToken = this.consumeAToken(); + const atRule: AtRule = { + type: "at-rule", + name: (inputToken).text, + prelude: [], + block: undefined + } + while(inputToken = this.consumeAToken()) { + if (inputToken === ";") { + return atRule; + } else if (inputToken === "{") { + atRule.block = this.consumeASimpleBlock(inputToken); + return atRule; + } else if ((inputToken).type === TokenObjectType.simpleBlock && (inputToken).associatedToken === "{") { + atRule.block = inputToken; + return atRule; + } + this.reconsumeTheCurrentInputToken(inputToken); + const component = this.consumeAComponentValue(); + if (component) { + atRule.prelude.push(component); + } + } + return atRule; + } + + /** + * 5.4.3. Consume a qualified rule + * https://www.w3.org/TR/css-syntax-3/#consume-a-qualified-rule + */ + public consumeAQualifiedRule(): QualifiedRule { + const qualifiedRule: QualifiedRule = { + type: "qualified-rule", + prelude: [], + block: undefined + }; + let inputToken: InputToken; + while(inputToken = this.consumeAToken()) { + if (inputToken === "{") { + let block = this.consumeASimpleBlock(inputToken); + qualifiedRule.block = block; + return qualifiedRule; + } else if ((inputToken).type === TokenObjectType.simpleBlock) { + const simpleBlock: SimpleBlock = inputToken; + if (simpleBlock.associatedToken === "{") { + qualifiedRule.block = simpleBlock; + return qualifiedRule; + } + } + this.reconsumeTheCurrentInputToken(inputToken); + const componentValue = this.consumeAComponentValue(); + if (componentValue) { + qualifiedRule.prelude.push(componentValue); + } + } + // TODO: This is a parse error, log parse errors! + return null; + } + + /** + * 5.4.6. Consume a component value + * https://www.w3.org/TR/css-syntax-3/#consume-a-component-value + */ + private consumeAComponentValue(): InputToken { + // const inputToken = this.consumeAToken(); + const inputToken = this.consumeAToken(); + switch(inputToken) { + case "{": + case "[": + case "(": + this.nextInputCodePointIndex++; + return this.consumeASimpleBlock(inputToken); + } + if (typeof inputToken === "object" && inputToken.type === TokenObjectType.functionToken) { + return this.consumeAFunction((inputToken).name); + } + return inputToken; + } + + /** + * 5.4.7. Consume a simple block + * https://www.w3.org/TR/css-syntax-3/#consume-a-simple-block + */ + private consumeASimpleBlock(associatedToken: InputToken): SimpleBlock { + const endianToken: "]" | "}" | ")" = { + "[": "]", + "{": "}", + "(": ")" + }[associatedToken]; + const start = this.nextInputCodePointIndex - 1; + const block: SimpleBlock = { + type: TokenObjectType.simpleBlock, + text: undefined, + associatedToken, + values: [] + }; + let nextInputToken; + while(nextInputToken = this.text[this.nextInputCodePointIndex]) { + if (nextInputToken === endianToken) { + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + block.text = this.text.substring(start, end); + return block; + } + const value = this.consumeAComponentValue(); + if (value) { + block.values.push(value); + } + } + block.text = this.text.substring(start); + return block; + } + + /** + * 5.4.8. Consume a function + * https://www.w3.org/TR/css-syntax-3/#consume-a-function + */ + private consumeAFunction(name: string): InputToken { + const start = this.nextInputCodePointIndex; + const funcToken: FunctionToken = { type: TokenObjectType.function, name, text: undefined, components: [] }; + do { + if (this.nextInputCodePointIndex >= this.text.length) { + funcToken.text = name + "(" + this.text.substring(start); + return funcToken; + } + const nextInputToken = this.text[this.nextInputCodePointIndex]; + switch(nextInputToken) { + case ")": + this.nextInputCodePointIndex++; + const end = this.nextInputCodePointIndex; + funcToken.text = name + "(" + this.text.substring(start, end); + return funcToken; + default: + const component = this.consumeAComponentValue(); + if (component) { + funcToken.components.push(component); + } + // TODO: Else we won't advance + } + } while(true); + } +} + +/** + * Consume a CSS3 parsed stylesheet and convert the rules and selectors to the + * NativeScript internal JSON representation. + */ +export class CSSNativeScript { + public parseStylesheet(stylesheet: Stylesheet): any { + return { + type: "stylesheet", + stylesheet: { + rules: this.parseRules(stylesheet.rules) + } + } + } + + private parseRules(rules: Rule[]): any { + return rules.map(rule => this.parseRule(rule)); + } + + private parseRule(rule: Rule): any { + if (rule.type === "at-rule") { + return this.parseAtRule(rule); + } else if (rule.type === "qualified-rule") { + return this.parseQualifiedRule(rule); + } + } + + private parseAtRule(rule: AtRule): any { + if (rule.name === "import") { + // TODO: We have used an "@improt { url('path somewhere'); }" at few places. + return { + import: rule.prelude.map(m => typeof m === "string" ? m : m.text).join("").trim(), + type: "import" + } + } + return; + } + + private parseQualifiedRule(rule: QualifiedRule): any { + return { + type: "rule", + selectors: this.preludeToSelectorsStringArray(rule.prelude), + declarations: this.ruleBlockToDeclarations(rule.block.values) + } + } + + private ruleBlockToDeclarations(declarationsInputTokens: InputToken[]): { type: "declaration", property: string, value: string }[] { + // return declarationsInputTokens; + const declarations: { type: "declaration", property: string, value: string }[] = []; + + let property = ""; + let value = ""; + let reading: "property" | "value" = "property"; + + for (var i = 0; i < declarationsInputTokens.length; i++) { + let inputToken = declarationsInputTokens[i]; + if (reading === "property") { + if (inputToken === ":") { + reading = "value"; + } else if (typeof inputToken === "string") { + property += inputToken; + } else { + property += inputToken.text; + } + } else { + if (inputToken === ";") { + property = property.trim(); + value = value.trim(); + declarations.push({ type: "declaration", property, value }); + property = ""; + value = ""; + reading = "property"; + } else if (typeof inputToken === "string") { + value += inputToken; + } else { + value += inputToken.text; + } + } + } + property = property.trim(); + value = value.trim(); + if (property || value) { + declarations.push({ type: "declaration", property, value }); + } + return declarations; + } + + private preludeToSelectorsStringArray(prelude: InputToken[]): string[] { + let selectors = []; + let selector = ""; + prelude.forEach(inputToken => { + if (typeof inputToken === "string") { + if (inputToken === ",") { + if (selector) { + selectors.push(selector.trim()); + } + selector = ""; + } else { + selector += inputToken; + } + } else if (typeof inputToken === "object") { + selector += inputToken.text; + } + }); + if (selector) { + selectors.push(selector.trim()); + } + return selectors; + } +} \ No newline at end of file diff --git a/tns-core-modules/file-system/file-system.ts b/tns-core-modules/file-system/file-system.ts index fe97ab75d..830d51817 100644 --- a/tns-core-modules/file-system/file-system.ts +++ b/tns-core-modules/file-system/file-system.ts @@ -19,20 +19,11 @@ function ensurePlatform() { } } -// we are defining these as private variables within the IO scope and will use them to access the corresponding properties for each FSEntity instance. -// this allows us to encapsulate (hide) the explicit property setters and force the users go through the exposed APIs to receive FSEntity instances. -var nameProperty = "_name"; -var pathProperty = "_path"; -var isKnownProperty = "_isKnown"; -var fileLockedProperty = "_locked"; -var extensionProperty = "_extension"; -var lastModifiedProperty = "_lastModified"; - var createFile = function (info: { path: string; name: string; extension: string }) { var file = new File(); - file[pathProperty] = info.path; - file[nameProperty] = info.name; - file[extensionProperty] = info.extension; + file._path = info.path; + file._name = info.name; + file._extension = info.extension; return file; }; @@ -50,13 +41,20 @@ var createFolder = function (info: { path: string; name: string; }) { var folder = new Folder(); - folder[pathProperty] = info.path; - folder[nameProperty] = info.name; + folder._path = info.path; + folder._name = info.name; return folder; }; export class FileSystemEntity { + _path: string; + _name: string; + _extension: string; + _locked: boolean; + _lastModified: Date; + _isKnown: boolean; + get parent(): Folder { var onError = function (error) { throw error; @@ -86,7 +84,7 @@ export class FileSystemEntity { } public removeSync(onError?: (error: any) => any): void { - if (this[isKnownProperty]) { + if (this._isKnown) { if (onError) { onError({ message: "Cannot delete known folder." }); } @@ -120,7 +118,7 @@ export class FileSystemEntity { } public renameSync(newName: string, onError?: (error: any) => any): void { - if (this[isKnownProperty]) { + if (this._isKnown) { if (onError) { onError(new Error("Cannot rename known folder.")); } @@ -149,26 +147,26 @@ export class FileSystemEntity { } fileAccess.rename(this.path, newPath, localError); - this[pathProperty] = newPath; - this[nameProperty] = newName; + this._path = newPath; + this._name = newName; if (this instanceof File) { - this[extensionProperty] = fileAccess.getFileExtension(newPath); + this._extension = fileAccess.getFileExtension(newPath); } } get name(): string { - return this[nameProperty]; + return this._name; } get path(): string { - return this[pathProperty]; + return this._path; } get lastModified(): Date { - var value = this[lastModifiedProperty]; - if (!this[lastModifiedProperty]) { - value = this[lastModifiedProperty] = getFileAccess().getLastModified(this.path); + var value = this._lastModified; + if (!this._lastModified) { + value = this._lastModified = getFileAccess().getLastModified(this.path); } return value; @@ -194,22 +192,22 @@ export class File extends FileSystemEntity { } get extension(): string { - return this[extensionProperty]; + return this._extension; } get isLocked(): boolean { // !! is a boolean conversion/cast, handling undefined as well - return !!this[fileLockedProperty]; + return !!this._locked; } public readSync(onError?: (error: any) => any): any { this.checkAccess(); - this[fileLockedProperty] = true; + this._locked = true; var that = this; var localError = (error) => { - that[fileLockedProperty] = false; + that._locked = false; if (onError) { onError(error); } @@ -217,7 +215,7 @@ export class File extends FileSystemEntity { var content = getFileAccess().read(this.path, localError); - this[fileLockedProperty] = false; + this._locked = false; return content; @@ -227,11 +225,11 @@ export class File extends FileSystemEntity { this.checkAccess(); try { - this[fileLockedProperty] = true; + this._locked = true; var that = this; var localError = function (error) { - that[fileLockedProperty] = false; + that._locked = false; if (onError) { onError(error); } @@ -239,7 +237,7 @@ export class File extends FileSystemEntity { getFileAccess().write(this.path, content, localError); } finally { - this[fileLockedProperty] = false; + this._locked = false; } } @@ -262,18 +260,18 @@ export class File extends FileSystemEntity { public readTextSync(onError?: (error: any) => any, encoding?: string): string { this.checkAccess(); - this[fileLockedProperty] = true; + this._locked = true; var that = this; var localError = (error) => { - that[fileLockedProperty] = false; + that._locked = false; if (onError) { onError(error); } }; var content = getFileAccess().readText(this.path, localError, encoding); - this[fileLockedProperty] = false; + this._locked = false; return content; } @@ -297,11 +295,11 @@ export class File extends FileSystemEntity { this.checkAccess(); try { - this[fileLockedProperty] = true; + this._locked = true; var that = this; var localError = function (error) { - that[fileLockedProperty] = false; + that._locked = false; if (onError) { onError(error); } @@ -310,7 +308,7 @@ export class File extends FileSystemEntity { // TODO: Asyncronous getFileAccess().writeText(this.path, content, localError, encoding); } finally { - this[fileLockedProperty] = false; + this._locked = false; } } @@ -370,7 +368,7 @@ export class Folder extends FileSystemEntity { } get isKnown(): boolean { - return this[isKnownProperty]; + return this._isKnown; } public getFile(name: string): File { @@ -473,8 +471,8 @@ export module knownFolders { if (!_documents) { var path = getFileAccess().getDocumentsFolderPath(); _documents = new Folder(); - _documents[pathProperty] = path; - _documents[isKnownProperty] = true; + _documents._path = path; + _documents._isKnown = true; } return _documents; @@ -484,8 +482,8 @@ export module knownFolders { if (!_temp) { var path = getFileAccess().getTempFolderPath(); _temp = new Folder(); - _temp[pathProperty] = path; - _temp[isKnownProperty] = true; + _temp._path = path; + _temp._isKnown = true; } return _temp; @@ -495,8 +493,8 @@ export module knownFolders { if (!_app) { var path = getFileAccess().getCurrentAppPath(); _app = new Folder(); - _app[pathProperty] = path; - _app[isKnownProperty] = true; + _app._path = path; + _app._isKnown = true; } return _app; @@ -518,8 +516,8 @@ export module knownFolders { if (existingFolderInfo) { _library = existingFolderInfo.folder; - _library[pathProperty] = existingFolderInfo.path; - _library[isKnownProperty] = true; + _library._path = existingFolderInfo.path; + _library._isKnown = true; } } @@ -534,8 +532,8 @@ export module knownFolders { if (existingFolderInfo) { _developer = existingFolderInfo.folder; - _developer[pathProperty] = existingFolderInfo.path; - _developer[isKnownProperty] = true; + _developer._path = existingFolderInfo.path; + _developer._isKnown = true; } } @@ -550,8 +548,8 @@ export module knownFolders { if (existingFolderInfo) { _desktop = existingFolderInfo.folder; - _desktop[pathProperty] = existingFolderInfo.path; - _desktop[isKnownProperty] = true; + _desktop._path = existingFolderInfo.path; + _desktop._isKnown = true; } } @@ -566,8 +564,8 @@ export module knownFolders { if (existingFolderInfo) { _downloads = existingFolderInfo.folder; - _downloads[pathProperty] = existingFolderInfo.path; - _downloads[isKnownProperty] = true; + _downloads._path = existingFolderInfo.path; + _downloads._isKnown = true; } } @@ -582,8 +580,8 @@ export module knownFolders { if (existingFolderInfo) { _movies = existingFolderInfo.folder; - _movies[pathProperty] = existingFolderInfo.path; - _movies[isKnownProperty] = true; + _movies._path = existingFolderInfo.path; + _movies._isKnown = true; } } @@ -598,8 +596,8 @@ export module knownFolders { if (existingFolderInfo) { _music = existingFolderInfo.folder; - _music[pathProperty] = existingFolderInfo.path; - _music[isKnownProperty] = true; + _music._path = existingFolderInfo.path; + _music._isKnown = true; } } @@ -614,8 +612,8 @@ export module knownFolders { if (existingFolderInfo) { _pictures = existingFolderInfo.folder; - _pictures[pathProperty] = existingFolderInfo.path; - _pictures[isKnownProperty] = true; + _pictures._path = existingFolderInfo.path; + _pictures._isKnown = true; } } @@ -630,15 +628,15 @@ export module knownFolders { if (existingFolderInfo) { _sharedPublic = existingFolderInfo.folder; - _sharedPublic[pathProperty] = existingFolderInfo.path; - _sharedPublic[isKnownProperty] = true; + _sharedPublic._path = existingFolderInfo.path; + _sharedPublic._isKnown = true; } } return _sharedPublic; }; - function getExistingFolderInfo(pathDirectory: NSSearchPathDirectory): { folder: Folder; path: string } { + function getExistingFolderInfo(pathDirectory: any /* NSSearchPathDirectory */): { folder: Folder; path: string } { var fileAccess = (getFileAccess()); var folderPath = fileAccess.getKnownPath(pathDirectory); var folderInfo = fileAccess.getExistingFolder(folderPath); diff --git a/tns-core-modules/globals/globals.ts b/tns-core-modules/globals/globals.ts index 6e8416a12..f57bfca3c 100644 --- a/tns-core-modules/globals/globals.ts +++ b/tns-core-modules/globals/globals.ts @@ -2,15 +2,17 @@ require("./decorators"); // Required by V8 snapshot generator -global.__extends = global.__extends || function (d, b) { - for (var p in b) { - if (b.hasOwnProperty(p)) { - d[p] = b[p]; +if (!global.__extends) { + global.__extends = function (d, b) { + for (var p in b) { + if (b.hasOwnProperty(p)) { + d[p] = b[p]; + } } - } - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -}; + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +} // This method iterates all the keys in the source exports object and copies them to the destination exports one. // Note: the method will not check for naming collisions and will override any already existing entries in the destination exports. @@ -26,6 +28,8 @@ import * as dialogsModule from "../ui/dialogs"; type ModuleLoader = (name?: string) => any; const modules: Map = new Map(); +(global).moduleResolvers = [global.require]; + global.registerModule = function(name: string, loader: ModuleLoader): void { modules.set(name, loader); } @@ -38,10 +42,13 @@ global.loadModule = function(name: string): any { const loader = modules.get(name); if (loader) { return loader(); - } else { - let result = global.require(name); - modules.set(name, () => result); - return result; + } + for (let resolver of (global).moduleResolvers) { + const result = resolver(name); + if (result) { + modules.set(name, () => result); + return result; + } } } @@ -71,10 +78,6 @@ function registerOnGlobalContext(name: string, module: string): void { get: function () { // We do not need to cache require() call since it is already cached in the runtime. let m = global.loadModule(module); - if (!__tnsGlobalMergedModules.has(module)) { - __tnsGlobalMergedModules.set(module, true); - global.moduleMerge(m, global); - } // Redefine the property to make sure the above code is executed only once. let resolvedValue = m[name]; @@ -106,6 +109,8 @@ export function install() { alert: dialogs.alert, confirm: dialogs.confirm, prompt: dialogs.prompt, + login: dialogs.login, + action: dialogs.action, XMLHttpRequest: xhr.XMLHttpRequest, FormData: xhr.FormData, @@ -124,12 +129,20 @@ export function install() { registerOnGlobalContext("clearTimeout", "timer"); registerOnGlobalContext("setInterval", "timer"); registerOnGlobalContext("clearInterval", "timer"); + registerOnGlobalContext("alert", "ui/dialogs"); registerOnGlobalContext("confirm", "ui/dialogs"); registerOnGlobalContext("prompt", "ui/dialogs"); + registerOnGlobalContext("login", "ui/dialogs"); + registerOnGlobalContext("action", "ui/dialogs"); + registerOnGlobalContext("XMLHttpRequest", "xhr"); registerOnGlobalContext("FormData", "xhr"); + registerOnGlobalContext("fetch", "fetch"); + registerOnGlobalContext("Headers", "fetch"); + registerOnGlobalContext("Request", "fetch"); + registerOnGlobalContext("Response", "fetch"); // check whether the 'android' namespace is exposed // if positive - the current device is an Android diff --git a/tns-core-modules/image-asset/image-asset-common.ts b/tns-core-modules/image-asset/image-asset-common.ts index 842865334..c6e35641d 100644 --- a/tns-core-modules/image-asset/image-asset-common.ts +++ b/tns-core-modules/image-asset/image-asset-common.ts @@ -4,9 +4,10 @@ import * as platform from "../platform"; export class ImageAsset extends observable.Observable implements definition.ImageAsset { private _options: definition.ImageAssetOptions; - private _ios: PHAsset; private _nativeImage: any; - private _android: string; //file name of the image + + ios: PHAsset; + android: string; get options(): definition.ImageAssetOptions { return this._options; @@ -16,22 +17,6 @@ export class ImageAsset extends observable.Observable implements definition.Ima this._options = value; } - get ios(): PHAsset { - return this._ios; - } - - set ios(value: PHAsset) { - this._ios = value; - } - - get android(): string { - return this._android; - } - - set android(value: string) { - this._android = value; - } - get nativeImage(): any { return this._nativeImage; } diff --git a/tns-core-modules/image-asset/image-asset.android.ts b/tns-core-modules/image-asset/image-asset.android.ts index 27912dd9f..282bafea8 100644 --- a/tns-core-modules/image-asset/image-asset.android.ts +++ b/tns-core-modules/image-asset/image-asset.android.ts @@ -4,11 +4,21 @@ import * as common from "./image-asset-common"; global.moduleMerge(common, exports); export class ImageAsset extends common.ImageAsset { + private _android: string; //file name of the image + constructor(asset: string) { super(); this.android = asset; } + get android(): string { + return this._android; + } + + set android(value: string) { + this._android = value; + } + public getImageAsync(callback: (image, error) => void) { let bitmapOptions = new android.graphics.BitmapFactory.Options(); bitmapOptions.inJustDecodeBounds = true; diff --git a/tns-core-modules/image-asset/image-asset.ios.ts b/tns-core-modules/image-asset/image-asset.ios.ts index f7e7e0502..48290c1a0 100644 --- a/tns-core-modules/image-asset/image-asset.ios.ts +++ b/tns-core-modules/image-asset/image-asset.ios.ts @@ -3,6 +3,8 @@ import * as common from "./image-asset-common"; global.moduleMerge(common, exports); export class ImageAsset extends common.ImageAsset { + private _ios: PHAsset; + constructor(asset: PHAsset | UIImage) { super(); if (asset instanceof UIImage) { @@ -13,6 +15,14 @@ export class ImageAsset extends common.ImageAsset { } } + get ios(): PHAsset { + return this._ios; + } + + set ios(value: PHAsset) { + this._ios = value; + } + public getImageAsync(callback: (image, error) => void) { let srcWidth = this.nativeImage ? this.nativeImage.size.width : this.ios.pixelWidth; let srcHeight = this.nativeImage ? this.nativeImage.size.height : this.ios.pixelHeight; diff --git a/tns-core-modules/module.d.ts b/tns-core-modules/module.d.ts index 53ab0f945..c61a060af 100644 --- a/tns-core-modules/module.d.ts +++ b/tns-core-modules/module.d.ts @@ -1,11 +1,32 @@ declare var global: NodeJS.Global; +interface ModuleResolver { + /** + * A function used to resolve the exports for a module. + * @param uri The name of the module to be resolved. + */ + (uri: string): any; +} + //Augment the NodeJS global type with our own extensions declare namespace NodeJS { interface Global { android?: any; require(id: string): any; registerModule(name: string, loader: ((name: string) => any)): void; + /** + * The NativeScript XML builder, style-scope, application modules use various resources such as: + * app.css, page.xml files and modules during the application life-cycle. + * The moduleResolvers can be used to provide additional mechanisms to locate such resources. + * For example: + * ``` + * global.moduleResolvers.unshift(uri => uri === "main-page" ? require("main-page") : null); + * ``` + * More advanced scenarios will allow for specific bundlers to integrate their module resolving mechanisms. + * When adding resolvers at the start of the array, avoid throwing and return null instead so subsequent resolvers may try to resolve the resource. + * By default the only member of the array is global.require, as last resort - if it fails to find a module it will throw. + */ + readonly moduleResolvers: ModuleResolver[]; loadModule(name: string): any; moduleExists(name: string): boolean; moduleMerge(sourceExports: any, destExports: any): void; diff --git a/tns-core-modules/package.json b/tns-core-modules/package.json index 458b7a578..303d557a3 100644 --- a/tns-core-modules/package.json +++ b/tns-core-modules/package.json @@ -10,6 +10,7 @@ "files": [ "**/*.d.ts", "**/*.js", + "**/package.json", "!android17.d.ts", "!ios.d.ts", "!bin/", diff --git a/tns-core-modules/profiling/profiling.ts b/tns-core-modules/profiling/profiling.ts index 865c80688..456cc6a27 100644 --- a/tns-core-modules/profiling/profiling.ts +++ b/tns-core-modules/profiling/profiling.ts @@ -85,7 +85,7 @@ export function isRunning(name: string): boolean { return !!(info && info.runCount); } -function countersProfileFunctionFactory(fn: F, name: string): F { +function countersProfileFunctionFactory(fn: F, name: string, type: MemberType = MemberType.Instance): F { profileNames.push(name); return function() { start(name); @@ -97,8 +97,8 @@ function countersProfileFunctionFactory(fn: F, name: string) } } -function timelineProfileFunctionFactory(fn: F, name: string): F { - return function() { +function timelineProfileFunctionFactory(fn: F, name: string, type: MemberType = MemberType.Instance): F { + return type === MemberType.Instance ? function() { const start = time(); try { return fn.apply(this, arguments); @@ -106,10 +106,23 @@ function timelineProfileFunctionFactory(fn: F, name: string) const end = time(); console.log(`Timeline: Modules: ${name} ${this} (${start}ms. - ${end}ms.)`); } - } + } : function() { + const start = time(); + try { + return fn.apply(this, arguments); + } finally { + const end = time(); + console.log(`Timeline: Modules: ${name} (${start}ms. - ${end}ms.)`); + } + }; } -let profileFunctionFactory: (fn: F, name: string) => F; +const enum MemberType { + Static, + Instance +} + +let profileFunctionFactory: (fn: F, name: string, type?: MemberType) => F; export function enable(mode: InstrumentationMode = "counters") { profileFunctionFactory = mode && { counters: countersProfileFunctionFactory, @@ -154,7 +167,28 @@ const profileMethodUnnamed = (target, key, descriptor) => { let name = className + key; //editing the descriptor/value parameter - descriptor.value = profileFunctionFactory(originalMethod, name); + descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Instance); + + // return edited descriptor as opposed to overwriting the descriptor + return descriptor; +} + +const profileStaticMethodUnnamed = (ctor, key, descriptor) => { + // save a reference to the original method this way we keep the values currently in the + // descriptor and don't overwrite what another decorator might have done to the descriptor. + if (descriptor === undefined) { + descriptor = Object.getOwnPropertyDescriptor(ctor, key); + } + var originalMethod = descriptor.value; + + let className = ""; + if (ctor && ctor.name) { + className = ctor.name + "."; + } + let name = className + key; + + //editing the descriptor/value parameter + descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Static); // return edited descriptor as opposed to overwriting the descriptor return descriptor; @@ -188,6 +222,11 @@ export function profile(nameFnOrTarget?: string | Function | Object, fnOrKey?: F return; } return profileMethodUnnamed(nameFnOrTarget, fnOrKey, descriptor); + } else if (typeof nameFnOrTarget === "function" && (typeof fnOrKey === "string" || typeof fnOrKey === "symbol")) { + if (!profileFunctionFactory) { + return; + } + return profileStaticMethodUnnamed(nameFnOrTarget, fnOrKey, descriptor); } else if (typeof nameFnOrTarget === "string" && typeof fnOrKey === "function") { if (!profileFunctionFactory) { return fnOrKey; diff --git a/tns-core-modules/ui/builder/builder.ts b/tns-core-modules/ui/builder/builder.ts index a3b15955c..2676538a7 100644 --- a/tns-core-modules/ui/builder/builder.ts +++ b/tns-core-modules/ui/builder/builder.ts @@ -10,7 +10,7 @@ import { isString, isDefined } from "../../utils/types"; import { ComponentModule, setPropertyValue, getComponentModule } from "./component-builder"; import { platformNames, device } from "../../platform"; import { resolveFileName } from "../../file-system/file-name-resolver"; -import { profile } from "tns-core-modules/profiling"; +import { profile } from "../../profiling"; import * as traceModule from "../../trace"; const ios = platformNames.ios.toLowerCase(); diff --git a/tns-core-modules/ui/builder/component-builder/component-builder.ts b/tns-core-modules/ui/builder/component-builder/component-builder.ts index 7985d0f9d..fa1b0aed2 100644 --- a/tns-core-modules/ui/builder/component-builder/component-builder.ts +++ b/tns-core-modules/ui/builder/component-builder/component-builder.ts @@ -7,7 +7,7 @@ import { isEventOrGesture } from "../../core/bindable"; import { File, path, knownFolders } from "../../../file-system"; import { getBindingOptions, bindingConstants } from "../binding-builder"; import { resolveFileName } from "../../../file-system/file-name-resolver"; -import { profile } from "tns-core-modules/profiling"; +import { profile } from "../../../profiling"; import * as debugModule from "../../../utils/debug"; import * as platform from "../../../platform"; diff --git a/tns-core-modules/ui/core/properties/properties.d.ts b/tns-core-modules/ui/core/properties/properties.d.ts index 43dda71d2..33adfae64 100644 --- a/tns-core-modules/ui/core/properties/properties.d.ts +++ b/tns-core-modules/ui/core/properties/properties.d.ts @@ -32,7 +32,7 @@ export interface CssPropertyOptions extends PropertyOptions< export interface ShorthandPropertyOptions

{ readonly name: string, readonly cssName: string; - readonly converter: (value: string | P) => [CssProperty, any][], + readonly converter: (value: string | P) => [CssProperty | CssAnimationProperty, any][], readonly getter: (this: Style) => string | P } diff --git a/tns-core-modules/ui/core/view-base/view-base.ts b/tns-core-modules/ui/core/view-base/view-base.ts index fac20020c..e7c13ef03 100644 --- a/tns-core-modules/ui/core/view-base/view-base.ts +++ b/tns-core-modules/ui/core/view-base/view-base.ts @@ -26,6 +26,7 @@ export * from "../bindable"; export * from "../properties"; import * as ssm from "../../styling/style-scope"; + let styleScopeModule: typeof ssm; function ensureStyleScopeModule() { if (!styleScopeModule) { @@ -588,7 +589,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition // } if (!nativeView) { - nativeView = this.createNativeView(); + nativeView = this.createNativeView(); } this._androidView = nativeView; @@ -597,7 +598,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition this._isPaddingRelative = nativeView.isPaddingRelative(); } - let result: android.graphics.Rect = (nativeView).defaultPaddings; + let result: any /* android.graphics.Rect */ = (nativeView).defaultPaddings; if (result === undefined) { result = org.nativescript.widgets.ViewHelper.getPadding(nativeView); (nativeView).defaultPaddings = result; diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index f071baed3..236fbe342 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -8,7 +8,7 @@ import { resolveFileName } from "../../file-system/file-name-resolver"; import { knownFolders, path } from "../../file-system"; import { parse, loadPage } from "../builder"; import * as application from "../../application"; -import { profile } from "tns-core-modules/profiling"; +import { profile } from "../../profiling"; export { application }; diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index d02affc59..ca1cda6c8 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -715,6 +715,7 @@ class ActivityCallbacksImplementation implements AndroidActivityCallbacks { private notifyLaunch(intent: android.content.Intent, savedInstanceState: android.os.Bundle): View { const launchArgs: application.LaunchEventData = { eventName: application.launchEvent, object: application.android, android: intent, savedInstanceState }; application.notify(launchArgs); + application.notify({ eventName: "loadAppCss", object: this, cssFile: application.getCssFileName() }); return launchArgs.root; } diff --git a/tns-core-modules/ui/frame/frame.ios.ts b/tns-core-modules/ui/frame/frame.ios.ts index e67b862bc..38f0647e8 100644 --- a/tns-core-modules/ui/frame/frame.ios.ts +++ b/tns-core-modules/ui/frame/frame.ios.ts @@ -8,7 +8,7 @@ import { FrameBase, View, application, layout, traceEnabled, traceWrite, traceCa import { _createIOSAnimatedTransitioning } from "./fragment.transitions"; // HACK: Webpack. Use a fully-qualified import to allow resolve.extensions(.ios.js) to // kick in. `../utils` doesn't seem to trigger the webpack extensions mechanism. -import * as uiUtils from "tns-core-modules/ui/utils"; +import * as uiUtils from "../../ui/utils"; import * as utils from "../../utils/utils"; export * from "./frame-common"; diff --git a/tns-core-modules/ui/page/page.ios.ts b/tns-core-modules/ui/page/page.ios.ts index 2d583e59c..fe054d757 100644 --- a/tns-core-modules/ui/page/page.ios.ts +++ b/tns-core-modules/ui/page/page.ios.ts @@ -7,7 +7,7 @@ import { ios as iosApp } from "../../application"; import { device } from "../../platform"; // HACK: Webpack. Use a fully-qualified import to allow resolve.extensions(.ios.js) to // kick in. `../utils` doesn't seem to trigger the webpack extensions mechanism. -import * as uiUtils from "tns-core-modules/ui/utils"; +import * as uiUtils from "../../ui/utils"; import { profile } from "../../profiling"; export * from "./page-common"; diff --git a/tns-core-modules/ui/styling/background.android.ts b/tns-core-modules/ui/styling/background.android.ts index a9df7df25..681475319 100644 --- a/tns-core-modules/ui/styling/background.android.ts +++ b/tns-core-modules/ui/styling/background.android.ts @@ -3,7 +3,7 @@ import { isDataURI, isFileOrResourcePath, layout, RESOURCE_PREFIX, FILE_PREFIX } import { parse } from "../../css-value"; import { path, knownFolders } from "../../file-system"; import * as application from "../../application"; -import { profile } from "tns-core-modules/profiling"; +import { profile } from "../../profiling"; export * from "./background-common" interface AndroidView { diff --git a/tns-core-modules/ui/styling/css-selector-parser.d.ts b/tns-core-modules/ui/styling/css-selector-parser.d.ts deleted file mode 100644 index 7824a782e..000000000 --- a/tns-core-modules/ui/styling/css-selector-parser.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @module "ui/styling/css-selector-parser" - * @private - */ /** */ - -//@private -export interface SimpleSelector { - pos: number; - type: "" | "*" | "#" | "." | ":" | "[]"; - comb?: "+" | "~" | ">" | " "; -} -export interface SimpleIdentifierSelector extends SimpleSelector { - ident: string; -} -export interface UniversalSelector extends SimpleSelector { - type: "*"; -} -export interface TypeSelector extends SimpleIdentifierSelector { - type: ""; -} -export interface ClassSelector extends SimpleIdentifierSelector { - type: "."; -} -export interface IdSelector extends SimpleIdentifierSelector { - type: "#"; -} -export interface PseudoClassSelector extends SimpleIdentifierSelector { - type: ":"; -} -export interface AttributeSelector extends SimpleSelector { - type: "[]"; - prop: string; - test?: "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|="; - value?: string; -} -export function isUniversal(sel: SimpleSelector): sel is UniversalSelector; -export function isType(sel: SimpleSelector): sel is TypeSelector; -export function isClass(sel: SimpleSelector): sel is ClassSelector; -export function isId(sel: SimpleSelector): sel is IdSelector; -export function isPseudo(sel: SimpleSelector): sel is PseudoClassSelector; -export function isAttribute(sel: SimpleSelector): sel is AttributeSelector; -export function parse(selector: string): SimpleSelector[]; diff --git a/tns-core-modules/ui/styling/css-selector-parser.ts b/tns-core-modules/ui/styling/css-selector-parser.ts deleted file mode 100644 index 2efbdd3b6..000000000 --- a/tns-core-modules/ui/styling/css-selector-parser.ts +++ /dev/null @@ -1,125 +0,0 @@ -/// -export interface SimpleSelector { - pos: number; - type: "" | "*" | "#" | "." | ":" | "[]"; - comb?: "+" | "~" | ">" | " "; -} -export interface SimpleIdentifierSelector extends SimpleSelector { - ident: string; -} -export interface UniversalSelector extends SimpleSelector { - type: "*"; -} -export interface TypeSelector extends SimpleIdentifierSelector { - type: ""; -} -export interface ClassSelector extends SimpleIdentifierSelector { - type: "."; -} -export interface IdSelector extends SimpleIdentifierSelector { - type: "#"; -} -export interface PseudoClassSelector extends SimpleIdentifierSelector { - type: ":"; -} -export interface AttributeSelector extends SimpleSelector { - type: "[]"; - prop: string; - test?: "=" | "^=" | "$=" | "*=" | "=" | "~=" | "|="; - value?: string; -} - -export function isUniversal(sel: SimpleSelector): sel is UniversalSelector { - return sel.type === "*"; -} -export function isType(sel: SimpleSelector): sel is TypeSelector { - return sel.type === ""; -} -export function isClass(sel: SimpleSelector): sel is ClassSelector { - return sel.type === "."; -} -export function isId(sel: SimpleSelector): sel is IdSelector { - return sel.type === "#"; -} -export function isPseudo(sel: SimpleSelector): sel is PseudoClassSelector { - return sel.type === ":"; -} -export function isAttribute(sel: SimpleSelector): sel is AttributeSelector { - return sel.type === "[]"; -} - -var regex = /(\s*)(?:(\*)|(#|\.|:|\b)([_-\w][_-\w\d]*)|\[\s*([_-\w][_-\w\d]*)\s*(?:(=|\^=|\$=|\*=|\~=|\|=)\s*(?:([_-\w][_-\w\d]*)|"((?:[^\\"]|\\(?:"|n|r|f|\\|0-9a-f))*)"|'((?:[^\\']|\\(?:'|n|r|f|\\|0-9a-f))*)')\s*)?\])(?:\s*(\+|~|>|\s))?/g; -// no lead ws univ type pref and ident [ prop = ident -or- "string escapes \" \00aaff" -or- 'string escapes \' urf-8: \00aaff' ] combinator - -export function parse(selector: string): SimpleSelector[] { - let selectors: any[] = []; - - var result: RegExpExecArray; - var lastIndex = regex.lastIndex = 0; - while (result = regex.exec(selector)) { - let pos = result.index; - if (lastIndex !== pos) { - throw new Error(`Unexpected characters at index, near: ${lastIndex}: ${result.input.substr(lastIndex, 32)}`); - } else if (!result[0] || result[0].length === 0) { - throw new Error(`Last selector match got zero character result at index ${lastIndex}, near: ${result.input.substr(lastIndex, 32)}`); - } - pos += getLeadingWhiteSpace(result).length; - lastIndex = regex.lastIndex; - - var type = getType(result); - let selector: SimpleSelector | SimpleIdentifierSelector | AttributeSelector; - switch (type) { - case "*": - selector = { pos, type }; - break; - case "#": - case ".": - case ":": - case "": - let ident = getIdentifier(result); - selector = { pos, type, ident }; - break; - case "[]": - let prop = getProperty(result); - let test = getPropertyTest(result); - // TODO: Unescape escape sequences. Unescape UTF-8 characters. - let value = getPropertyValue(result); - selector = test ? { pos, type, prop, test, value } : { pos, type, prop }; - break; - default: - throw new Error("Unhandled type."); - } - - let comb = getCombinator(result); - if (comb) { - selector.comb = comb; - } - selectors.push(selector); - } - - if (selectors.length > 0) { - delete selectors[selectors.length - 1].comb; - } - return selectors; -} -function getLeadingWhiteSpace(result: RegExpExecArray): string { - return result[1] || ""; -} -function getType(result: RegExpExecArray): "" | "*" | "." | "#" | ":" | "[]" { - return <"[]">(result[5] && "[]") || <"*">result[2] || <"" | "." | "#" | ":">result[3]; -} -function getIdentifier(result: RegExpExecArray): string { - return result[4]; -} -function getProperty(result: RegExpExecArray): string { - return result[5]; -} -function getPropertyTest(result: RegExpExecArray): string { - return result[6] || undefined; -} -function getPropertyValue(result: RegExpExecArray): string { - return result[7] || result[8] || result[9]; -} -function getCombinator(result: RegExpExecArray): "+" | "~" | ">" | " " { - return <("+" | "~" | ">" | " ")>result[result.length - 1] || undefined; -} diff --git a/tns-core-modules/ui/styling/css-selector/css-selector.ts b/tns-core-modules/ui/styling/css-selector/css-selector.ts index 743f37c61..ad169e802 100644 --- a/tns-core-modules/ui/styling/css-selector/css-selector.ts +++ b/tns-core-modules/ui/styling/css-selector/css-selector.ts @@ -1,9 +1,9 @@ import { Node, Declaration, Changes, ChangeMap } from "."; import { isNullOrUndefined } from "../../../utils/types"; -import { escapeRegexSymbols } from "../../../utils/utils"; +import { escapeRegexSymbols } from "../../../utils/utils-common"; import * as cssParser from "../../../css"; -import * as selectorParser from "../css-selector-parser"; +import * as parser from "../../../css/parser"; const enum Specificity { Inline = 0x01000000, @@ -60,7 +60,7 @@ function SelectorProperties(specificity: Specificity, rarity: Rarity, dynamic: b return cls => { cls.prototype.specificity = specificity; cls.prototype.rarity = rarity; - cls.prototype.combinator = ""; + cls.prototype.combinator = undefined; cls.prototype.dynamic = dynamic; return cls; } @@ -408,61 +408,55 @@ function createDeclaration(decl: cssParser.Declaration): any { return { property: decl.property.toLowerCase(), value: decl.value }; } -export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { - try { - let ast = selectorParser.parse(sel); - if (ast.length === 0) { - return new InvalidSelector(new Error("Empty selector")); - } - - let selectors = ast.map(createSimpleSelector); - let sequences: (SimpleSelector | SimpleSelectorSequence)[] = []; - // Join simple selectors into sequences, set combinators - for (let seqStart = 0, seqEnd = 0, last = selectors.length - 1; seqEnd <= last; seqEnd++) { - let sel = selectors[seqEnd]; - let astComb = ast[seqEnd].comb; - if (astComb || seqEnd === last) { - if (seqStart === seqEnd) { - // This is a sequnce with single SimpleSelector, so we will not combine it into SimpleSelectorSequence. - sel.combinator = astComb; - sequences.push(sel); - } else { - let sequence = new SimpleSelectorSequence(selectors.slice(seqStart, seqEnd + 1)); - sequence.combinator = astComb; - sequences.push(sequence); - } - seqStart = seqEnd + 1; - } - } - - if (sequences.length === 1) { - // This is a selector with a single SinmpleSelectorSequence so we will not combine it into Selector. - return sequences[0]; - } else { - return new Selector(sequences); - } - } catch(e) { - return new InvalidSelector(e); +function createSimpleSelectorFromAst(ast: parser.SimpleSelector): SimpleSelector { + switch(ast.type) { + case "*": return new UniversalSelector(); + case "#": return new IdSelector(ast.identifier); + case "": return new TypeSelector(ast.identifier.replace(/-/, '').toLowerCase()); + case ".": return new ClassSelector(ast.identifier); + case ":": return new PseudoClassSelector(ast.identifier); + case "[]": return ast.test ? new AttributeSelector(ast.property, ast.test, ast.value) : new AttributeSelector(ast.property); } } -function createSimpleSelector(sel: selectorParser.SimpleSelector): SimpleSelector { - if (selectorParser.isUniversal(sel)) { - return new UniversalSelector(); - } else if (selectorParser.isId(sel)) { - return new IdSelector(sel.ident); - } else if (selectorParser.isType(sel)) { - return new TypeSelector(sel.ident.replace(/-/, '').toLowerCase()); - } else if (selectorParser.isClass(sel)) { - return new ClassSelector(sel.ident); - } else if (selectorParser.isPseudo(sel)) { - return new PseudoClassSelector(sel.ident); - } else if (selectorParser.isAttribute(sel)) { - if (sel.test) { - return new AttributeSelector(sel.prop, sel.test, sel.value); - } else { - return new AttributeSelector(sel.prop) +function createSimpleSelectorSequenceFromAst(ast: parser.SimpleSelectorSequence): SimpleSelectorSequence | SimpleSelector { + if (ast.length === 0) { + return new InvalidSelector(new Error("Empty simple selector sequence.")); + } else if (ast.length === 1) { + return createSimpleSelectorFromAst(ast[0]); + } else { + return new SimpleSelectorSequence(ast.map(createSimpleSelectorFromAst)); + } +} + +function createSelectorFromAst(ast: parser.Selector): SimpleSelector | SimpleSelectorSequence | Selector { + if (ast.length === 0) { + return new InvalidSelector(new Error("Empty selector.")); + } else if (ast.length <= 2) { + return createSimpleSelectorSequenceFromAst(ast[0]); + } else { + let simpleSelectorSequences = []; + for (var i = 0; i < ast.length; i += 2) { + const simpleSelectorSequence = createSimpleSelectorSequenceFromAst(ast[i]); + const combinator = ast[i + 1]; + if (combinator) { + simpleSelectorSequence.combinator = combinator; + } + simpleSelectorSequences.push(simpleSelectorSequence); } + return new Selector(simpleSelectorSequences); + } +} + +export function createSelector(sel: string): SimpleSelector | SimpleSelectorSequence | Selector { + try { + let parsedSelector = parser.parseSelector(sel); + if (!parsedSelector) { + return new InvalidSelector(new Error("Empty selector")); + } + return createSelectorFromAst(parsedSelector.value); + } catch(e) { + return new InvalidSelector(e); } } diff --git a/tns-core-modules/ui/styling/font-common.ts b/tns-core-modules/ui/styling/font-common.ts index f06de20a8..9c307c0dc 100644 --- a/tns-core-modules/ui/styling/font-common.ts +++ b/tns-core-modules/ui/styling/font-common.ts @@ -1,7 +1,7 @@ import { Font as FontDefinition, ParsedFont } from "./font"; import { makeValidator, makeParser } from "../core/properties"; -export abstract class FontBase implements FontDefinition { +export abstract class Font implements FontDefinition { public static default = undefined; get isItalic(): boolean { @@ -23,14 +23,14 @@ export abstract class FontBase implements FontDefinition { public readonly fontWeight: FontWeight) { } - public abstract getAndroidTypeface(): android.graphics.Typeface; - public abstract getUIFont(defaultFont: UIFont): UIFont; - public abstract withFontFamily(family: string): FontBase; - public abstract withFontStyle(style: string): FontBase; - public abstract withFontWeight(weight: string): FontBase; - public abstract withFontSize(size: number): FontBase; + public abstract getAndroidTypeface(): any /* android.graphics.Typeface */; + public abstract getUIFont(defaultFont: any /* UIFont */): any /* UIFont */; + public abstract withFontFamily(family: string): Font; + public abstract withFontStyle(style: string): Font; + public abstract withFontWeight(weight: string): Font; + public abstract withFontSize(size: number): Font; - public static equals(value1: FontBase, value2: FontBase): boolean { + public static equals(value1: Font, value2: Font): boolean { // both values are falsy if (!value1 && !value2) { return true; diff --git a/tns-core-modules/ui/styling/font.android.ts b/tns-core-modules/ui/styling/font.android.ts index 821abad16..85af1c44f 100644 --- a/tns-core-modules/ui/styling/font.android.ts +++ b/tns-core-modules/ui/styling/font.android.ts @@ -1,4 +1,4 @@ -import { FontBase, parseFontFamily, genericFontFamilies, FontWeight } from "./font-common"; +import { Font as FontBase, parseFontFamily, genericFontFamilies, FontWeight } from "./font-common"; import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace"; import * as application from "../../application"; import * as fs from "../../file-system"; diff --git a/tns-core-modules/ui/styling/font.ios.ts b/tns-core-modules/ui/styling/font.ios.ts index 21c60a67c..6cf0afaf7 100644 --- a/tns-core-modules/ui/styling/font.ios.ts +++ b/tns-core-modules/ui/styling/font.ios.ts @@ -1,4 +1,4 @@ -import { FontBase, parseFontFamily, genericFontFamilies, FontStyle, FontWeight } from "./font-common"; +import { Font as FontBase, parseFontFamily, genericFontFamilies, FontStyle, FontWeight } from "./font-common"; import { isEnabled as traceEnabled, write as traceWrite, categories as traceCategories, messageType as traceMessageType } from "../../trace"; import { device } from "../../platform" import * as fs from "../../file-system"; diff --git a/tns-core-modules/ui/styling/style-properties.ts b/tns-core-modules/ui/styling/style-properties.ts index 7165ab0c9..b73f4141a 100644 --- a/tns-core-modules/ui/styling/style-properties.ts +++ b/tns-core-modules/ui/styling/style-properties.ts @@ -8,9 +8,9 @@ import { import { dip, px, percent } from "../core/view"; import { Color } from "../../color"; -import { Font, parseFont, FontStyle, FontWeight } from "./font"; +import { Font, parseFont, FontStyle, FontWeight } from "../../ui/styling/font"; import { layout } from "../../utils/utils"; -import { Background } from "./background"; +import { Background } from "../../ui/styling/background"; import { isIOS } from "../../platform"; import { Style } from "./style"; diff --git a/tns-core-modules/ui/styling/style-scope.ts b/tns-core-modules/ui/styling/style-scope.ts index b142d4d22..a8fa19575 100644 --- a/tns-core-modules/ui/styling/style-scope.ts +++ b/tns-core-modules/ui/styling/style-scope.ts @@ -8,6 +8,11 @@ import { parse as parseCss, Node as CssNode, } from "../../css"; +import { + CSS3Parser, + CSSNativeScript +} from "../../css/parser"; + import { RuleSet, SelectorsMap, @@ -42,6 +47,16 @@ function ensureCssAnimationParserModule() { } } +let parser: "rework" | "nativescript" = "rework"; +try { + const appConfig = require("~/package.json"); + if (appConfig && appConfig.cssParser === "nativescript") { + parser = "nativescript"; + } +} catch(e) { + // +} + export function mergeCssSelectors(): void { applicationCssSelectors = applicationSelectors.slice(); applicationCssSelectors.push.apply(applicationCssSelectors, applicationAdditionalSelectors); @@ -58,25 +73,49 @@ const pattern: RegExp = /('|")(.*?)\1/; class CSSSource { private _selectors: RuleSet[] = []; - private _ast: SyntaxTree; - private static cssFilesCache: { [path: string]: CSSSource } = {}; - private constructor(private _url: string, private _file: string, private _keyframes: KeyframesMap, private _source?: string) { - if (this._file && !this._source) { - this.load(); - } + private constructor(private _ast: SyntaxTree, private _url: string, private _file: string, private _keyframes: KeyframesMap, private _source: string) { this.parse(); } + public static fromURI(uri: string, keyframes: KeyframesMap): CSSSource { + try { + const cssOrAst = global.loadModule(uri); + if (cssOrAst) { + if (typeof cssOrAst === "string") { + return CSSSource.fromSource(cssOrAst, keyframes, uri); + } else if (typeof cssOrAst === "object" && cssOrAst.type === "stylesheet" && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) { + return CSSSource.fromAST(cssOrAst, keyframes, uri); + } else { + // Probably a webpack css-loader exported object. + return CSSSource.fromSource(cssOrAst.toString(), keyframes, uri); + } + } + } catch(e) { + // + } + return CSSSource.fromFile(uri, keyframes); + } + public static fromFile(url: string, keyframes: KeyframesMap): CSSSource { + const file = CSSSource.resolveCSSPathFromURL(url); + return new CSSSource(undefined, url, file, keyframes, undefined); + } + + @profile + public static resolveCSSPathFromURL(url: string): string { const app = knownFolders.currentApp().path; const file = resolveFileNameFromUrl(url, app, File.exists); - return new CSSSource(url, file, keyframes, undefined); + return file; } public static fromSource(source: string, keyframes: KeyframesMap, url?: string): CSSSource { - return new CSSSource(url, undefined, keyframes, source); + return new CSSSource(undefined, url, undefined, keyframes, source); + } + + public static fromAST(ast: SyntaxTree, keyframes: KeyframesMap, url?: string): CSSSource { + return new CSSSource(ast, url, undefined, keyframes, undefined); } get selectors(): RuleSet[] { return this._selectors; } @@ -90,24 +129,53 @@ class CSSSource { @profile private parse(): void { - if (this._source) { - try { - this._ast = this._source ? parseCss(this._source, { source: this._file }) : null; - // TODO: Don't merge arrays, instead chain the css files. - if (this._ast) { - this._selectors = [ - ...this.createSelectorsFromImports(), - ...this.createSelectorsFromSyntaxTree() - ]; + try { + if (!this._ast) { + if (!this._source && this._file) { + this.load(); + } + if (this._source) { + this.parseCSSAst(); } - } catch (e) { - traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error); } - } else { + if (this._ast) { + this.createSelectors(); + } else { + this._selectors = []; + } + } catch (e) { + traceWrite("Css styling failed: " + e, traceCategories.Error, traceMessageType.error); this._selectors = []; } } + @profile + private parseCSSAst() { + if (this._source) { + switch(parser) { + case "nativescript": + const cssparser = new CSS3Parser(this._source); + const stylesheet = cssparser.parseAStylesheet(); + const cssNS = new CSSNativeScript(); + this._ast = cssNS.parseStylesheet(stylesheet); + return; + case "rework": + this._ast = parseCss(this._source, { source: this._file }); + return; + } + } + } + + @profile + private createSelectors() { + if (this._ast) { + this._selectors = [ + ...this.createSelectorsFromImports(), + ...this.createSelectorsFromSyntaxTree() + ]; + } + } + private createSelectorsFromImports(): RuleSet[] { let selectors: RuleSet[] = []; const imports = this._ast["stylesheet"]["rules"].filter(r => r.type === "import"); @@ -118,7 +186,7 @@ class CSSSource { const url = match && match[2]; if (url !== null && url !== undefined) { - const cssFile = CSSSource.fromFile(url, this._keyframes); + const cssFile = CSSSource.fromURI(url, this._keyframes); selectors = selectors.concat(cssFile.selectors); } } @@ -169,7 +237,7 @@ const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => { return undefined; } - const result = CSSSource.fromFile(cssFile, applicationKeyframes).selectors; + const result = CSSSource.fromURI(cssFile, applicationKeyframes).selectors; if (result.length > 0) { applicationSelectors = result; mergeCssSelectors(); @@ -179,15 +247,15 @@ const loadCss = profile(`"style-scope".loadCss`, (cssFile: string) => { application.on("cssChanged", onCssChanged); application.on("livesync", onLiveSync); -export const loadCssOnLaunch = profile('"style-scope".loadCssOnLaunch', () => { - loadCss(application.getCssFileName()); - application.off("launch", loadCssOnLaunch); +export const loadAppCSS = profile('"style-scope".loadAppCSS', (args: application.LoadAppCSSEventData) => { + loadCss(args.cssFile); + application.off("loadAppCss", loadAppCSS); }); if (application.hasLaunched()) { - loadCssOnLaunch(); + loadAppCSS({ eventName: "loadAppCss", object: application, cssFile: application.getCssFileName() }); } else { - application.on("launch", loadCssOnLaunch); + application.on("loadAppCss", loadAppCSS); } export class CssState { diff --git a/tns-core-modules/ui/styling/style/style.d.ts b/tns-core-modules/ui/styling/style/style.d.ts index 65b101e93..fb299139e 100644 --- a/tns-core-modules/ui/styling/style/style.d.ts +++ b/tns-core-modules/ui/styling/style/style.d.ts @@ -62,6 +62,7 @@ export class Style extends Observable { public tintColor: Color; public placeholderColor: Color; + public background: string | Color; public backgroundColor: Color; public backgroundImage: string; public backgroundRepeat: BackgroundRepeat; diff --git a/tns-core-modules/ui/styling/style/style.ts b/tns-core-modules/ui/styling/style/style.ts index 5bd84b7f6..9c0b1c670 100644 --- a/tns-core-modules/ui/styling/style/style.ts +++ b/tns-core-modules/ui/styling/style/style.ts @@ -35,6 +35,7 @@ export class Style extends Observable implements StyleDefinition { public tintColor: Color; public placeholderColor: Color; + public background: string | Color; public backgroundColor: Color; public backgroundImage: string; public backgroundRepeat: BackgroundRepeat; diff --git a/tns-core-modules/utils/debug.android.ts b/tns-core-modules/utils/debug.android.ts deleted file mode 100644 index ab0a391a9..000000000 --- a/tns-core-modules/utils/debug.android.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Source } from "./debug-common"; -export * from "./debug-common"; - -export class ScopeError extends Error { - constructor(inner: Error, message?: string) { - let formattedMessage; - if (message && inner.message) { - formattedMessage = message + "\n > " + inner.message.replace("\n", "\n "); - } else { - formattedMessage = message || inner.message || undefined; - } - super(formattedMessage); - this.stack = "Error: " + this.message + "\n" + inner.stack.substr(inner.stack.indexOf("\n") + 1); - this.message = formattedMessage; - } -} - -export class SourceError extends ScopeError { - constructor(child: Error, source: Source, message?: string) { - super(child, message ? message + " @" + source + "" : source + ""); - } -} diff --git a/tns-core-modules/utils/debug.ios.ts b/tns-core-modules/utils/debug.ios.ts deleted file mode 100644 index ee09f31fd..000000000 --- a/tns-core-modules/utils/debug.ios.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Source } from "./debug-common"; -export * from "./debug-common"; - -export class ScopeError extends Error { - constructor(inner: Error, message?: string) { - let formattedMessage; - if (message && inner.message) { - formattedMessage = message + "\n > " + inner.message.replace("\n", "\n "); - } else { - formattedMessage = message || inner.message || undefined; - } - super(formattedMessage); - this.stack = inner.stack; - this.message = formattedMessage; - } -} - -export class SourceError extends ScopeError { - constructor(child: Error, source: Source, message?: string) { - super(child, message ? message + " @" + source + "" : source + ""); - } -} diff --git a/tns-core-modules/utils/debug-common.ts b/tns-core-modules/utils/debug.ts similarity index 63% rename from tns-core-modules/utils/debug-common.ts rename to tns-core-modules/utils/debug.ts index 7ea2ff4f6..189941756 100644 --- a/tns-core-modules/utils/debug-common.ts +++ b/tns-core-modules/utils/debug.ts @@ -1,4 +1,5 @@ import { knownFolders } from "../file-system" +import { isAndroid } from "../platform" export var debug = true; @@ -45,3 +46,23 @@ export class Source { object[Source._source] = src; } } + +export class ScopeError extends Error { + constructor(inner: Error, message?: string) { + let formattedMessage; + if (message && inner.message) { + formattedMessage = message + "\n > " + inner.message.replace("\n", "\n "); + } else { + formattedMessage = message || inner.message || undefined; + } + super(formattedMessage); + this.stack = isAndroid ? "Error: " + this.message + "\n" + inner.stack.substr(inner.stack.indexOf("\n") + 1) : inner.stack; + this.message = formattedMessage; + } +} + +export class SourceError extends ScopeError { + constructor(child: Error, source: Source, message?: string) { + super(child, message ? message + " @" + source + "" : source + ""); + } +} diff --git a/tsconfig.json b/tsconfig.json index aa85749ce..284ad433d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,12 +22,6 @@ "tns-platform-declarations/references.d.ts", "tns-core-modules/references.d.ts", "tns-platform-declarations/ios/objc-x86_64/", - "node-tests/" - ], - "compilerOptions": { - "baseUrl": ".", - "paths": { - "tns-core-modules/*": ["tns-core-modules/*"] - } - } + "unit-tests/common-types.d.ts" + ] } diff --git a/tsconfig.node-tests.json b/tsconfig.node-tests.json deleted file mode 100644 index efa880670..000000000 --- a/tsconfig.node-tests.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "./tsconfig.shared", - "include": [ - "tns-core-modules/js-libs/easysax/**/*.ts", - "tns-core-modules/module.d.ts", - "tns-core-modules/lib.core.d.ts", - "tns-core-modules/lib.dom.d.ts", - "tns-core-modules/es-collections.d.ts", - "tns-core-modules/declarations.d.ts", - "tns-core-modules/es6-promise.d.ts", - "node-tests/**/*.ts" - ], - "compilerOptions": { - "types": ["node"] - } -} diff --git a/tsconfig.shared.json b/tsconfig.shared.json index ba55cc870..eea37a566 100644 --- a/tsconfig.shared.json +++ b/tsconfig.shared.json @@ -16,6 +16,14 @@ "lib": [ "es6", "dom" ], - "types": [] + "types": [ + "node", + "chai", + "mocha" + ], + "baseUrl": ".", + "paths": { + "tns-core-modules/*": ["tns-core-modules/*"] + } } } diff --git a/tsconfig.unit-tests.json b/tsconfig.unit-tests.json new file mode 100644 index 000000000..ad369828d --- /dev/null +++ b/tsconfig.unit-tests.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "sourceMap": true, + "inlineSourceMap": false + }, + "extends": "./tsconfig.shared", + "include": [ + "tns-core-modules/**/*.ts", + "unit-tests/**/*.ts" + ], + "exclude": [ + "**/*.android.ts", + "**/*.ios.ts", + "tns-platform-declarations", + "tns-core-modules/node-modules", + "tns-core-modules/references.d.ts" + ] +} diff --git a/unit-tests/common-types.d.ts b/unit-tests/common-types.d.ts new file mode 100644 index 000000000..11e4ae93a --- /dev/null +++ b/unit-tests/common-types.d.ts @@ -0,0 +1,33 @@ +declare var UIColor, PHAsset, NSSearchPathDirectory; +declare type UIColor = any; +declare type PHAsset = any; + +declare namespace android { + export namespace content { + export type Context = any; + export var Context: any; + } + export namespace view { + export type MotionEvent = any; + export var MotionEvent: any; + } + export namespace util { + export var Base64: any; + export var Log: any; + } + export namespace graphics.Bitmap.CompressFormat { + export var PNG: any; + export var android: any; + } +} + +declare namespace org.nativescript.widgets { + export var ViewHelper: any; +} +declare namespace org.nativescript.widgets.Async.Http { + export type RequestResult = any; + export var RequestResult: any; +} + +declare type java = any; +declare var java: any; \ No newline at end of file diff --git a/unit-tests/css/assets/core.light.css b/unit-tests/css/assets/core.light.css new file mode 100644 index 000000000..777c15a6f --- /dev/null +++ b/unit-tests/css/assets/core.light.css @@ -0,0 +1,6 @@ +/*! +* NativeScript Theme v1.0.4 (https://nativescript.org) +* Copyright 2016-2016 The Theme Authors +* Copyright 2016-2016 Telerik +* Licensed under MIT (https://github.com/NativeScript/theme/blob/master/LICENSE) +*/.c-white{color:#fff}.c-bg-white{background-color:#fff}.c-black{color:#000}.c-bg-black{background-color:#000}.c-aqua{color:#00caab}.c-bg-aqua{background-color:#00caab}.c-blue{color:#3d5afe}.c-bg-blue{background-color:#3d5afe}.c-charcoal{color:#303030}.c-bg-charcoal{background-color:#303030}.c-brown{color:#795548}.c-bg-brown{background-color:#795548}.c-forest{color:#006968}.c-bg-forest{background-color:#006968}.c-grey{color:#e0e0e0}.c-bg-grey{background-color:#e0e0e0}.c-grey-light{color:#bababa}.c-bg-grey-light{background-color:#bababa}.c-grey-dark{color:#5c687c}.c-bg-grey-dark{background-color:#5c687c}.c-purple{color:#8130ff}.c-bg-purple{background-color:#8130ff}.c-lemon{color:#ffea00}.c-bg-lemon{background-color:#ffea00}.c-lime{color:#aee406}.c-bg-lime{background-color:#aee406}.c-orange{color:#f57c00}.c-bg-orange{background-color:#f57c00}.c-ruby{color:#ff1744}.c-bg-ruby{background-color:#ff1744}.c-sky{color:#30bcff}.c-bg-sky{background-color:#30bcff}.w-full{width:100%}.w-100{width:100}.h-full{height:100%}.h-100{height:100}.m-0{margin:0}.m-t-0{margin-top:0}.m-r-0{margin-right:0}.m-b-0{margin-bottom:0}.m-l-0{margin-left:0}.m-x-0{margin-right:0;margin-left:0}.m-y-0{margin-top:0;margin-bottom:0}.m-2{margin:2}.m-t-2{margin-top:2}.m-r-2{margin-right:2}.m-b-2{margin-bottom:2}.m-l-2{margin-left:2}.m-x-2{margin-right:2;margin-left:2}.m-y-2{margin-top:2;margin-bottom:2}.m-4{margin:4}.m-t-4{margin-top:4}.m-r-4{margin-right:4}.m-b-4{margin-bottom:4}.m-l-4{margin-left:4}.m-x-4{margin-right:4;margin-left:4}.m-y-4{margin-top:4;margin-bottom:4}.m-5{margin:5}.m-t-5{margin-top:5}.m-r-5{margin-right:5}.m-b-5{margin-bottom:5}.m-l-5{margin-left:5}.m-x-5{margin-right:5;margin-left:5}.m-y-5{margin-top:5;margin-bottom:5}.m-8{margin:8}.m-t-8{margin-top:8}.m-r-8{margin-right:8}.m-b-8{margin-bottom:8}.m-l-8{margin-left:8}.m-x-8{margin-right:8;margin-left:8}.m-y-8{margin-top:8;margin-bottom:8}.m-10{margin:10}.m-t-10{margin-top:10}.m-r-10{margin-right:10}.m-b-10{margin-bottom:10}.m-l-10{margin-left:10}.m-x-10{margin-right:10;margin-left:10}.m-y-10{margin-top:10;margin-bottom:10}.m-12{margin:12}.m-t-12{margin-top:12}.m-r-12{margin-right:12}.m-b-12{margin-bottom:12}.m-l-12{margin-left:12}.m-x-12{margin-right:12;margin-left:12}.m-y-12{margin-top:12;margin-bottom:12}.m-15{margin:15}.m-t-15{margin-top:15}.m-r-15{margin-right:15}.m-b-15{margin-bottom:15}.m-l-15{margin-left:15}.m-x-15{margin-right:15;margin-left:15}.m-y-15{margin-top:15;margin-bottom:15}.m-16{margin:16}.m-t-16{margin-top:16}.m-r-16{margin-right:16}.m-b-16{margin-bottom:16}.m-l-16{margin-left:16}.m-x-16{margin-right:16;margin-left:16}.m-y-16{margin-top:16;margin-bottom:16}.m-20{margin:20}.m-t-20{margin-top:20}.m-r-20{margin-right:20}.m-b-20{margin-bottom:20}.m-l-20{margin-left:20}.m-x-20{margin-right:20;margin-left:20}.m-y-20{margin-top:20;margin-bottom:20}.m-24{margin:24}.m-t-24{margin-top:24}.m-r-24{margin-right:24}.m-b-24{margin-bottom:24}.m-l-24{margin-left:24}.m-x-24{margin-right:24;margin-left:24}.m-y-24{margin-top:24;margin-bottom:24}.m-25{margin:25}.m-t-25{margin-top:25}.m-r-25{margin-right:25}.m-b-25{margin-bottom:25}.m-l-25{margin-left:25}.m-x-25{margin-right:25;margin-left:25}.m-y-25{margin-top:25;margin-bottom:25}.m-28{margin:28}.m-t-28{margin-top:28}.m-r-28{margin-right:28}.m-b-28{margin-bottom:28}.m-l-28{margin-left:28}.m-x-28{margin-right:28;margin-left:28}.m-y-28{margin-top:28;margin-bottom:28}.m-30{margin:30}.m-t-30{margin-top:30}.m-r-30{margin-right:30}.m-b-30{margin-bottom:30}.m-l-30{margin-left:30}.m-x-30{margin-right:30;margin-left:30}.m-y-30{margin-top:30;margin-bottom:30}.p-0{padding:0}.p-t-0{padding-top:0}.p-r-0{padding-right:0}.p-b-0{padding-bottom:0}.p-l-0{padding-left:0}.p-x-0{padding-right:0;padding-left:0}.p-y-0{padding-top:0;padding-bottom:0}.p-2{padding:2}.p-t-2{padding-top:2}.p-r-2{padding-right:2}.p-b-2{padding-bottom:2}.p-l-2{padding-left:2}.p-x-2{padding-right:2;padding-left:2}.p-y-2{padding-top:2;padding-bottom:2}.p-4{padding:4}.p-t-4{padding-top:4}.p-r-4{padding-right:4}.p-b-4{padding-bottom:4}.p-l-4{padding-left:4}.p-x-4{padding-right:4;padding-left:4}.p-y-4{padding-top:4;padding-bottom:4}.p-5{padding:5}.p-t-5{padding-top:5}.p-r-5{padding-right:5}.p-b-5{padding-bottom:5}.p-l-5{padding-left:5}.p-x-5{padding-right:5;padding-left:5}.p-y-5{padding-top:5;padding-bottom:5}.p-8{padding:8}.p-t-8{padding-top:8}.p-r-8{padding-right:8}.p-b-8{padding-bottom:8}.p-l-8{padding-left:8}.p-x-8{padding-right:8;padding-left:8}.p-y-8{padding-top:8;padding-bottom:8}.p-10{padding:10}.p-t-10{padding-top:10}.p-r-10{padding-right:10}.p-b-10{padding-bottom:10}.p-l-10{padding-left:10}.p-x-10{padding-right:10;padding-left:10}.p-y-10{padding-top:10;padding-bottom:10}.p-12{padding:12}.p-t-12{padding-top:12}.p-r-12{padding-right:12}.p-b-12{padding-bottom:12}.p-l-12{padding-left:12}.p-x-12{padding-right:12;padding-left:12}.p-y-12{padding-top:12;padding-bottom:12}.p-15{padding:15}.p-t-15{padding-top:15}.p-r-15{padding-right:15}.p-b-15{padding-bottom:15}.p-l-15{padding-left:15}.p-x-15{padding-right:15;padding-left:15}.p-y-15{padding-top:15;padding-bottom:15}.p-16{padding:16}.p-t-16{padding-top:16}.p-r-16{padding-right:16}.p-b-16{padding-bottom:16}.p-l-16{padding-left:16}.p-x-16{padding-right:16;padding-left:16}.p-y-16{padding-top:16;padding-bottom:16}.p-20{padding:20}.p-t-20{padding-top:20}.p-r-20{padding-right:20}.p-b-20{padding-bottom:20}.p-l-20{padding-left:20}.p-x-20{padding-right:20;padding-left:20}.p-y-20{padding-top:20;padding-bottom:20}.p-24{padding:24}.p-t-24{padding-top:24}.p-r-24{padding-right:24}.p-b-24{padding-bottom:24}.p-l-24{padding-left:24}.p-x-24{padding-right:24;padding-left:24}.p-y-24{padding-top:24;padding-bottom:24}.p-25{padding:25}.p-t-25{padding-top:25}.p-r-25{padding-right:25}.p-b-25{padding-bottom:25}.p-l-25{padding-left:25}.p-x-25{padding-right:25;padding-left:25}.p-y-25{padding-top:25;padding-bottom:25}.p-28{padding:28}.p-t-28{padding-top:28}.p-r-28{padding-right:28}.p-b-28{padding-bottom:28}.p-l-28{padding-left:28}.p-x-28{padding-right:28;padding-left:28}.p-y-28{padding-top:28;padding-bottom:28}.p-30{padding:30}.p-t-30{padding-top:30}.p-r-30{padding-right:30}.p-b-30{padding-bottom:30}.p-l-30{padding-left:30}.p-x-30{padding-right:30;padding-left:30}.p-y-30{padding-top:30;padding-bottom:30}.hr-light{height:1;background-color:#e0e0e0;width:100%}.hr-dark{height:1;background-color:#303030;width:100%}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.font-weight-normal{font-weight:normal}.font-weight-bold{font-weight:bold}.font-italic{font-style:italic}.t-10{font-size:10}.t-12{font-size:12}.t-14{font-size:14}.t-15{font-size:15}.t-16{font-size:16}.t-17{font-size:17}.t-18{font-size:18}.t-19{font-size:19}.t-20{font-size:20}.t-25{font-size:25}.t-30{font-size:30}.img-rounded{border-radius:5}.img-circle{border-radius:20}.img-thumbnail{border-radius:0}.invisible{visibility:collapse}.pull-left{horizontal-align:left}.pull-right{horizontal-align:right}.m-x-auto{horizontal-align:center}.m-y-auto{vertical-align:center}.text-primary{color:#30bcff}.text-danger{color:#d50000}.text-muted{color:#9e9e9e}.bg-primary{background-color:#30bcff;color:#fff}.bg-danger{background-color:#d50000;color:#fff}.action-bar{background-color:#F8F8F8;color:#212121}.action-bar .action-bar-title{font-weight:bold;font-size:17;vertical-align:center}.action-bar .action-item{font-weight:normal}.activity-indicator{color:#30bcff;width:30;height:30}.btn{color:#30bcff;background-color:transparent;min-height:36;min-width:64;padding:10 10 10 10;font-size:18;margin:8 16 8 16}.btn.btn-active:highlighted{color:#fff;background-color:#c0ebff}.btn-primary{background-color:#30bcff;border-color:#30bcff;color:#fff}.btn-primary.btn-active:highlighted{background-color:#01a0ec;border-color:#01a0ec}.btn-primary.btn-aqua{background-color:#00caab}.btn-primary.btn-blue{background-color:#3d5afe}.btn-primary.btn-brown{background-color:#795548}.btn-primary.btn-forest{background-color:#006968}.btn-primary.btn-grey{background-color:#5c687c}.btn-primary.btn-lemon{background-color:#ffea00;color:#000}.btn-primary.btn-lime{background-color:#aee406;color:#000}.btn-primary.btn-orange{background-color:#f57c00}.btn-primary.btn-purple{background-color:#8130ff}.btn-primary.btn-ruby{background-color:#ff1744}.btn-primary.btn-sky{background-color:#30bcff}.btn-outline{background-color:transparent;border-color:#30bcff;color:#30bcff}.btn-outline.btn-active:highlighted{background-color:#c0ebff}.btn[isEnabled=false]{color:#a4a4a4;background-color:#e0e0e0;border-color:#e0e0e0}.fa{font-family:FontAwesome, fontawesome-webfont}.form .input{padding:16 8 16 8;background-color:transparent}.form .input.input-border{border-width:1;border-color:#e0e0e0;border-radius:2;padding:16}.form .input.input-rounded{border-width:1;border-color:#e0e0e0;border-radius:28;padding:16}.form .input[isEnabled=false]{background-color:#fafafa}.form .input-field{margin:8}.form .input-field .label{font-size:12;color:#bababa}.form .input-field .input{padding:0;margin:0 0 8 0}.form .input-field .hr-light.active,.form .input-field .hr-dark.active{background-color:#30bcff}.form .input-field.input-sides .label{font-size:18;margin:0 0 8 0}.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:4;font-weight:normal;color:#212121}.body,.body2,.footnote{font-weight:normal;color:#757575}.h1{font-size:32}.h2{font-size:22}.h3{font-size:15}.h4{font-size:12}.h5{font-size:11}.h6{font-size:10}.body{font-size:14}.body2{font-size:17}.footnote{font-size:13}.list-group .list-group-item{color:#212121;font-size:16;margin:0;padding:16}.list-group .list-group-item Label{vertical-align:center}.list-group .list-group-item .thumb{stretch:fill;width:40;height:40;margin-right:16}.list-group .list-group-item.active{background-color:#e0e0e0}.list-group .list-group-item .list-group-item-text{color:#757575;font-size:14}.page{background-color:#fff}.progress{color:#30bcff;background-color:#bababa}.segmented-bar{font-size:13;background-color:#fff;color:#212121;selected-background-color:#30bcff}.sidedrawer-left,.sidedrawer-center{background-color:#fafafa}.sidedrawer-header{background-color:#fafafa;height:148;width:100%}.sidedrawer-left .sidedrawer-header{padding:16 16 0 16}.sidedrawer-center .sidedrawer-header{padding:20 15 0 15}.sidedrawer-header-image{background-color:#e0e0e0}.sidedrawer-left .sidedrawer-header-image{height:64;width:64;border-radius:32;horizontal-align:left;margin-bottom:36}.sidedrawer-center .sidedrawer-header-image{height:74;width:74;border-radius:37;horizontal-align:center;margin-bottom:24}.sidedrawer-header-brand{color:#737373}.sidedrawer-left .sidedrawer-header-brand{horizontal-align:left;font-size:14}.sidedrawer-center .sidedrawer-header-brand{horizontal-align:center;font-size:15}.sidedrawer-list-item{height:48;horizontal-align:left;width:100%;orientation:horizontal}.sidedrawer-list-item .sidedrawer-list-item-icon{width:24;text-align:center;font-size:20;height:48;vertical-align:center}.sidedrawer-list-item.active{color:#fff;background-color:#30bcff}.sidedrawer-list-item.active .sidedrawer-list-item-icon{color:#fff}.sidedrawer-left .sidedrawer-list-item-icon{margin:0 16 0 16}.sidedrawer-center .sidedrawer-list-item-icon{margin:0 0 0 15}.sidedrawer-list-item-text{horizontal-align:left;text-align:left;font-size:15;background-color:transparent;border-width:0.1;width:80%;vertical-align:center}.sidedrawer-left .sidedrawer-list-item-text{padding-left:16}.sidedrawer-center .sidedrawer-list-item-text{padding-left:15}.slider{background-color:#30bcff}.slider[isEnabled=false]{background-color:#e0e0e0;color:#e0e0e0}.switch[checked=true]{background-color:#30bcff}.switch[checked=true][isEnabled=false]{background-color:#e0e0e0;color:#fff}.switch[isEnabled=false]{background-color:#e0e0e0;color:#e0e0e0}.tab-view{selected-color:#30bcff;tabs-background-color:#fff}.tab-view .tab-view-item{background-color:#fff;tabs-background-color:#fff}#login-background{margin-top:-20;background-size:cover;background-position:center}.login-wrap{padding:0 40}.logo-wrap{margin:60 0 10 0;padding:20 0}.logo-wrap .login-logo{text-align:center;font-size:30;font-weight:bold;margin-bottom:10;opacity:1;color:#212121;opacity:.9}.logo-wrap .login-logo-sub{color:#212121;opacity:.8;text-align:center}.login-wrapper{padding:20;background-color:#fff;border-radius:3}.login-wrapper TextField{padding:10 10;margin:10 0 0 0}.go-back{font-size:14;text-align:center;color:#212121;margin-top:10}.btn{border-width:0;font-family:'SF UI Text Medium';font-size:15}.btn-outline{border-width:1}.btn-rounded-sm{border-radius:4}.btn-rounded-lg{border-radius:19}.form{font-family:'SF UI Text Regular'}.form .input{font-size:15}.form .input.input-rounded{border-radius:27}.h1{font-size:32}.slider{margin:10 15}.sidedrawer-list-item-icon,.sidedrawer-list-item{color:#949494}.switch{margin:8 15}.list-group .list-group-item{padding:16 15 16 15}.list-group .list-group-item .thumb{margin-right:15}.list-group .list-group-item .list-group-item-heading{margin-bottom:5}.segmented-bar{margin:0 15;color:#30bcff} diff --git a/unit-tests/css/assets/what-is-new.ios.css b/unit-tests/css/assets/what-is-new.ios.css new file mode 100644 index 000000000..a20e0940a --- /dev/null +++ b/unit-tests/css/assets/what-is-new.ios.css @@ -0,0 +1,23 @@ +@import url('~/views/what-is-new-common.css'); + +.news-card { + margin: 12 12 0 12; +} + +.title { + font-size: 14; +} +.body { + font-size: 14; +} +.learn-more { + font-size: 14; +} +.date { + font-size: 12; +} + +.empty-placeholder { + vertical-align: center; + text-align: center; +} \ No newline at end of file diff --git a/unit-tests/css/out/.gitignore b/unit-tests/css/out/.gitignore new file mode 100644 index 000000000..09feb5054 --- /dev/null +++ b/unit-tests/css/out/.gitignore @@ -0,0 +1,3 @@ +*.* +!*.md +!.gitignore \ No newline at end of file diff --git a/unit-tests/css/out/README.md b/unit-tests/css/out/README.md new file mode 100644 index 000000000..b33a120cc --- /dev/null +++ b/unit-tests/css/out/README.md @@ -0,0 +1,2 @@ +Some tests output .json files in attempt to serialize the CSS. +These are used for reference during development, that's why the folder is gitignored. \ No newline at end of file diff --git a/unit-tests/css/parser.ts b/unit-tests/css/parser.ts new file mode 100644 index 000000000..49484e51c --- /dev/null +++ b/unit-tests/css/parser.ts @@ -0,0 +1,461 @@ +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(parse: (value: string, lastIndex?: number) => T, value: string, expected: T); + function test(parse: (value: string, lastIndex?: number) => T, value: string, lastIndex: number, expected: T); + function test(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, 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]}); + (["=", "^=", "$=", "*=", "=", "~=", "|="]).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); + }); + }); + }); +}); diff --git a/unit-tests/mocha.opts b/unit-tests/mocha.opts new file mode 100644 index 000000000..708951c41 --- /dev/null +++ b/unit-tests/mocha.opts @@ -0,0 +1,4 @@ +--require unit-tests/runtime.js +--ui mocha-typescript +--require source-map-support/register +--recursive unit-tests \ No newline at end of file diff --git a/unit-tests/package.json b/unit-tests/package.json new file mode 100644 index 000000000..544b7b4dd --- /dev/null +++ b/unit-tests/package.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/unit-tests/polyfills/file-system-access.ts b/unit-tests/polyfills/file-system-access.ts new file mode 100644 index 000000000..e9d4b0af8 --- /dev/null +++ b/unit-tests/polyfills/file-system-access.ts @@ -0,0 +1,7 @@ +import * as path from "path"; + +export class FileSystemAccess { + public getPathSeparator(): string { + return path.sep; + } +} \ No newline at end of file diff --git a/unit-tests/polyfills/platform.ts b/unit-tests/polyfills/platform.ts new file mode 100644 index 000000000..e69de29bb diff --git a/unit-tests/runtime.ts b/unit-tests/runtime.ts new file mode 100644 index 000000000..118271758 --- /dev/null +++ b/unit-tests/runtime.ts @@ -0,0 +1,20 @@ +import "tslib"; + +import * as moduleAlias from "module-alias"; +import * as path from "path"; + +const tnsCoreModules = path.resolve(__dirname, "..", "tns-core-modules"); + +moduleAlias.addPath(tnsCoreModules); +moduleAlias.addAliases({ + // NOTE: require("tns-core-modules/platform") with these aliases will work in node but fail in Angular AoT + // "tns-core-modules/platform": path.resolve(__dirname, "polyfills", "platform"), + // "tns-core-modules/file-system/file-system-access": path.resolve(__dirname, "polyfills", "file-system-access"), + // "tns-core-modules/utils/utils": path.resolve(tnsCoreModules, "utils/utils-common"), + // "tns-core-modules/color": path.resolve(tnsCoreModules, "color/color-common"), + // "tns-core-modules/ui/styling/font": path.resolve(tnsCoreModules, "ui/styling/font-common"), + // "tns-core-modules/ui/styling/background": path.resolve(tnsCoreModules, "ui/styling/background-common"), + + "tns-core-modules": tnsCoreModules, + "~": __dirname +}); diff --git a/unit-tests/ui/styling/css-selector.ts b/unit-tests/ui/styling/css-selector.ts new file mode 100644 index 000000000..3d2945d14 --- /dev/null +++ b/unit-tests/ui/styling/css-selector.ts @@ -0,0 +1,288 @@ +import { assert } from "chai"; +import * as parser from "tns-core-modules/css"; +import * as selector from "tns-core-modules/ui/styling/css-selector"; + +describe("ui", () => { + describe("styling", () => { + describe("css-selectors", () => { + describe("match", () => { + it("button[attr]", () => { + const sel = selector.createSelector("button[testAttr]"); + assert.isTrue(sel.match({ + cssType: "button", + testAttr: true + })); + assert.isFalse(sel.match({ + cssType: "button" + })); + }); + + function create(css: string, source: string = "css-selectors.ts@test"): { rules: selector.RuleSet[], map: selector.SelectorsMap } { + let parse = parser.parse(css, { source }); + let rulesAst = parse.stylesheet.rules.filter(n => n.type === "rule"); + let rules = selector.fromAstNodes(rulesAst); + let map = new selector.SelectorsMap(rules); + return { rules, map }; + } + + function createOne(css: string, source: string = "css-selectors.ts@test"): selector.RuleSet { + let {rules} = create(css, source); + assert.equal(rules.length, 1); + return rules[0]; + } + + it("single selector", () => { + let rule = createOne(`* { color: red; }`); + assert.isTrue(rule.selectors[0].match({ cssType: "button" })); + assert.isTrue(rule.selectors[0].match({ cssType: "image" })); + }); + + it("two selectors", () => { + let rule = createOne(`button, image { color: red; }`); + assert.isTrue(rule.selectors[0].match({ cssType: "button" })); + assert.isTrue(rule.selectors[1].match({ cssType: "image" })); + assert.isFalse(rule.selectors[0].match({ cssType: "stacklayout" })); + assert.isFalse(rule.selectors[1].match({ cssType: "stacklayout" })); + }); + + it("narrow selection", () => { + let {map} = create(` + .login { color: blue; } + button { color: red; } + image { color: green; } + `); + + let buttonQuerry = map.query({ cssType: "button" }).selectors; + assert.equal(buttonQuerry.length, 1); + assert.includeDeepMembers(buttonQuerry[0].ruleset.declarations, [ + { property: "color", value: "red" } + ]); + + let imageQuerry = map.query({ cssType: "image", cssClasses: new Set(["login"]) }).selectors; + assert.equal(imageQuerry.length, 2); + // Note class before type + assert.includeDeepMembers(imageQuerry[0].ruleset.declarations, [ + { property: "color", value: "green" } + ]); + assert.includeDeepMembers(imageQuerry[1].ruleset.declarations, [ + { property: "color", value: "blue" } + ]); + }); + + let positiveMatches = { + "*": (view) => true, + "type": (view) => view.cssType === "type", + "#id": (view) => view.id === "id", + ".class": (view) => view.cssClasses.has("class"), + ":pseudo": (view) => view.cssPseudoClasses.has("pseudo"), + "[src1]": (view) => "src1" in view, + "[src2='src-value']": (view) => view['src2'] === 'src-value' + } + + let positivelyMatchingView = { + cssType: "type", + id: "id", + cssClasses: new Set(["class"]), + cssPseudoClasses: new Set(["pseudo"]), + "src1": "src", + "src2": "src-value" + } + + let negativelyMatchingView = { + cssType: "nottype", + id: "notid", + cssClasses: new Set(["notclass"]), + cssPseudoClasses: new Set(["notpseudo"]), + // Has no "src1" + "src2": "not-src-value" + } + + it("simple selectors match", () => { + for (let sel in positiveMatches) { + let css = sel + " { color: red; }"; + let rule = createOne(css); + assert.isTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css); + if (sel !== "*") { + assert.isFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css); + } + } + }); + + it("two selector sequence positive match", () => { + for (let firstStr in positiveMatches) { + for (let secondStr in positiveMatches) { + if (secondStr !== firstStr && secondStr !== "*" && secondStr !== "type") { + let css = firstStr + secondStr + " { color: red; }"; + let rule = createOne(css); + assert.isTrue(rule.selectors[0].match(positivelyMatchingView), "Expected successful match for: " + css); + if (firstStr !== "*") { + assert.isFalse(rule.selectors[0].match(negativelyMatchingView), "Expected match failure for: " + css); + } + } + } + } + }); + + it("direct parent combinator", () => { + let rule = createOne(`listview > item:selected { color: red; }`); + assert.isTrue(rule.selectors[0].match({ + cssType: "item", + cssPseudoClasses: new Set(["selected"]), + parent: { + cssType: "listview" + } + }), "Item in list view expected to match"); + assert.isFalse(rule.selectors[0].match({ + cssType: "item", + cssPseudoClasses: new Set(["selected"]), + parent: { + cssType: "stacklayout", + parent: { + cssType: "listview" + } + } + }), "Item in stack in list view NOT expected to match."); + }); + + it("ancestor combinator", () => { + let rule = createOne(`listview item:selected { color: red; }`); + assert.isTrue(rule.selectors[0].match({ + cssType: "item", + cssPseudoClasses: new Set(["selected"]), + parent: { + cssType: "listview" + } + }), "Item in list view expected to match"); + assert.isTrue(rule.selectors[0].match({ + cssType: "item", + cssPseudoClasses: new Set(["selected"]), + parent: { + cssType: "stacklayout", + parent: { + cssType: "listview" + } + } + }), "Item in stack in list view expected to match."); + assert.isFalse(rule.selectors[0].match({ + cssType: "item", + cssPseudoClasses: new Set(["selected"]), + parent: { + cssType: "stacklayout", + parent: { + cssType: "page" + } + } + }), "Item in stack in page NOT expected to match."); + }); + + it("backtracking css selector", () => { + let sel = createOne(`a>b c { color: red; }`).selectors[0]; + let child = { + cssType: "c", + parent: { + cssType: "b", + parent: { + cssType: "fail", + parent: { + cssType: "b", + parent: { + cssType: "a" + } + } + } + } + } + assert.isTrue(sel.match(child)); + }); + + function toString() { return this.cssType; } + + it("simple query match", () => { + let {map} = create(`list grid[promotion] button:highlighted { color: red; }`); + + let list, grid, button; + + button = { + cssType: "button", + cssPseudoClasses: new Set(["highlighted"]), + toString, + parent: grid = { + cssType: "grid", + promotion: true, + toString, + parent: list = { + cssType: "list", + toString + } + } + } + + let match = map.query(button); + assert.equal(match.selectors.length, 1, "Expected match to have one selector."); + + let expected = new Map() + .set(grid, { attributes: new Set(["promotion"]) }) + .set(button, { pseudoClasses: new Set(["highlighted"]) }); + + assert.deepEqual(match.changeMap, expected); + }); + + it("query match one child group", () => { + let {map} = create(`#prod[special] > gridlayout { color: red; }`); + let gridlayout, prod; + + gridlayout = { + cssType: "gridlayout", + toString, + parent: prod = { + id: "prod", + cssType: "listview", + toString + } + }; + + let match = map.query(gridlayout); + assert.equal(match.selectors.length, 1, "Expected match to have one selector."); + + let expected = new Map().set(prod, { attributes: new Set(["special"])} ); + assert.deepEqual(match.changeMap, expected); + }); + + it("query match one sibling group (deepEqual does not compare Map?)", () => { + let {map} = create(`list button:highlighted+button:disabled { color: red; }`); + let list, button, disabledButton; + + list = { + cssType: "list", + toString, + getChildIndex: () => 1, + getChildAt: () => button + }; + + button = { + cssType: "button", + cssPseudoClasses: new Set(["highlighted"]), + toString, + parent: list + }; + + disabledButton = { + cssType: "button", + cssPseudoClasses: new Set(["disabled"]), + toString, + parent: list + }; + + let match = map.query(disabledButton); + assert.equal(match.selectors.length, 1, "Expected match to have one selector."); + + let expected = new Map() + .set(disabledButton, { pseudoClasses: new Set(["disabled"]) }) + .set(button, { pseudoClasses: new Set(["highlighted"]) }); + + assert.deepEqual(match.changeMap, expected); + }); + }); + }); + }); +}); diff --git a/node-tests/test-angular-xml.ts b/unit-tests/xml/test-angular-xml.ts similarity index 95% rename from node-tests/test-angular-xml.ts rename to unit-tests/xml/test-angular-xml.ts index 1ab65689d..52c519fb6 100644 --- a/node-tests/test-angular-xml.ts +++ b/unit-tests/xml/test-angular-xml.ts @@ -1,6 +1,5 @@ import {assert} from "chai"; -//TODO: use a path mapping to the "xml" module after upgrading to TS 2.1 -var xml = require("../tns-core-modules/xml"); +const xml = require("tns-core-modules/xml"); describe("angular xml parser", () => { let last_element = null; diff --git a/node-tests/test-xml.ts b/unit-tests/xml/test-xml.ts similarity index 92% rename from node-tests/test-xml.ts rename to unit-tests/xml/test-xml.ts index 3f3f1de3e..202d20ca1 100644 --- a/node-tests/test-xml.ts +++ b/unit-tests/xml/test-xml.ts @@ -1,6 +1,5 @@ import {assert} from "chai"; -//TODO: use a path mapping to the "xml" module after upgrading to TS 2.1 -var xml = require("../tns-core-modules/xml"); +const xml = require("tns-core-modules/xml"); describe("xml parser", () => { let last_element = null;