UNPKG

@nativescript/core

Version:

A JavaScript library providing an easy to use api for interacting with iOS and Android platform APIs.

1,269 lines 51.5 kB
import { getNativeScriptGlobals } from '../../globals/global-utils';
import { _evaluateCssVariableExpression, _evaluateCssCalcExpression, _expandCssShorthand, _isCssPendingSubstitution, _isCssValueStillApplied, isCssVariable, isCssVariableExpression, isCssCalcExpression } from '../core/properties';
import { unsetValue } from '../core/properties/property-shared';
import { StyleSheetSelectorScope, SelectorsMatch, fromAstNode, matchMediaQueryString, matchSelectorCandidates } from './css-selector';
import { Trace } from './styling-shared';
import { File, knownFolders, path } from '../../file-system';
import { Application } from '../../application';
import { profile } from './styling-profile';
import { KeyframeAnimationInfo, KeyframeAnimation } from '../animation/keyframe-animation';
import { CssAnimationParser } from './css-animation-parser';
import { sanitizeModuleName } from '../../utils/common';
import { resolveModuleName } from '../../module-name-resolver';
import { cssTreeParse } from '../../css/css-tree-parser';
import { CSS3Parser } from '../../css/CSS3Parser';
import { CSSNativeScript } from '../../css/CSSNativeScript';
import { parse as parseCss } from '../../css/lib/parse';
// @ts-ignore apps resolve this at runtime with path alias in project bundlers
import appConfig from '~/package.json';
let parser = 'css-tree';
try {
    if (appConfig) {
        if (appConfig.cssParser === 'rework') {
            parser = 'rework';
        }
        else if (appConfig.cssParser === 'nativescript') {
            parser = 'nativescript';
        }
    }
}
catch (e) {
    //
}
let mergedApplicationCssSelectors = [];
let applicationCssSelectors = [];
const applicationAdditionalSelectors = [];
let mergedApplicationCssSelectorsInvalid = false;
let mergedApplicationCssKeyframes = [];
let applicationCssKeyframes = [];
const applicationAdditionalKeyframes = [];
let mergedApplicationCssKeyframesInvalid = false;
let applicationCssSelectorVersion = 0;
/**
 * Shared index over the application stylesheets, built once for all style scopes.
 * Tagged rules stay in it and are filtered out at match time - see `matchSelectorCandidates`.
 */
let applicationSelectorScope = null;
let applicationSelectorScopeVersion = -1;
let applicationSelectorScopeRuleCount = 0;
let applicationSelectorsHaveScopedTags = false;
/** Bumped when the application rules change in a way an append cannot express. */
let applicationSelectorsResetVersion = 0;
let applicationSelectorScopeResetVersion = -1;
const tagToScopeTag = new Map();
let currentScopeTag = null;
const animationsSymbol = Symbol('animations');
const kebabCasePattern = /-([a-z])/g;
const kebabCaseReplacementFunc = (g) => g[1].toUpperCase();
const pattern = /('|")(.*?)\1/;
/**
 * Parse a pending-substitution shorthand once its expression has been evaluated
 * against the view; a value that does not survive evaluation leaves its longhands unset.
 */
function resolvePendingSubstitution(view, pending) {
    const value = evaluateCssExpressions(view, pending.shorthand, pending.value);
    if (value === unsetValue) {
        return CssState.emptyPropertyBag;
    }
    const expanded = _expandCssShorthand(pending.shorthand, value);
    if (!expanded) {
        Trace.write(`Failed to expand shorthand [${pending.shorthand}] resolved to [${value}] for ${view}.`, Trace.categories.Style, Trace.messageType.warn);
        return CssState.emptyPropertyBag;
    }
    const resolved = {};
    for (let i = 0, length = expanded.length; i < length; i++) {
        resolved[expanded[i][0]] = expanded[i][1];
    }
    return resolved;
}
/**
 * Evaluate css-variable and css-calc expressions
 */
function evaluateCssExpressions(view, property, value) {
    const newValue = _evaluateCssVariableExpression(view, property, value);
    if (newValue === 'unset') {
        return unsetValue;
    }
    value = newValue;
    try {
        value = _evaluateCssCalcExpression(value);
    }
    catch (e) {
        Trace.write(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
        return unsetValue;
    }
    return value;
}
/**
 * Only marks the merged list dirty - it is rebuilt on next read, since frameworks
 * register stylesheets one call at a time.
 */
export function mergeCssSelectors() {
    mergedApplicationCssSelectorsInvalid = true;
}
export function mergeCssKeyframes() {
    mergedApplicationCssKeyframesInvalid = true;
}
function getMergedApplicationCssSelectors() {
    if (mergedApplicationCssSelectorsInvalid) {
        mergedApplicationCssSelectorsInvalid = false;
        mergedApplicationCssSelectors = concatRuleSets(applicationCssSelectors, applicationAdditionalSelectors);
    }
    return mergedApplicationCssSelectors;
}
function getMergedApplicationCssKeyframes() {
    if (mergedApplicationCssKeyframesInvalid) {
        mergedApplicationCssKeyframesInvalid = false;
        mergedApplicationCssKeyframes = concatRuleSets(applicationCssKeyframes, applicationAdditionalKeyframes);
    }
    return mergedApplicationCssKeyframes;
}
function getApplicationSelectorScope() {
    if (applicationSelectorScopeVersion === applicationCssSelectorVersion) {
        return applicationSelectorScope;
    }
    const rulesets = getMergedApplicationCssSelectors();
    const canAppend = applicationSelectorScope && applicationSelectorScopeResetVersion === applicationSelectorsResetVersion && rulesets.length >= applicationSelectorScopeRuleCount;
    if (canAppend) {
        applicationSelectorScope.appendRulesets(rulesets, applicationSelectorScopeRuleCount);
    }
    else {
        applicationSelectorScope = rulesets.length > 0 ? new StyleSheetSelectorScope(rulesets, 0 /* SelectorTier.Application */) : null;
        applicationSelectorsHaveScopedTags = false;
        applicationSelectorScopeRuleCount = 0;
    }
    for (let i = applicationSelectorScopeRuleCount, length = rulesets.length; i < length; i++) {
        if (rulesets[i].scopedTag) {
            applicationSelectorsHaveScopedTags = true;
            break;
        }
    }
    applicationSelectorScopeRuleCount = rulesets.length;
    applicationSelectorScopeVersion = applicationCssSelectorVersion;
    applicationSelectorScopeResetVersion = applicationSelectorsResetVersion;
    return applicationSelectorScope;
}
/** Avoids `push(...arr)`, which blows the stack past the engine's argument limit. */
function concatRuleSets(base, additional) {
    const baseLength = base.length;
    const merged = new Array(baseLength + additional.length);
    for (let i = 0; i < baseLength; i++) {
        merged[i] = base[i];
    }
    for (let i = 0, length = additional.length; i < length; i++) {
        merged[baseLength + i] = additional[i];
    }
    return merged;
}
class CSSSource {
    constructor(_ast, _url, _file, _source) {
        this._ast = _ast;
        this._url = _url;
        this._file = _file;
        this._source = _source;
        this._selectors = [];
        this._keyframes = [];
        this.parse();
    }
    static fromDetect(cssOrAst, fileName) {
        if (typeof cssOrAst === 'string') {
            // raw-loader
            return CSSSource.fromSource(cssOrAst, fileName);
        }
        else if (typeof cssOrAst === 'object') {
            if (cssOrAst.default) {
                cssOrAst = cssOrAst.default;
            }
            if (cssOrAst.type === 'stylesheet' && cssOrAst.stylesheet && cssOrAst.stylesheet.rules) {
                // css-loader
                return CSSSource.fromAST(cssOrAst, fileName);
            }
        }
        // css2json-loader
        return CSSSource.fromSource(cssOrAst.toString(), fileName);
    }
    static fromURI(uri) {
        // webpack modules require all file paths to be relative to /app folder
        const appRelativeUri = CSSSource.pathRelativeToApp(uri);
        const sanitizedModuleName = sanitizeModuleName(appRelativeUri);
        const resolvedModuleName = resolveModuleName(sanitizedModuleName, 'css');
        try {
            const cssOrAst = global.loadModule(resolvedModuleName, true);
            if (cssOrAst) {
                return CSSSource.fromDetect(cssOrAst, resolvedModuleName);
            }
        }
        catch (e) {
            if (Trace.isEnabled()) {
                Trace.write(`Could not load CSS from ${uri}: ${e}`, Trace.categories.Error, Trace.messageType.warn);
            }
        }
        return CSSSource.fromFile(appRelativeUri);
    }
    static pathRelativeToApp(uri) {
        if (!uri.startsWith('/')) {
            return uri;
        }
        const appPath = knownFolders.currentApp().path;
        if (!uri.startsWith(appPath)) {
            Trace.write(`${uri} does not start with ${appPath}`, Trace.categories.Error, Trace.messageType.error);
            return uri;
        }
        const relativeUri = `.${uri.substring(appPath.length)}`;
        return relativeUri;
    }
    static fromFile(url) {
        // .scss, .sass, etc. css files in vanilla app are usually compiled to .css so we will try to load a compiled file first.
        const cssFileUrl = url.replace(/\..\w+$/, '.css');
        if (cssFileUrl !== url) {
            const cssFile = CSSSource.resolveCSSPathFromURL(cssFileUrl);
            if (cssFile) {
                return new CSSSource(undefined, url, cssFile, undefined);
            }
        }
        const file = CSSSource.resolveCSSPathFromURL(url);
        return new CSSSource(undefined, url, file, undefined);
    }
    static fromFileImport(url, importSource) {
        const file = CSSSource.resolveCSSPathFromURL(url, importSource);
        return new CSSSource(undefined, url, file, undefined);
    }
    static resolveCSSPathFromURL(url, importSource) {
        const app = knownFolders.currentApp().path;
        const file = resolveFileNameFromUrl(url, app, File.exists, importSource);
        return file;
    }
    static fromSource(source, url) {
        return new CSSSource(undefined, url, undefined, source);
    }
    static fromAST(ast, url) {
        return new CSSSource(ast, url, undefined, undefined);
    }
    get selectors() {
        return this._selectors;
    }
    get keyframes() {
        return this._keyframes;
    }
    get source() {
        return this._source;
    }
    load() {
        const file = File.fromPath(this._file);
        this._source = file.readTextSync();
    }
    parse() {
        try {
            if (!this._ast) {
                if (!this._source && this._file) {
                    this.load();
                }
                // [object Object] check guards against empty app.css file
                if (this._source && this.source !== '[object Object]') {
                    this.parseCSSAst();
                }
            }
            if (this._ast) {
                this.createSelectorsAndKeyframes();
            }
            else {
                this._selectors = [];
            }
        }
        catch (e) {
            if (Trace.isEnabled()) {
                Trace.write('Css styling failed: ' + e, Trace.categories.Style, Trace.messageType.error);
            }
            this._selectors = [];
        }
    }
    parseCSSAst() {
        if (this._source) {
            if (__CSS_PARSER__ === 'css-tree') {
                this._ast = cssTreeParse(this._source, this._file);
            }
            else if (__CSS_PARSER__ === 'nativescript') {
                const cssparser = new CSS3Parser(this._source);
                const stylesheet = cssparser.parseAStylesheet();
                const cssNS = new CSSNativeScript();
                this._ast = cssNS.parseStylesheet(stylesheet);
            }
            else if (__CSS_PARSER__ === 'rework') {
                this._ast = parseCss(this._source, { source: this._file });
            }
        }
    }
    createSelectorsAndKeyframes() {
        if (this._ast) {
            const nodes = this._ast.stylesheet.rules;
            const rulesets = [];
            const keyframes = [];
            // When css2json-loader is enabled, imports are handled there and removed from AST rules
            populateRulesFromImports(nodes, rulesets, keyframes);
            _populateRules(nodes, rulesets, keyframes);
            if (rulesets && rulesets.length) {
                rulesets.forEach((rule) => {
                    rule[animationsSymbol] = CssAnimationParser.keyframeAnimationsFromCSSDeclarations(rule.declarations);
                });
            }
            this._selectors = rulesets;
            this._keyframes = keyframes;
        }
    }
    toString() {
        return this._file || this._url || '(in-memory)';
    }
}
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CSSSource.prototype, "load", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CSSSource.prototype, "parse", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CSSSource.prototype, "parseCSSAst", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CSSSource.prototype, "createSelectorsAndKeyframes", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, String]),
    __metadata("design:returntype", String)
], CSSSource, "resolveCSSPathFromURL", null);
function populateRulesFromImports(nodes, rulesets, keyframes) {
    const imports = nodes.filter((r) => r.type === 'import');
    if (!imports.length) {
        return;
    }
    const urlFromImportObject = (importObject) => {
        const importItem = importObject['import'];
        const urlMatch = importItem && importItem.match(pattern);
        return urlMatch && urlMatch[2];
    };
    const sourceFromImportObject = (importObject) => importObject['position'] && importObject['position']['source'];
    const toUrlSourcePair = (importObject) => ({
        url: urlFromImportObject(importObject),
        source: sourceFromImportObject(importObject),
    });
    const getCssFile = ({ url, source }) => (source ? CSSSource.fromFileImport(url, source) : CSSSource.fromURI(url));
    const cssFiles = imports
        .map(toUrlSourcePair)
        .filter(({ url }) => !!url)
        .map(getCssFile);
    for (const cssFile of cssFiles) {
        if (cssFile) {
            rulesets.push(...cssFile.selectors);
            keyframes.push(...cssFile.keyframes);
        }
    }
}
export function _populateRules(nodes, rulesets, keyframes, mediaQueryString) {
    for (const node of nodes) {
        if (isKeyframe(node)) {
            const keyframeRule = {
                name: node.name,
                keyframes: node.keyframes,
                mediaQueryString: mediaQueryString,
            };
            keyframes.push(keyframeRule);
        }
        else if (isMedia(node)) {
            // Media query can be an array of strings in case of nested queries
            let compositeMediaQuery;
            if (mediaQueryString) {
                if (typeof mediaQueryString === 'string') {
                    compositeMediaQuery = [mediaQueryString, node.media];
                }
                else {
                    mediaQueryString.push(node.media);
                    compositeMediaQuery = mediaQueryString;
                }
            }
            else {
                compositeMediaQuery = node.media;
            }
            _populateRules(node.rules, rulesets, keyframes, compositeMediaQuery);
        }
        else if (isRule(node)) {
            const ruleset = fromAstNode(node);
            ruleset.mediaQueryString = mediaQueryString;
            rulesets.push(ruleset);
        }
    }
}
export function removeTaggedAdditionalCSS(tag) {
    let selectorsChanged = false;
    let keyframesChanged = false;
    let updated = false;
    for (let i = 0; i < applicationAdditionalSelectors.length; i++) {
        if (applicationAdditionalSelectors[i].tag === tag) {
            applicationAdditionalSelectors.splice(i, 1);
            i--;
            selectorsChanged = true;
        }
    }
    for (let i = 0; i < applicationAdditionalKeyframes.length; i++) {
        if (applicationAdditionalKeyframes[i].tag === tag) {
            applicationAdditionalKeyframes.splice(i, 1);
            i--;
            keyframesChanged = true;
        }
    }
    if (selectorsChanged) {
        // Rules were dropped from the middle of the list, so the index has to be rebuilt.
        applicationSelectorsResetVersion++;
        mergeCssSelectors();
        updated = true;
    }
    if (keyframesChanged) {
        mergeCssKeyframes();
        updated = true;
    }
    if (updated) {
        applicationCssSelectorVersion++;
    }
    return updated;
}
export function addTaggedAdditionalCSS(cssText, tag) {
    const { selectors, keyframes } = CSSSource.fromDetect(cssText, undefined);
    const tagScope = currentScopeTag || (tag && tagToScopeTag.has(tag) && tagToScopeTag.get(tag)) || null;
    if (tagScope && tag) {
        tagToScopeTag.set(tag, tagScope);
    }
    let selectorsChanged = false;
    let keyframesChanged = false;
    let updated = false;
    if (selectors && selectors.length) {
        selectorsChanged = true;
        if (tag != null || tagScope != null) {
            for (let i = 0, length = selectors.length; i < length; i++) {
                selectors[i].tag = tag;
                selectors[i].scopedTag = tagScope;
            }
        }
        applicationAdditionalSelectors.push(...selectors);
        mergeCssSelectors();
        updated = true;
    }
    if (keyframes && keyframes.length) {
        keyframesChanged = true;
        if (tag != null || tagScope != null) {
            for (let i = 0, length = keyframes.length; i < length; i++) {
                keyframes[i].tag = tag;
                keyframes[i].scopedTag = tagScope;
            }
        }
        applicationAdditionalKeyframes.push(...keyframes);
        mergeCssKeyframes();
        updated = true;
    }
    if (updated) {
        applicationCssSelectorVersion++;
    }
    return updated;
}
const onCssChanged = profile('"style-scope".onCssChanged', (args) => {
    if (args.cssText) {
        const { selectors, keyframes } = CSSSource.fromSource(args.cssText, args.cssFile);
        let updated = false;
        if (selectors) {
            applicationAdditionalSelectors.push(...selectors);
            mergeCssSelectors();
            updated = true;
        }
        if (keyframes) {
            applicationAdditionalKeyframes.push(...keyframes);
            mergeCssKeyframes();
            updated = true;
        }
        if (updated) {
            applicationCssSelectorVersion++;
        }
    }
    else if (args.cssFile) {
        loadCss(args.cssFile, null, null);
    }
});
function onLiveSync(args) {
    loadCss(Application.getCssFileName(), null, null);
}
const loadCss = profile(`"style-scope".loadCss`, (cssModule) => {
    if (!cssModule) {
        return;
    }
    // safely remove "./" as global CSS should be resolved relative to app folder
    if (cssModule.startsWith('./')) {
        cssModule = cssModule.substring(2);
    }
    const { selectors, keyframes } = CSSSource.fromURI(cssModule);
    let updated = false;
    // Check for existing application css selectors too in case the app is undergoing a live-sync
    if (selectors.length > 0 || applicationCssSelectors.length > 0) {
        // The base stylesheet is replaced rather than appended to.
        applicationSelectorsResetVersion++;
        applicationCssSelectors = selectors;
        mergeCssSelectors();
        updated = true;
    }
    // Check for existing application css keyframes too in case the app is undergoing a live-sync
    if (keyframes.length > 0 || applicationCssKeyframes.length > 0) {
        applicationCssKeyframes = keyframes;
        mergeCssKeyframes();
        updated = true;
    }
    if (updated) {
        applicationCssSelectorVersion++;
    }
});
getNativeScriptGlobals().events.on('cssChanged', onCssChanged);
getNativeScriptGlobals().events.on('livesync', onLiveSync);
// Call to this method is injected in the application in:
//  - no-snapshot - code injected in app.ts by [bundle-config-loader](https://github.com/NativeScript/nativescript-dev-webpack/blob/9b1e34d8ef838006c9b575285c42d2304f5f02b5/bundle-config-loader.ts#L85-L92)
//  - with-snapshot - code injected in snapshot bundle by [NativeScriptSnapshotPlugin](https://github.com/NativeScript/nativescript-dev-webpack/blob/48b26f412fd70c19dc0b9c7763e08e9505a0ae11/plugins/NativeScriptSnapshotPlugin/index.js#L48-L56)
// Having the app.css loaded in snapshot provides significant boost in startup (when using the ns-theme ~150 ms). However, because app.css is resolved at build-time,
// when the snapshot is created - there is no way to use file qualifiers or change the name of on app.css
export const loadAppCSS = profile('"style-scope".loadAppCSS', (args) => {
    loadCss(args.cssFile, null, null);
    getNativeScriptGlobals().events.off('loadAppCss', loadAppCSS);
});
if (Application.hasLaunched()) {
    loadAppCSS({
        eventName: 'loadAppCss',
        object: Application,
        cssFile: Application.getCssFileName(),
    }, null, null);
}
else {
    getNativeScriptGlobals().events.on('loadAppCss', loadAppCSS);
}
function trackedNamesEqual(a, b) {
    if (a === b) {
        return true;
    }
    if (!a || !b || a.size !== b.size) {
        return false;
    }
    for (const name of a) {
        if (!b.has(name)) {
            return false;
        }
    }
    return true;
}
/** Two empty maps compare equal even when they are different objects. */
function changeMapsEqual(applied, current) {
    if (applied === current) {
        return true;
    }
    if (applied.size !== current.size) {
        return false;
    }
    for (const [view, changes] of applied) {
        const currentChanges = current.get(view);
        if (!currentChanges || !trackedNamesEqual(changes.attributes, currentChanges.attributes) || !trackedNamesEqual(changes.pseudoClasses, currentChanges.pseudoClasses)) {
            return false;
        }
    }
    return true;
}
export class CssState {
    constructor(viewRef) {
        this.viewRef = viewRef;
        this._appliedPropertyValues = CssState.emptyPropertyBag;
        this._appliedLocalValueVersion = -1;
        this._onDynamicStateChangeHandler = () => this.updateDynamicState();
    }
    /**
     * Called when a change had occurred that may invalidate the statically matching selectors (class, id, ancestor selectors).
     * As a result, at some point in time, the selectors matched have to be requerried from the style scope and applied to the view.
     */
    onChange() {
        const view = this.viewRef.get();
        if (view && view.isLoaded) {
            // Matching does not read the subscriptions, so re-subscribe only when the
            // dependencies actually changed - they are usually identical.
            this.updateMatch();
            if (!changeMapsEqual(this._appliedChangeMap, this._match.changeMap)) {
                this.unsubscribeFromDynamicUpdates();
                this.subscribeForDynamicUpdates();
            }
            this.updateDynamicState();
        }
        else {
            this._matchInvalid = true;
        }
    }
    /**
     * Checks whether style scope and CSS state selectors are in sync.
     */
    isSelectorsLatestVersionApplied() {
        const view = this.viewRef.get();
        if (!view) {
            Trace.write(`isSelectorsLatestVersionApplied returns default value "false" because "this.viewRef" cleared.`, Trace.categories.Style, Trace.messageType.warn);
            return false;
        }
        return this.viewRef.get()._styleScope.getSelectorsVersion() === this._appliedSelectorsVersion;
    }
    onLoaded() {
        if (this._matchInvalid) {
            this.updateMatch();
        }
        this.subscribeForDynamicUpdates();
        this.updateDynamicState();
    }
    onUnloaded() {
        this.unsubscribeFromDynamicUpdates();
        this.stopKeyframeAnimations();
    }
    updateMatch() {
        const view = this.viewRef.get();
        if (view && view._styleScope) {
            this._match = view._styleScope.matchSelectors(view) ?? CssState.emptyMatch;
            this._appliedSelectorsVersion = view._styleScope.getSelectorsVersion();
        }
        else {
            this._match = CssState.emptyMatch;
        }
        this._matchInvalid = false;
    }
    updateDynamicState() {
        const view = this.viewRef.get();
        if (!view) {
            Trace.write(`updateDynamicState not executed to view because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
            return;
        }
        const selectors = this._match.selectors;
        const matchingSelectors = [];
        for (let i = 0, length = selectors.length; i < length; i++) {
            const sel = selectors[i];
            if (!sel.dynamic || sel.match(view)) {
                matchingSelectors.push(sel);
            }
        }
        // Ideally we should return here if there are no matching selectors, however
        // if there are property removals, returning here would not remove them
        // this is seen in STYLE test in automated.
        // if (!matchingSelectors || matchingSelectors.length === 0) {
        // 		return;
        // }
        view._batchUpdate(() => {
            this.stopKeyframeAnimations();
            this.setPropertyValues(matchingSelectors);
            this.playKeyframeAnimations(matchingSelectors);
        });
    }
    playKeyframeAnimations(matchingSelectors) {
        const animations = [];
        matchingSelectors.forEach((selector) => {
            const ruleAnimations = selector.ruleset?.[animationsSymbol];
            if (ruleAnimations) {
                for (const animationInfo of ruleAnimations) {
                    const animation = KeyframeAnimation.keyframeAnimationFromInfo(animationInfo);
                    if (animation) {
                        animations.push(animation);
                    }
                }
            }
        });
        if ((this._playsKeyframeAnimations = animations.length > 0)) {
            const view = this.viewRef.get();
            if (!view) {
                Trace.write(`KeyframeAnimations cannot play because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
                return;
            }
            animations.map((animation) => animation.play(view));
            Object.freeze(animations);
            this._appliedAnimations = animations;
        }
    }
    stopKeyframeAnimations() {
        if (!this._playsKeyframeAnimations) {
            return;
        }
        this._appliedAnimations.filter((animation) => animation.isPlaying).forEach((animation) => animation.cancel());
        this._appliedAnimations = CssState.emptyAnimationArray;
        const view = this.viewRef.get();
        if (view) {
            view.style['keyframe:rotate'] = unsetValue;
            view.style['keyframe:rotateX'] = unsetValue;
            view.style['keyframe:rotateY'] = unsetValue;
            view.style['keyframe:scaleX'] = unsetValue;
            view.style['keyframe:scaleY'] = unsetValue;
            view.style['keyframe:translateX'] = unsetValue;
            view.style['keyframe:translateY'] = unsetValue;
            view.style['keyframe:backgroundColor'] = unsetValue;
            view.style['keyframe:opacity'] = unsetValue;
        }
        else {
            Trace.write(`KeyframeAnimations cannot be stopped because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
        }
        this._playsKeyframeAnimations = false;
    }
    /**
     * Calculate the difference between the previously applied property values,
     * and the new set of property values that have to be applied for the provided selectors.
     * Apply the values and ensure each property setter is called at most once to avoid excessive change notifications.
     * @param matchingSelectors
     */
    setPropertyValues(matchingSelectors) {
        const view = this.viewRef.get();
        if (!view) {
            Trace.write(`${matchingSelectors} not set to view's property because ".viewRef" is cleared`, Trace.categories.Style, Trace.messageType.warn);
            return;
        }
        const newPropertyValues = new view.style.PropertyBag();
        matchingSelectors.forEach((selector) => selector.ruleset.declarations.forEach((declaration) => (newPropertyValues[declaration.property] = declaration.value)));
        const oldProperties = this._appliedPropertyValues;
        // A local write is the only thing that can drop a value the cascade already
        // applied, so the recorded values only have to be verified after one.
        const localValueVersion = view.style._localValueVersion;
        const verifyApplied = localValueVersion !== this._appliedLocalValueVersion;
        // Update values for the scope's css-variables
        view.style.resetScopedCssVariables();
        let valuesToApply;
        let cssExpsProperties;
        let pendingProperties;
        for (const property in newPropertyValues) {
            const value = newPropertyValues[property];
            if (_isCssPendingSubstitution(value)) {
                // Resolvable only after the css variables below are up to date.
                if (!pendingProperties) {
                    pendingProperties = {};
                }
                pendingProperties[property] = value;
                continue;
            }
            // Expanded shorthand values are already converted and may not be strings.
            const isCssExp = typeof value === 'string' && (isCssVariableExpression(value) || isCssCalcExpression(value));
            if (isCssExp) {
                // we handle css exp separately because css vars must be evaluated first
                if (!cssExpsProperties) {
                    cssExpsProperties = {};
                }
                cssExpsProperties[property] = value;
                continue;
            }
            // Whatever is left in oldProperties after these loops was removed and gets unset.
            const hadOldValue = property in oldProperties;
            const unchanged = hadOldValue && oldProperties[property] === value && (!verifyApplied || _isCssValueStillApplied(view.style, property, value));
            if (hadOldValue) {
                delete oldProperties[property];
            }
            if (isCssVariable(property)) {
                // The scoped css-variables were just reset, so they always have to be re-registered.
                view.style.setScopedCssVariable(property, value);
                delete newPropertyValues[property];
                continue;
            }
            if (unchanged) {
                continue;
            }
            if (!valuesToApply) {
                valuesToApply = {};
            }
            valuesToApply[property] = value;
        }
        //we need to parse CSS vars first before evaluating css expressions
        for (const property in cssExpsProperties) {
            const hadOldValue = property in oldProperties;
            const oldValue = hadOldValue ? oldProperties[property] : undefined;
            if (hadOldValue) {
                delete oldProperties[property];
            }
            const value = evaluateCssExpressions(view, property, cssExpsProperties[property]);
            if (isCssVariable(property)) {
                view.style.setScopedCssVariable(property, value);
                delete newPropertyValues[property];
                continue;
            }
            if (value === unsetValue) {
                delete newPropertyValues[property];
            }
            else {
                // Record the evaluated value - the next diff compares against it.
                newPropertyValues[property] = value;
            }
            if (hadOldValue && oldValue === value && (!verifyApplied || _isCssValueStillApplied(view.style, property, value))) {
                continue;
            }
            if (!valuesToApply) {
                valuesToApply = {};
            }
            valuesToApply[property] = value;
        }
        // Each shorthand is resolved once, however many longhands point at it.
        let resolvedShorthands;
        for (const property in pendingProperties) {
            const pending = pendingProperties[property];
            if (!resolvedShorthands) {
                resolvedShorthands = new Map();
            }
            let resolved = resolvedShorthands.get(pending);
            if (!resolved) {
                resolved = resolvePendingSubstitution(view, pending);
                resolvedShorthands.set(pending, resolved);
            }
            const value = property in resolved ? resolved[property] : unsetValue;
            const hadOldValue = property in oldProperties;
            const oldValue = hadOldValue ? oldProperties[property] : undefined;
            if (hadOldValue) {
                delete oldProperties[property];
            }
            if (value === unsetValue) {
                delete newPropertyValues[property];
            }
            else {
                newPropertyValues[property] = value;
            }
            if (hadOldValue && oldValue === value && (!verifyApplied || _isCssValueStillApplied(view.style, property, value))) {
                continue;
            }
            if (!valuesToApply) {
                valuesToApply = {};
            }
            valuesToApply[property] = value;
        }
        // Unset removed values - the bag is keyed by longhands only, so unsetting
        // one entry cannot clear a value another one set.
        for (const property in oldProperties) {
            if (property in view.style) {
                view.style[`css:${property}`] = unsetValue;
            }
            else {
                const camelCasedProperty = property.replace(kebabCasePattern, kebabCaseReplacementFunc);
                view[camelCasedProperty] = unsetValue;
            }
        }
        // Set new values to the style
        for (const property in valuesToApply) {
            const value = valuesToApply[property];
            try {
                if (property in view.style) {
                    view.style[`css:${property}`] = value;
                }
                else {
                    const camelCasedProperty = property.replace(kebabCasePattern, kebabCaseReplacementFunc);
                    view[camelCasedProperty] = value;
                }
            }
            catch (e) {
                Trace.write(`Failed to apply property [${property}] with value [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
            }
        }
        this._appliedPropertyValues = newPropertyValues;
        this._appliedLocalValueVersion = view.style._localValueVersion;
    }
    subscribeForDynamicUpdates() {
        const changeMap = this._match.changeMap;
        changeMap.forEach((changes, view) => {
            if (changes.attributes) {
                changes.attributes.forEach((attribute) => {
                    view.addEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
                });
            }
            if (changes.pseudoClasses) {
                changes.pseudoClasses.forEach((pseudoClass) => {
                    const eventName = ':' + pseudoClass;
                    view.addEventListener(':' + pseudoClass, this._onDynamicStateChangeHandler);
                    if (view[eventName]) {
                        view[eventName](true);
                    }
                });
            }
        });
        this._appliedChangeMap = changeMap;
    }
    unsubscribeFromDynamicUpdates() {
        this._appliedChangeMap.forEach((changes, view) => {
            if (changes.attributes) {
                changes.attributes.forEach((attribute) => {
                    view.removeEventListener(attribute + 'Change', this._onDynamicStateChangeHandler);
                });
            }
            if (changes.pseudoClasses) {
                changes.pseudoClasses.forEach((pseudoClass) => {
                    const eventName = ':' + pseudoClass;
                    view.removeEventListener(eventName, this._onDynamicStateChangeHandler);
                    if (view[eventName]) {
                        view[eventName](false);
                    }
                });
            }
        });
        this._appliedChangeMap = CssState.emptyChangeMap;
    }
    toString() {
        const view = this.viewRef.get();
        if (!view) {
            Trace.write(`toString() of CssState cannot execute correctly because ".viewRef" is cleared`, Trace.categories.Animation, Trace.messageType.warn);
            return '';
        }
        return `${view}._cssState`;
    }
}
CssState.emptyChangeMap = Object.freeze(new Map());
CssState.emptyPropertyBag = {};
CssState.emptyAnimationArray = Object.freeze([]);
CssState.emptyMatch = {
    selectors: [],
    changeMap: new Map(),
    addAttribute: () => { },
    addPseudoClass: () => { },
    properties: null,
};
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CssState.prototype, "updateMatch", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], CssState.prototype, "updateDynamicState", null);
CssState.prototype._appliedChangeMap = CssState.emptyChangeMap;
CssState.prototype._appliedAnimations = CssState.emptyAnimationArray;
CssState.prototype._matchInvalid = true;
export class StyleScope {
    constructor() {
        this._css = '';
        this._hasSelectors = false;
        this._localCssSelectors = [];
        this._localCssKeyframes = [];
        this._localCssSelectorVersion = 0;
        this._localCssSelectorsAppliedVersion = 0;
        this._applicationCssSelectorsAppliedVersion = 0;
        this._cssFiles = new Set();
    }
    get css() {
        return this._css;
    }
    set css(value) {
        this.setCss(value);
    }
    addCss(cssString, cssFileName) {
        this.appendCss(cssString, cssFileName);
    }
    addCssFile(cssFileName) {
        this.appendCss(null, cssFileName);
    }
    changeCssFile(cssFileName) {
        if (!cssFileName) {
            return;
        }
        this._cssFiles.add(cssFileName);
        currentScopeTag = cssFileName;
        const cssFile = CSSSource.fromURI(cssFileName);
        currentScopeTag = null;
        this._css = cssFile.source;
        this._localCssSelectors = cssFile.selectors;
        this._localCssKeyframes = cssFile.keyframes;
        this._localCssSelectorVersion++;
        this.ensureSelectors();
    }
    setCss(cssString, cssFileName) {
        this._css = cssString;
        const cssFile = CSSSource.fromSource(cssString, cssFileName);
        this._localCssSelectors = cssFile.selectors;
        this._localCssKeyframes = cssFile.keyframes;
        this._localCssSelectorVersion++;
        this.ensureSelectors();
    }
    appendCss(cssString, cssFileName) {
        if (!cssString && !cssFileName) {
            return;
        }
        if (cssFileName) {
            this._cssFiles.add(cssFileName);
            currentScopeTag = cssFileName;
        }
        const cssFile = cssString ? CSSSource.fromSource(cssString, cssFileName) : CSSSource.fromURI(cssFileName);
        currentScopeTag = null;
        this._css = this._css + cssFile.source;
        this._localCssSelectors.push(...cssFile.selectors);
        this._localCssKeyframes.push(...cssFile.keyframes);
        this._localCssSelectorVersion++;
        this.ensureSelectors();
    }
    getKeyframeAnimationWithName(animationName) {
        if (!this._mergedCssKeyframes) {
            return null;
        }
        const keyframeRule = this.findKeyframeRule(animationName);
        const animation = new KeyframeAnimationInfo();
        animation.keyframes = keyframeRule ? CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
        return animation;
    }
    ensureSelectors() {
        if (!this.isApplicationCssSelectorsLatestVersionApplied() || !this.isLocalCssSelectorsLatestVersionApplied()) {
            this._createSelectors();
        }
        return this.getSelectorsVersion();
    }
    /**
     * True when any selector in the scope contains an adjacent sibling ('+') combinator.
     */
    get hasAdjacentCombinatorSelectors() {
        this.ensureSelectors();
        return !!applicationSelectorScope?.hasAdjacentCombinatorSelectors || !!this._localSelectorScope?.hasAdjacentCombinatorSelectors;
    }
    /**
     * True when any selector in the scope contains a general sibling ('~') combinator.
     */
    get hasSiblingCombinatorSelectors() {
        this.ensureSelectors();
        return !!applicationSelectorScope?.hasSiblingCombinatorSelectors || !!this._localSelectorScope?.hasSiblingCombinatorSelectors;
    }
    /**
     * Increase the application CSS selector version.
     */
    _increaseApplicationCssSelectorVersion() {
        applicationCssSelectorVersion++;
    }
    isApplicationCssSelectorsLatestVersionApplied() {
        return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion;
    }
    isLocalCssSelectorsLatestVersionApplied() {
        return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion;
    }
    _createSelectors() {
        const cssFiles = this._cssFiles;
        const applicationScope = getApplicationSelectorScope();
        this._applicationCssSelectorsAppliedVersion = applicationCssSelectorVersion;
        if (!this.isLocalCssSelectorsLatestVersionApplied() || (!this._localSelectorScope && this._localCssSelectors.length > 0)) {
            this._localSelectorScope = this._localCssSelectors.length > 0 ? new StyleSheetSelectorScope(this._localCssSelectors, 1 /* SelectorTier.Local */) : null;
        }
        this._localCssSelectorsAppliedVersion = this._localCssSelectorVersion;
        this._hasSelectors = !!applicationScope || !!this._localSelectorScope;
        const toMergeKeyframes = [];
        const applicationKeyframes = getMergedApplicationCssKeyframes();
        for (let i = 0, length = applicationKeyframes.length; i < length; i++) {
            const keyframe = applicationKeyframes[i];
            if (!keyframe.scopedTag || cssFiles.has(keyframe.scopedTag)) {
                toMergeKeyframes.push(keyframe);
            }
        }
        const localKeyframes = this._localCssKeyframes;
        for (let i = 0, length = localKeyframes.length; i < length; i++) {
            toMergeKeyframes.push(localKeyframes[i]);
        }
        this._mergedCssKeyframes = toMergeKeyframes.length > 0 ? toMergeKeyframes : null;
    }
    // HACK: This @profile decorator creates a circular dependency
    // HACK: because the function parameter type is evaluated with 'typeof'
    matchSelectors(view) {
        // should be (view: ViewBase): SelectorsMatch<ViewBase>
        this.ensureSelectors();
        if (!this._hasSelectors) {
            return null;
        }
        // The cascade has to see the application and local candidates as a single ordered set.
        const candidates = [];
        applicationSelectorScope?.collectCandidates(view, candidates);
        this._localSelectorScope?.collectCandidates(view, candidates);
        const match = matchSelectorCandidates(view, candidates, applicationSelectorsHaveScopedTags ? this._cssFiles : undefined);
        // Make sure to re-apply keyframes to matching selectors as a media query keyframe might be applicable at this point
        this._applyKeyframesToSelectors(match.selectors);
        return match;
    }
    query(node) {
        this.ensureSelectors();
        const match = this.matchSelectors(node);
        return match ? match.selectors : [];
    }
    getSelectorsVersion() {
        // The counters can only go up. So we can return just appVersion + localVersion
        // The 100000 * appVersion is just for easier debugging
        return 100000 * this._applicationCssSelectorsAppliedVersion + this._localCssSelectorsAppliedVersion;
    }
    _applyKeyframesToSelectors(selectors) {
        if (!selectors?.length) {
            return;
        }
        for (let i = selectors.length - 1; i >= 0; i--) {
            const ruleset = selectors[i].ruleset;
            const animations = ruleset[animationsSymbol];
            if (animations != null && animations.length) {
                for (const animation of animations) {
                    const keyframeRule = this.findKeyframeRule(animation.name);
                    animation.keyframes = keyframeRule ? CssAnimationParser.keyframesArrayFromCSS(keyframeRule.keyframes) : null;
                }
            }
        }
    }
    getAnimations(ruleset) {
        return ruleset[animationsSymbol];
    }
    findKeyframeRule(animationName) {
        if (!this._mergedCssKeyframes) {
            return null;
        }
        // Cache media query results to avoid validations of other identical queries
        let validatedMediaQueries;
        // Iterate in reverse order as the last usable keyframe rule matters the most
        for (let i = this._mergedCssKeyframes.length - 1; i >= 0; i--) {
            const rule = this._mergedCssKeyframes[i];
            if (rule.name !== animationName) {
                continue;
            }
            if (!rule.mediaQueryString) {
                return rule;
            }
            if (!validatedMediaQueries) {
                validatedMediaQueries = [];
            }
            const isMatchingAllQueries = matchMediaQueryString(rule.mediaQueryString, validatedMediaQueries);
            if (isMatchingAllQueries) {
                return rule;
            }
        }
        return null;
    }
}
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, Object]),
    __metadata("design:returntype", void 0)
], StyleScope.prototype, "setCss", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [String, Object]),
    __metadata("design:returntype", void 0)
], StyleScope.prototype, "appendCss", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", void 0)
], StyleScope.prototype, "_createSelectors", null);
__decorate([
    profile,
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", SelectorsMatch)
], StyleScope.prototype, "matchSelectors", null);
export function resolveFileNameFromUrl(url, appDirectory, fileExists, importSource) {
    let fileName = typeof url === 'string' ? url.trim() : '';
    if (fileName.indexOf('~/') === 0) {
        fileName = fileName.replace('~/', '');
    }
    const isAbsolutePath = fileName.indexOf('/') === 0;
    const absolutePath = isAbsolutePath ? fileName : path.join(appDirectory, fileName);
    if (fileExists(absolutePath)) {
        return absolutePath;
    }
    if (!isAbsolutePath) {
        if (fileName[0] === '~' && fileName[1] !== '/' && fileName[1] !== '"') {
            fileName = fileName.substring(1);
        }
        if (importSource) {
            const importFile = resolveFilePathFromImport(importSource, fileName);
            if (fileExists(importFile)) {
                return importFile;
            }
        }
        const external = path.join(appDirectory, 'tns_modules', fileName);
        if (fileExists(external)) {
            return external;
        }
    }
    return null;
}
function resolveFilePathFromImport(importSource, fileName) {
    const importSourceParts = importSource.split(path.separator);
    const fileNameParts = fileName
        .split(path.separator)
        // exclude the dot-segment for current directory
        .filter((p) => !isCurrentDirectory(p));
    // remove current file name
    importSourceParts.pop();
    // remove element in case of dot-segment for parent directory or add file name
    fileNameParts.forEach((p) => (isParentDirectory(p) ? importSourceParts.pop() : importSourceParts.push(p)));
    return importSourceParts.join(path.separator);
}
export const applyInlineStyle = profile('applyInlineStyle', function applyInlineStyle(view, styleStr) {
    const localStyle = `local { ${styleStr} }`;
    const inlineRuleSet = CSSSource.fromSource(localStyle).selectors;
    // Reset unscoped css-variables
    view.style.resetUnscopedCssVariables();
    // Set all the css-variables first, so we can be sure they are up-to-date
    inlineRuleSet[0].declarations.forEach((d) => {
        // Use the actual property name so that a local value is set.
        const property = d.property;
        if (isCssVariable(property)) {
            view.style.setUnscopedCssVariable(property, d.value);
        }
    });
    // Pending-substitution longhands share one placeholder - resolve it once.
    let resolvedShorthands;
    inlineRuleSet[0].declarations.forEach((d) => {
        // Use the actual property name so that a local value is set.
        const property = d.property;
        try {
            if (isCssVariable(property)) {
                // Skip css-variables, they have been handled
                return;
            }
            let value;
            if (_isCssPendingSubstitution(d.value)) {
                if (!resolvedShorthands) {
                    resolvedShorthands = new Map();
                }
                let resolved = resolvedShorthands.get(d.value);
                if (!resolved) {
                    resolved = resolvePendingSubstitution(view, d.value);
                    resolvedShorthands.set(d.value, resolved);
                }
                value = property in resolved ? resolved[property] : unsetValue;
            }
            else {
                value = evaluateCssExpressions(view, property, d.value);
            }
            if (property in view.style) {
                view.style[property] = value;
            }
            else {
                view[property] = value;
            }
        }
        catch (e) {
            Trace.write(`Failed to apply property [${d.property}] with value [${d.value}] to ${view}. ${e}`, Trace.categories.Error, Trace.messageType.error);
        }
    });
    // This is needed in case of changes to css-variable or css-calc expressions.
    view._onCssStateChange();
});
function isCurrentDirectory(uriPart) {
    return uriPart === '.';
}
function isParentDirectory(uriPart) {
    return uriPart === '..';
}
function isMedia(node) {
    return node.type === 'media';
}
function isKeyframe(node) {
    return node.type === 'keyframes';
}
function isRule(node) {
    return node.type === 'rule';
}
//# sourceMappingURL=style-scope.js.map