@adpt/core
Version:
AdaptJS core library
451 lines • 14.4 kB
JavaScript
;
/*
* 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