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;