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;