UNPKG

@adpt/core

Version:
451 lines 14.4 kB
"use strict"; /* * Copyright 2018-2019 Unbounded Systems, LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const util = tslib_1.__importStar(require("util")); const cssWhat = require("css-what"); const ld = tslib_1.__importStar(require("lodash")); const error_1 = require("./error"); const jsx_1 = require("./jsx"); /** @internal */ exports.$matchInfoReg = Symbol.for("$matchInfoReg"); function isStyleBuildInfoInternal(val) { return val[exports.$matchInfoReg] != null; } const matchConfig = { attribute: matchAttribute, child: matchChild, descendant: () => { throw new error_1.InternalError("should not get here"); }, pseudo: matchPseudo, tag: matchTag }; function last(arr) { if (arr.length <= 0) { return { prefix: [], elem: null }; } const lastElem = arr[arr.length - 1]; return { prefix: arr.slice(0, -1), elem: lastElem }; } function getPropValue(elem, prop, ignoreCase) { let val; if (ignoreCase) { prop = prop.toLowerCase(); for (const key of Object.keys(elem.props)) { if (key.toLowerCase() === prop) val = elem.props[key]; } } else { val = elem.props[prop]; } return typeof val === "string" ? val : undefined; } function fragToString(frag) { //FIXME(manishv) Actually convert back to CSS syntax return util.inspect(frag); } /* * Pseudo-classes */ const matchPseudoConfig = { root: matchRoot, not: matchNot, }; function matchPseudo(frag, path) { if (frag.type !== "pseudo") throw new error_1.InternalError(util.inspect(frag)); const matcher = matchPseudoConfig[frag.name]; if (matcher == null) throw new Error(`Unsupported CSS pseudo-class :${frag.name}`); return matcher(frag, path); } function matchNot(frag, path) { if (frag.type !== "pseudo") throw new error_1.InternalError(util.inspect(frag)); if (frag.data == null || frag.data.length === 0 || frag.data[0].length === 0) { throw new Error(`CSS ":not" requires at least one selector argument in parentheses`); } return { newPath: path, matched: !matchWithSelector(frag.data, path) }; } function matchRoot(frag, path) { // NOTE(mark): This implementation for matchRoot depends on path always // starting with the actual root element, so therefore path.length is // the actual depth of the last element in path. matchChild and // matchDecendant both modify path, but always remove from the END of // the path. return { newPath: path, matched: path.length === 1 }; } /* * Basic selectors */ function matchAttribute(frag, path) { if (frag.type !== "attribute") throw new error_1.InternalError(util.inspect(frag)); const { elem } = last(path); if (elem == null) throw new error_1.InternalError("null element"); const value = getPropValue(elem, frag.name, frag.ignoreCase); if (value === undefined) return { newPath: path, matched: false }; let matched; switch (frag.action) { case "exists": matched = true; break; case "equals": matched = value === frag.value; break; case "start": matched = value.startsWith(frag.value); break; case "any": matched = value.includes(frag.value); break; case "end": matched = value.endsWith(frag.value); break; case "element": matched = value.split(/\s+/).indexOf(frag.value) !== -1; break; default: throw new Error(`CSS attribute selector action '${frag.action}' not supported`); } return { newPath: path, matched }; } function matchTag(frag, path) { if (frag.type !== "tag") throw new error_1.InternalError(util.inspect(frag)); const { elem } = last(path); if (elem == null) throw new error_1.InternalError("null element"); return { newPath: path, matched: uniqueName(elem.componentType) === frag.name }; } /* * Combinators */ function matchChild(frag, path) { if (frag.type !== "child") throw new error_1.InternalError(util.inspect(frag)); if (path.length < 1) return { newPath: path, matched: false }; return { newPath: path.slice(0, -1), matched: true }; } function matchDescendant(selector, path) { if (selector.length <= 0) { throw new error_1.InternalError(`validated but malformed CSS ${util.inspect(selector)}`); } //Note(manishv) An optimization here is to find the deepest element in path //that matches the next set of selectors up to the next descendant selector //and use that path up to that node as tryPath. If it failse, //use the next deepest, etc. Not sure that saves much though because that is //what happens already, albiet through several function calls. for (let i = 1; i < path.length; i++) { const tryPath = path.slice(0, -i); if (matchWithSelector([selector], tryPath)) { return true; } } return false; } /* * Top-level matching */ function matchFrag(selFrag, path) { const matcher = matchConfig[selFrag.type]; if (matcher === undefined) { throw new Error("Unsupported selector fragment: " + fragToString(selFrag)); } return matcher(selFrag, path); } function matchWithSelector(selector, path) { for (const block of selector) { if (matchWithBlock(block, path)) { return true; } } return false; } function matchWithBlock(selBlock, path) { const { prefix, elem: selFrag } = last(selBlock); if (selFrag == null) { return true; //Empty selector matches everything } if (selFrag.type === "descendant") { return matchDescendant(prefix, path); } else { const { newPath, matched } = matchFrag(selFrag, path); if (!matched) return false; if (newPath.length === 0) { return false; } return matchWithBlock(prefix, newPath); } } function validateSelector(_selector) { return; //FIXME(manishv) Actuall validate CSS parse tree here } function buildStyle(rawStyle) { const selector = cssWhat(rawStyle.selector, { xmlMode: true }); validateSelector(selector); return { selector: rawStyle.selector, sfc: rawStyle.build, match: (path) => matchWithSelector(selector, path) }; } function makeStyle(selector, build) { return { selector, build }; } function parseStyles(styles) { const ret = []; for (const style of styles) { ret.push(buildStyle(style)); } return ret; } class Rule { constructor(override) { this.override = override; } } exports.Rule = Rule; function rule(override) { if (override === undefined) { override = (_, i) => i.origElement; } return new Rule(override); } exports.rule = rule; function isRule(x) { return (typeof x === "object") && (x instanceof Rule); } function createMatchInfoReg() { return new Map(); } exports.createMatchInfoReg = createMatchInfoReg; function getCssMatched(reg, el) { let mi = reg.get(el); if (mi === undefined) { mi = {}; reg.set(el, mi); } return mi; } function ruleHasMatched(reg, el, r) { const m = getCssMatched(reg, el); return (m.matched && m.matched.has(r)) === true; } exports.ruleHasMatched = ruleHasMatched; function ruleMatches(reg, el, r) { const m = getCssMatched(reg, el); if (!m.matched) m.matched = new Set(); m.matched.add(r); } exports.ruleMatches = ruleMatches; function neverMatch(reg, el) { const m = getCssMatched(reg, el); m.neverMatch = true; } exports.neverMatch = neverMatch; function canMatch(reg, el) { const m = getCssMatched(reg, el); return m.neverMatch !== true; } exports.canMatch = canMatch; function copyRuleMatches(reg, fromEl, toEl) { const from = getCssMatched(reg, fromEl); const to = getCssMatched(reg, toEl); if (from.neverMatch) { to.neverMatch = true; } else if (from.matched) { if (!to.matched) to.matched = new Set(); for (const r of from.matched) { to.matched.add(r); } } } exports.copyRuleMatches = copyRuleMatches; /** * Marks an element returned by a style rule to not rematch that rule. * * @param info - The second argument to a rule callback * function. This indicates which rule to ignore matches of. * @param elem - The element that should not match the * specified rule. * @returns `elem` is returned as a convenience * * @remarks * This function can be used in a style rule build function to * mark the props of the passed in element such that the rule associated * with the info parameter will not match against the specified element. * * This works by copying the set of all rules that have already matched * successfully against the original element (origElement) specified in the * info parameter onto the passed in elem. * * @example * ```tsx * <Style> * {MyComponent} {Adapt.rule<MyComponentProps>(({ handle, ...props}, info) => * ruleNoRematch(info, <MyComponent {...props} />))} * </Style> * ``` * * @public */ function ruleNoRematch(info, elem) { if (jsx_1.isMountedElement(elem)) { throw new Error(`elem has already been mounted. elem must be a newly created element`); } if (!isStyleBuildInfoInternal(info)) { throw new Error(`Unable to find $matchInfoReg symbol on StyleBuildInfo object`); } copyRuleMatches(info[exports.$matchInfoReg], info.origElement, elem); return elem; } exports.ruleNoRematch = ruleNoRematch; function isStylesComponent(componentType) { return componentType === Style; } const objToName = new WeakMap(); const uniqueNamePrefix = "UniqueName"; let nextUniqueNameIndex = 0; function hasName(o) { if (Object.hasOwnProperty.apply(o, ["name"])) { return ld.isString(o.name); } return false; } function uniqueName(o) { let ret = objToName.get(o); if (ret === undefined) { const objName = hasName(o) ? o.name : ""; ret = uniqueNamePrefix + nextUniqueNameIndex + objName; objToName.set(o, ret); nextUniqueNameIndex++; } return ret; } function buildStyles(styleElem) { if (styleElem == null) { return []; } const stylesConstructor = styleElem.componentType; if (!isStylesComponent(stylesConstructor)) { throw new Error("Invalid Styles element: " + util.inspect(styleElem)); } let curSelector = ""; const rawStyles = []; for (const child of jsx_1.childrenToArray(styleElem.props.children)) { if (typeof child === "function") { curSelector = curSelector + uniqueName(child); } else if (typeof child === "string") { curSelector += child; } else if (isRule(child)) { rawStyles.push(makeStyle(curSelector.trim(), child.override)); curSelector = ""; } else { throw new Error(`Unsupported child type in Styles: "${typeof child}" (value: ${util.inspect(child)})`); } } if (curSelector !== "") { throw new Error("Missing rule in final style"); } return parseStyles(rawStyles); } exports.buildStyles = buildStyles; //FIXME(manishv) This is horribly slow, use a browser-like right-to-left set-matching algorithm instead function findInDomImpl(styles, path) { const elem = ld.last(path); if (elem == null) return []; const matches = []; for (const style of styles) { if (style.match(path)) { matches.push(path); break; } } const children = jsx_1.childrenToArray(elem.props.children); for (const child of children) { if (jsx_1.isElement(child)) { matches.push(...findInDomImpl(styles, [...path, child])); } } return matches; } function findElementsInDom(stylesIn, dom) { return ld.compact(findPathsInDom(stylesIn, dom) .map((path) => ld.last(path))); } exports.findElementsInDom = findElementsInDom; function findPathsInDom(stylesIn, dom) { if (stylesIn == null) return []; const styles = jsx_1.isElement(stylesIn) ? buildStyles(stylesIn) : stylesIn; if (dom === null) return []; return findInDomImpl(styles, [dom]); } exports.findPathsInDom = findPathsInDom; class Style extends jsx_1.Component { build() { return null; //Don't output anything for styles if it makes it to DOM } } exports.Style = Style; /** * Concatenate all of the rules of the given Style elements * together into a single Style element that contains all of the * rules. Always returns a new Style element and does not modify * the Style element parameters. * * @param styles - * Zero or more Style elements, each containing style rules. * @returns * A new Style element containing the concatenation of all * of the rules from the passed in Style elements. * @public */ function concatStyles(...styles) { const rules = []; for (const styleElem of styles) { if (!isStylesComponent(styleElem.componentType)) { throw new Error("Invalid Styles element: " + util.inspect(styleElem)); } const kids = styleElem.props.children; if (kids == null) continue; if (!Array.isArray(kids)) { throw new Error(`Invalid type for children of a Style ` + `element: ${typeof kids}`); } rules.push(...styleElem.props.children); } return jsx_1.createElement(Style, {}, rules); } exports.concatStyles = concatStyles; //# sourceMappingURL=css.js.map