less
Version:
Leaner CSS
1,067 lines (954 loc) • 42.2 kB
JavaScript
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo, VisibilityInfo } from './node.js' */
/** @import { FunctionRegistry } from './nested-at-rule.js' */
import Node from './node.js';
import Declaration from './declaration.js';
import Keyword from './keyword.js';
import Comment from './comment.js';
import Paren from './paren.js';
import Selector from './selector.js';
import Element from './element.js';
import Anonymous from './anonymous.js';
import contexts from '../contexts.js';
import globalFunctionRegistry from '../functions/function-registry.js';
import defaultFunc from '../functions/default.js';
import getDebugInfo from './debug-info.js';
import * as utils from '../utils.js';
import Parser from '../parser/parser.js';
/**
* @typedef {Node & {
* rules?: Node[],
* selectors?: Selector[],
* root?: boolean,
* firstRoot?: boolean,
* allowImports?: boolean,
* functionRegistry?: FunctionRegistry,
* originalRuleset?: Node,
* debugInfo?: { lineNumber: number, fileName: string },
* evalFirst?: boolean,
* isRuleset?: boolean,
* isCharset?: () => boolean,
* merge?: boolean | string,
* multiMedia?: boolean,
* parse?: { context: EvalContext, importManager: object },
* bubbleSelectors?: (selectors: Selector[]) => void
* }} RuleNode
*/
class Ruleset extends Node {
get type() { return 'Ruleset'; }
/**
* @param {Selector[] | null} selectors
* @param {Node[] | null} rules
* @param {boolean} [strictImports]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(selectors, rules, strictImports, visibilityInfo) {
super();
/** @type {Selector[] | null} */
this.selectors = selectors;
/** @type {Node[] | null} */
this.rules = rules;
/** @type {Object<string, { rule: Node, path: Node[] }[]>} */
this._lookups = {};
/** @type {Object<string, Declaration> | null} */
this._variables = null;
/** @type {Object<string, Declaration[]> | null} */
this._properties = null;
/** @type {boolean | undefined} */
this.strictImports = strictImports;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
/** @type {boolean} */
this.isRuleset = true;
/** @type {boolean | undefined} */
this.root = undefined;
/** @type {boolean | undefined} */
this.firstRoot = undefined;
/** @type {boolean | undefined} */
this.allowImports = undefined;
/** @type {FunctionRegistry | undefined} */
this.functionRegistry = undefined;
/** @type {Node | undefined} */
this.originalRuleset = undefined;
/** @type {{ lineNumber: number, fileName: string } | undefined} */
this.debugInfo = undefined;
/** @type {Selector[][] | undefined} */
this.paths = undefined;
/** @type {Ruleset[] | null | undefined} */
this._rulesets = undefined;
/** @type {boolean | undefined} */
this.evalFirst = undefined;
this.setParent(this.selectors, this);
this.setParent(this.rules, this);
}
isRulesetLike() { return true; }
/** @param {TreeVisitor} visitor */
accept(visitor) {
if (this.paths) {
this.paths = /** @type {Selector[][]} */ (/** @type {unknown} */ (visitor.visitArray(/** @type {Node[]} */ (/** @type {unknown} */ (this.paths)), true)));
} else if (this.selectors) {
this.selectors = /** @type {Selector[]} */ (visitor.visitArray(this.selectors));
}
if (this.rules && this.rules.length) {
this.rules = visitor.visitArray(this.rules);
}
}
/** @param {EvalContext} context */
eval(context) {
/** @type {Selector[] | undefined} */
let selectors;
/** @type {number} */
let selCnt;
/** @type {Selector} */
let selector;
/** @type {number} */
let i;
/** @type {boolean | undefined} */
let hasVariable;
let hasOnePassingSelector = false;
if (this.selectors && (selCnt = this.selectors.length)) {
selectors = new Array(selCnt);
defaultFunc.error({
type: 'Syntax',
message: 'it is currently only allowed in parametric mixin guards,'
});
for (i = 0; i < selCnt; i++) {
selector = /** @type {Selector} */ (this.selectors[i].eval(context));
for (let j = 0; j < selector.elements.length; j++) {
if (selector.elements[j].isVariable) {
hasVariable = true;
break;
}
}
selectors[i] = selector;
if (selector.evaldCondition) {
hasOnePassingSelector = true;
}
}
if (hasVariable) {
const toParseSelectors = new Array(selCnt);
for (i = 0; i < selCnt; i++) {
selector = selectors[i];
toParseSelectors[i] = selector.toCSS(context);
}
const startingIndex = selectors[0].getIndex();
const selectorFileInfo = selectors[0].fileInfo();
new (/** @type {new (...args: [EvalContext, object, FileInfo, number]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(context, /** @type {{ context: EvalContext, importManager: object }} */ (this.parse).importManager, selectorFileInfo, startingIndex).parseNode(
toParseSelectors.join(','),
['selectors'],
function(/** @type {Error | null} */ err, /** @type {Node[]} */ result) {
if (result) {
selectors = /** @type {Selector[]} */ (utils.flattenArray(result));
}
});
}
defaultFunc.reset();
} else {
hasOnePassingSelector = true;
}
let rules = this.rules ? utils.copyArray(this.rules) : null;
const ruleset = new Ruleset(selectors, rules, this.strictImports, this.visibilityInfo());
/** @type {Node} */
let rule;
/** @type {Node} */
let subRule;
ruleset.originalRuleset = this;
ruleset.root = this.root;
ruleset.firstRoot = this.firstRoot;
ruleset.allowImports = this.allowImports;
if (this.debugInfo) {
ruleset.debugInfo = this.debugInfo;
}
if (!hasOnePassingSelector) {
/** @type {Node[]} */ (rules).length = 0;
}
// push the current ruleset to the frames stack
const ctxFrames = context.frames;
// inherit a function registry from the frames stack when possible;
// otherwise from the global registry
/** @type {FunctionRegistry | undefined} */
let foundRegistry;
for (let fi = 0, fn = ctxFrames.length; fi !== fn; ++fi) {
foundRegistry = /** @type {RuleNode} */ (ctxFrames[fi]).functionRegistry;
if (foundRegistry) { break; }
}
ruleset.functionRegistry = (foundRegistry || globalFunctionRegistry).inherit();
ctxFrames.unshift(ruleset);
// currrent selectors
/** @type {Selector[][] | undefined} */
let ctxSelectors = /** @type {EvalContext & { selectors?: Selector[][] }} */ (context).selectors;
if (!ctxSelectors) {
/** @type {EvalContext & { selectors?: Selector[][] }} */ (context).selectors = ctxSelectors = [];
}
ctxSelectors.unshift(this.selectors);
// Evaluate imports
if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) {
ruleset.evalImports(context);
}
// Store the frames around mixin definitions,
// so they can be evaluated like closures when the time comes.
const rsRules = /** @type {Node[]} */ (ruleset.rules);
for (i = 0; (rule = rsRules[i]); i++) {
if (/** @type {RuleNode} */ (rule).evalFirst) {
rsRules[i] = rule.eval(context);
}
}
const mediaBlockCount = (context.mediaBlocks && context.mediaBlocks.length) || 0;
// Evaluate mixin calls.
for (i = 0; (rule = rsRules[i]); i++) {
if (rule.type === 'MixinCall') {
/* jshint loopfunc:true */
rules = /** @type {Node[]} */ (/** @type {unknown} */ (rule.eval(context))).filter(function(/** @type {Node & { variable?: boolean }} */ r) {
if ((r instanceof Declaration) && r.variable) {
// do not pollute the scope if the variable is
// already there. consider returning false here
// but we need a way to "return" variable from mixins
return !(ruleset.variable(/** @type {string} */ (r.name)));
}
return true;
});
rsRules.splice.apply(rsRules, /** @type {[number, number, ...Node[]]} */ ([i, 1].concat(rules)));
i += rules.length - 1;
ruleset.resetCache();
} else if (rule.type === 'VariableCall') {
/* jshint loopfunc:true */
rules = /** @type {Node[]} */ (/** @type {RuleNode} */ (rule.eval(context)).rules).filter(function(/** @type {Node & { variable?: boolean }} */ r) {
if ((r instanceof Declaration) && r.variable) {
// do not pollute the scope at all
return false;
}
return true;
});
rsRules.splice.apply(rsRules, /** @type {[number, number, ...Node[]]} */ ([i, 1].concat(rules)));
i += rules.length - 1;
ruleset.resetCache();
}
}
// Evaluate everything else
for (i = 0; (rule = rsRules[i]); i++) {
if (!/** @type {RuleNode} */ (rule).evalFirst) {
rsRules[i] = rule = rule.eval ? rule.eval(context) : rule;
}
}
// Evaluate everything else
for (i = 0; (rule = rsRules[i]); i++) {
// for rulesets, check if it is a css guard and can be removed
if (rule instanceof Ruleset && rule.selectors && rule.selectors.length === 1) {
// check if it can be folded in (e.g. & where)
if (rule.selectors[0] && rule.selectors[0].isJustParentSelector()) {
rsRules.splice(i--, 1);
for (let j = 0; (subRule = rule.rules[j]); j++) {
if (subRule instanceof Node) {
subRule.copyVisibilityInfo(rule.visibilityInfo());
if (!(subRule instanceof Declaration) || !subRule.variable) {
rsRules.splice(++i, 0, subRule);
}
}
}
}
}
}
// Pop the stack
ctxFrames.shift();
ctxSelectors.shift();
if (context.mediaBlocks) {
for (i = mediaBlockCount; i < context.mediaBlocks.length; i++) {
/** @type {RuleNode} */ (context.mediaBlocks[i]).bubbleSelectors(selectors);
}
}
return ruleset;
}
/** @param {EvalContext} context */
evalImports(context) {
const rules = this.rules;
/** @type {number} */
let i;
/** @type {Node | Node[]} */
let importRules;
if (!rules) { return; }
for (i = 0; i < rules.length; i++) {
if (rules[i].type === 'Import') {
importRules = rules[i].eval(context);
if (importRules && (/** @type {Node[]} */ (/** @type {unknown} */ (importRules)).length || /** @type {Node[]} */ (/** @type {unknown} */ (importRules)).length === 0)) {
const importArr = /** @type {Node[]} */ (/** @type {unknown} */ (importRules));
rules.splice(i, 1, ...importArr);
i += importArr.length - 1;
} else {
rules.splice(i, 1, importRules);
}
this.resetCache();
}
}
}
makeImportant() {
const result = new Ruleset(this.selectors, /** @type {Node[]} */ (this.rules).map(function (/** @type {Node & { makeImportant?: () => Node }} */ r) {
if (r.makeImportant) {
return r.makeImportant();
} else {
return r;
}
}), this.strictImports, this.visibilityInfo());
return result;
}
/** @param {Node[] | object[] | null} [args] */
matchArgs(args) {
return !args || args.length === 0;
}
/**
* @param {Node[] | object[] | null} args
* @param {EvalContext} context
*/
matchCondition(args, context) {
const lastSelector = /** @type {Selector[]} */ (this.selectors)[/** @type {Selector[]} */ (this.selectors).length - 1];
if (!lastSelector.evaldCondition) {
return false;
}
if (lastSelector.condition &&
!lastSelector.condition.eval(
new contexts.Eval(context,
context.frames))) {
return false;
}
return true;
}
resetCache() {
this._rulesets = null;
this._variables = null;
this._properties = null;
this._lookups = {};
}
variables() {
if (!this._variables) {
this._variables = !this.rules ? {} : this.rules.reduce(function (/** @type {Object<string, Declaration>} */ hash, /** @type {Node} */ r) {
if (r instanceof Declaration && r.variable === true) {
hash[/** @type {string} */ (r.name)] = r;
}
// when evaluating variables in an import statement, imports have not been eval'd
// so we need to go inside import statements.
// guard against root being a string (in the case of inlined less)
if (r.type === 'Import' && /** @type {RuleNode} */ (r).root && /** @type {RuleNode & { root: Ruleset }} */ (r).root.variables) {
const vars = /** @type {RuleNode & { root: Ruleset }} */ (r).root.variables();
for (const name in vars) {
if (Object.prototype.hasOwnProperty.call(vars, name)) {
hash[name] = /** @type {Declaration} */ (/** @type {RuleNode & { root: Ruleset }} */ (r).root.variable(name));
}
}
}
return hash;
}, {});
}
return this._variables;
}
properties() {
if (!this._properties) {
this._properties = !this.rules ? {} : this.rules.reduce(function (/** @type {Object<string, Declaration[]>} */ hash, /** @type {Node} */ r) {
if (r instanceof Declaration && r.variable !== true) {
const name = (/** @type {Node[]} */ (r.name).length === 1) && (/** @type {Node[]} */ (r.name)[0] instanceof Keyword) ?
/** @type {string} */ (/** @type {Node[]} */ (r.name)[0].value) : /** @type {string} */ (r.name);
// Properties don't overwrite as they can merge
if (!hash[`$${name}`]) {
hash[`$${name}`] = [ r ];
}
else {
hash[`$${name}`].push(r);
}
}
return hash;
}, {});
}
return this._properties;
}
/** @param {string} name */
variable(name) {
const decl = this.variables()[name];
if (decl) {
return this.parseValue(decl);
}
}
/** @param {string} name */
property(name) {
const decl = this.properties()[name];
if (decl) {
return this.parseValue(decl);
}
}
lastDeclaration() {
for (let i = /** @type {Node[]} */ (this.rules).length; i > 0; i--) {
const decl = /** @type {Node[]} */ (this.rules)[i - 1];
if (decl instanceof Declaration) {
return this.parseValue(decl);
}
}
}
/** @param {Declaration | Declaration[]} toParse */
parseValue(toParse) {
const self = this;
/** @param {Declaration} decl */
function transformDeclaration(decl) {
if (decl.value instanceof Anonymous && !/** @type {Declaration & { parsed?: boolean }} */ (decl).parsed) {
if (typeof decl.value.value === 'string') {
new (/** @type {new (...args: [EvalContext, object, FileInfo, number]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(/** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).context, /** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).importManager, decl.fileInfo(), decl.value.getIndex()).parseNode(
decl.value.value,
['value', 'important'],
function(/** @type {Error | null} */ err, /** @type {Node[]} */ result) {
if (err) {
decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true));
}
if (result) {
decl.value = result[0];
/** @type {Declaration & { important?: string }} */ (decl).important = /** @type {string} */ (/** @type {unknown} */ (result[1])) || '';
decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true));
}
});
} else {
decl.parsed = /** @type {Node} */ (/** @type {unknown} */ (true));
}
return decl;
}
else {
return decl;
}
}
if (!Array.isArray(toParse)) {
return transformDeclaration.call(self, toParse);
}
else {
/** @type {Declaration[]} */
const nodes = [];
for (let ti = 0; ti < toParse.length; ti++) {
nodes.push(transformDeclaration.call(self, toParse[ti]));
}
return nodes;
}
}
rulesets() {
if (!this.rules) { return []; }
/** @type {Node[]} */
const filtRules = [];
const rules = this.rules;
/** @type {number} */
let i;
/** @type {Node} */
let rule;
for (i = 0; (rule = rules[i]); i++) {
if (/** @type {RuleNode} */ (rule).isRuleset) {
filtRules.push(rule);
}
}
return filtRules;
}
/** @param {Node} rule */
prependRule(rule) {
const rules = this.rules;
if (rules) {
rules.unshift(rule);
} else {
this.rules = [ rule ];
}
this.setParent(rule, this);
}
/**
* @param {Selector} selector
* @param {Ruleset | null} [self]
* @param {((rule: Node) => boolean)} [filter]
* @returns {{ rule: Node, path: Node[] }[]}
*/
find(selector, self, filter) {
self = self || this;
/** @type {{ rule: Node, path: Node[] }[]} */
const rules = [];
/** @type {number | undefined} */
let match;
/** @type {{ rule: Node, path: Node[] }[]} */
let foundMixins;
const key = selector.toCSS(/** @type {EvalContext} */ ({}));
if (key in this._lookups) { return /** @type {{ rule: Node, path: Node[] }[]} */ (this._lookups[key]); }
this.rulesets().forEach(function (rule) {
if (rule !== self) {
for (let j = 0; j < /** @type {RuleNode} */ (rule).selectors.length; j++) {
match = selector.match(/** @type {RuleNode} */ (rule).selectors[j]);
if (match) {
if (selector.elements.length > match) {
if (!filter || filter(rule)) {
foundMixins = /** @type {Ruleset} */ (/** @type {unknown} */ (rule)).find(new Selector(selector.elements.slice(match)), self, filter);
for (let i = 0; i < foundMixins.length; ++i) {
foundMixins[i].path.push(rule);
}
Array.prototype.push.apply(rules, foundMixins);
}
} else {
rules.push({ rule, path: []});
}
break;
}
}
}
});
this._lookups[key] = rules;
return rules;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
/** @type {number} */
let i;
/** @type {number} */
let j;
/** @type {Node[]} */
const charsetRuleNodes = [];
/** @type {Node[]} */
let ruleNodes = [];
let // Line number debugging
debugInfo;
/** @type {Node} */
let rule;
/** @type {Selector[]} */
let path;
context.tabLevel = (context.tabLevel || 0);
if (!this.root) {
context.tabLevel++;
}
const tabRuleStr = context.compress ? '' : Array(context.tabLevel + 1).join(' ');
const tabSetStr = context.compress ? '' : Array(context.tabLevel).join(' ');
/** @type {string} */
let sep;
let charsetNodeIndex = 0;
let importNodeIndex = 0;
for (i = 0; (rule = /** @type {Node[]} */ (this.rules)[i]); i++) {
if (rule instanceof Comment) {
if (importNodeIndex === i) {
importNodeIndex++;
}
ruleNodes.push(rule);
} else if (/** @type {RuleNode} */ (rule).isCharset && /** @type {RuleNode} */ (rule).isCharset()) {
ruleNodes.splice(charsetNodeIndex, 0, rule);
charsetNodeIndex++;
importNodeIndex++;
} else if (rule.type === 'Import') {
ruleNodes.splice(importNodeIndex, 0, rule);
importNodeIndex++;
} else {
ruleNodes.push(rule);
}
}
ruleNodes = charsetRuleNodes.concat(ruleNodes);
// If this is the root node, we don't render
// a selector, or {}.
if (!this.root) {
debugInfo = getDebugInfo(context, /** @type {{ debugInfo: { lineNumber: number, fileName: string } }} */ (/** @type {unknown} */ (this)), tabSetStr);
if (debugInfo) {
output.add(debugInfo);
output.add(tabSetStr);
}
const paths = /** @type {Selector[][]} */ (this.paths);
const pathCnt = paths.length;
/** @type {number} */
let pathSubCnt;
sep = context.compress ? ',' : (`,\n${tabSetStr}`);
for (i = 0; i < pathCnt; i++) {
path = paths[i];
if (!(pathSubCnt = path.length)) { continue; }
if (i > 0) { output.add(sep); }
/** @type {EvalContext & { firstSelector?: boolean }} */ (context).firstSelector = true;
path[0].genCSS(context, output);
/** @type {EvalContext & { firstSelector?: boolean }} */ (context).firstSelector = false;
for (j = 1; j < pathSubCnt; j++) {
path[j].genCSS(context, output);
}
}
output.add((context.compress ? '{' : ' {\n') + tabRuleStr);
}
// Compile rules and rulesets
for (i = 0; (rule = ruleNodes[i]); i++) {
if (i + 1 === ruleNodes.length) {
context.lastRule = true;
}
const currentLastRule = context.lastRule;
if (rule.isRulesetLike()) {
context.lastRule = false;
}
if (rule.genCSS) {
rule.genCSS(context, output);
} else if (rule.value) {
output.add(/** @type {string} */ (rule.value).toString());
}
context.lastRule = currentLastRule;
if (!context.lastRule && rule.isVisible()) {
output.add(context.compress ? '' : (`\n${tabRuleStr}`));
} else {
context.lastRule = false;
}
}
if (!this.root) {
output.add((context.compress ? '}' : `\n${tabSetStr}}`));
context.tabLevel--;
}
if (!output.isEmpty() && !context.compress && this.firstRoot) {
output.add('\n');
}
}
/**
* @param {Selector[][]} paths
* @param {Selector[][]} context
* @param {Selector[]} selectors
*/
joinSelectors(paths, context, selectors) {
for (let s = 0; s < selectors.length; s++) {
this.joinSelector(paths, context, selectors[s]);
}
}
/**
* @param {Selector[][]} paths
* @param {Selector[][]} context
* @param {Selector} selector
*/
joinSelector(paths, context, selector) {
/**
* @param {Selector[]} elementsToPak
* @param {Element} originalElement
* @returns {Paren}
*/
function createParenthesis(elementsToPak, originalElement) {
/** @type {Paren} */
let replacementParen;
/** @type {number} */
let j;
if (elementsToPak.length === 0) {
replacementParen = new Paren(elementsToPak[0]);
} else {
const insideParent = new Array(elementsToPak.length);
for (j = 0; j < elementsToPak.length; j++) {
insideParent[j] = new Element(
null,
elementsToPak[j],
originalElement.isVariable,
originalElement._index,
originalElement._fileInfo
);
}
replacementParen = new Paren(new Selector(insideParent));
}
return replacementParen;
}
/**
* @param {Paren | Selector} containedElement
* @param {Element} originalElement
* @returns {Selector}
*/
function createSelector(containedElement, originalElement) {
/** @type {Element} */
let element;
/** @type {Selector} */
let selector;
element = new Element(null, containedElement, originalElement.isVariable, originalElement._index, originalElement._fileInfo);
selector = new Selector([element]);
return selector;
}
/**
* @param {Selector[]} beginningPath
* @param {Selector[]} addPath
* @param {Element} replacedElement
* @param {Selector} originalSelector
* @returns {Selector[]}
*/
function addReplacementIntoPath(beginningPath, addPath, replacedElement, originalSelector) {
/** @type {Selector[]} */
let newSelectorPath;
/** @type {Selector} */
let lastSelector;
/** @type {Selector} */
let newJoinedSelector;
// our new selector path
newSelectorPath = [];
// construct the joined selector - if & is the first thing this will be empty,
// if not newJoinedSelector will be the last set of elements in the selector
if (beginningPath.length > 0) {
newSelectorPath = utils.copyArray(beginningPath);
lastSelector = newSelectorPath.pop();
newJoinedSelector = originalSelector.createDerived(utils.copyArray(lastSelector.elements));
}
else {
newJoinedSelector = originalSelector.createDerived([]);
}
if (addPath.length > 0) {
// /deep/ is a CSS4 selector - (removed, so should deprecate)
// that is valid without anything in front of it
// so if the & does not have a combinator that is "" or " " then
// and there is a combinator on the parent, then grab that.
// this also allows + a { & .b { .a & { ... though not sure why you would want to do that
let combinator = replacedElement.combinator;
const parentEl = addPath[0].elements[0];
if (combinator.emptyOrWhitespace && !parentEl.combinator.emptyOrWhitespace) {
combinator = parentEl.combinator;
}
// join the elements so far with the first part of the parent
newJoinedSelector.elements.push(new Element(
combinator,
parentEl.value,
replacedElement.isVariable,
replacedElement._index,
replacedElement._fileInfo
));
newJoinedSelector.elements = newJoinedSelector.elements.concat(addPath[0].elements.slice(1));
}
// now add the joined selector - but only if it is not empty
if (newJoinedSelector.elements.length !== 0) {
newSelectorPath.push(newJoinedSelector);
}
// put together the parent selectors after the join (e.g. the rest of the parent)
if (addPath.length > 1) {
let restOfPath = addPath.slice(1);
restOfPath = restOfPath.map(function (/** @type {Selector} */ selector) {
return selector.createDerived(selector.elements, []);
});
newSelectorPath = newSelectorPath.concat(restOfPath);
}
return newSelectorPath;
}
/**
* @param {Selector[][]} beginningPath
* @param {Selector[]} addPaths
* @param {Element} replacedElement
* @param {Selector} originalSelector
* @param {Selector[][]} result
* @returns {Selector[][]}
*/
function addAllReplacementsIntoPath( beginningPath, addPaths, replacedElement, originalSelector, result) {
/** @type {number} */
let j;
for (j = 0; j < beginningPath.length; j++) {
const newSelectorPath = addReplacementIntoPath(beginningPath[j], addPaths, replacedElement, originalSelector);
result.push(newSelectorPath);
}
return result;
}
/**
* @param {Element[]} elements
* @param {Selector[][]} selectors
*/
function mergeElementsOnToSelectors(elements, selectors) {
/** @type {number} */
let i;
/** @type {Selector[]} */
let sel;
if (elements.length === 0) {
return ;
}
if (selectors.length === 0) {
selectors.push([ new Selector(elements) ]);
return;
}
for (i = 0; (sel = selectors[i]); i++) {
// if the previous thing in sel is a parent this needs to join on to it
if (sel.length > 0) {
sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements));
}
else {
sel.push(new Selector(elements));
}
}
}
/**
* @param {Selector[][]} paths
* @param {Selector[][]} context
* @param {Selector} inSelector
* @returns {boolean}
*/
function replaceParentSelector(paths, context, inSelector) {
// The paths are [[Selector]]
// The first list is a list of comma separated selectors
// The inner list is a list of inheritance separated selectors
// e.g.
// .a, .b {
// .c {
// }
// }
// == [[.a] [.c]] [[.b] [.c]]
//
/** @type {number} */
let i;
/** @type {number} */
let j;
/** @type {number} */
let k;
/** @type {Element[]} */
let currentElements;
/** @type {Selector[][]} */
let newSelectors;
/** @type {Selector[][]} */
let selectorsMultiplied;
/** @type {Selector[]} */
let sel;
/** @type {Element} */
let el;
let hadParentSelector = false;
/** @type {number} */
let length;
/** @type {Selector} */
let lastSelector;
/**
* @param {Element} element
* @returns {Selector | null}
*/
function findNestedSelector(element) {
/** @type {Node} */
let maybeSelector;
if (!(element.value instanceof Paren)) {
return null;
}
maybeSelector = /** @type {Node} */ (element.value.value);
if (!(maybeSelector instanceof Selector)) {
return null;
}
return maybeSelector;
}
// the elements from the current selector so far
currentElements = [];
// the current list of new selectors to add to the path.
// We will build it up. We initiate it with one empty selector as we "multiply" the new selectors
// by the parents
newSelectors = [
[]
];
for (i = 0; (el = inSelector.elements[i]); i++) {
// non parent reference elements just get added
if (el.value !== '&') {
const nestedSelector = findNestedSelector(el);
if (nestedSelector !== null) {
// merge the current list of non parent selector elements
// on to the current list of selectors to add
mergeElementsOnToSelectors(currentElements, newSelectors);
/** @type {Selector[][]} */
const nestedPaths = [];
/** @type {boolean | undefined} */
let replaced;
/** @type {Selector[][]} */
const replacedNewSelectors = [];
// Check if this is a comma-separated selector list inside the paren
// e.g. :not(&.a, &.b) produces Selector([Selector, Anonymous(','), Selector])
const hasSubSelectors = nestedSelector.elements.some(e => e instanceof Selector);
if (hasSubSelectors) {
// Process each sub-selector individually
/** @type {(Element | Selector)[]} */
const resolvedElements = [];
for (const subEl of nestedSelector.elements) {
if (subEl instanceof Selector) {
/** @type {Selector[][]} */
const subPaths = [];
const subReplaced = replaceParentSelector(subPaths, context, subEl);
replaced = replaced || subReplaced;
if (subPaths.length > 0 && subPaths[0].length > 0) {
resolvedElements.push(subPaths[0][0]);
} else {
resolvedElements.push(subEl);
}
} else {
resolvedElements.push(subEl);
}
}
hadParentSelector = hadParentSelector || /** @type {boolean} */ (replaced);
const resolvedNestedSelector = new Selector(resolvedElements);
const replacementSelector = createSelector(createParenthesis([resolvedNestedSelector], el), el);
addAllReplacementsIntoPath(newSelectors, [replacementSelector], el, inSelector, replacedNewSelectors);
} else {
replaced = replaceParentSelector(nestedPaths, context, nestedSelector);
hadParentSelector = hadParentSelector || replaced;
// the nestedPaths array should have only one member - replaceParentSelector does not multiply selectors
for (k = 0; k < nestedPaths.length; k++) {
const replacementSelector = createSelector(createParenthesis(nestedPaths[k], el), el);
addAllReplacementsIntoPath(newSelectors, [replacementSelector], el, inSelector, replacedNewSelectors);
}
}
newSelectors = replacedNewSelectors;
currentElements = [];
} else {
currentElements.push(el);
}
} else {
hadParentSelector = true;
// the new list of selectors to add
selectorsMultiplied = [];
// merge the current list of non parent selector elements
// on to the current list of selectors to add
mergeElementsOnToSelectors(currentElements, newSelectors);
// loop through our current selectors
for (j = 0; j < newSelectors.length; j++) {
sel = newSelectors[j];
// if we don't have any parent paths, the & might be in a mixin so that it can be used
// whether there are parents or not
if (context.length === 0) {
// the combinator used on el should now be applied to the next element instead so that
// it is not lost
if (sel.length > 0) {
sel[0].elements.push(new Element(el.combinator, '', el.isVariable, el._index, el._fileInfo));
}
selectorsMultiplied.push(sel);
}
else {
// and the parent selectors
for (k = 0; k < context.length; k++) {
// We need to put the current selectors
// then join the last selector's elements on to the parents selectors
const newSelectorPath = addReplacementIntoPath(sel, context[k], el, inSelector);
// add that to our new set of selectors
selectorsMultiplied.push(newSelectorPath);
}
}
}
// our new selectors has been multiplied, so reset the state
newSelectors = selectorsMultiplied;
currentElements = [];
}
}
// if we have any elements left over (e.g. .a& .b == .b)
// add them on to all the current selectors
mergeElementsOnToSelectors(currentElements, newSelectors);
for (i = 0; i < newSelectors.length; i++) {
length = newSelectors[i].length;
if (length > 0) {
paths.push(newSelectors[i]);
lastSelector = newSelectors[i][length - 1];
newSelectors[i][length - 1] = lastSelector.createDerived(lastSelector.elements, inSelector.extendList);
}
}
return hadParentSelector;
}
/**
* @param {VisibilityInfo} visibilityInfo
* @param {Selector} deriveFrom
*/
function deriveSelector(visibilityInfo, deriveFrom) {
const newSelector = deriveFrom.createDerived(deriveFrom.elements, deriveFrom.extendList, deriveFrom.evaldCondition);
newSelector.copyVisibilityInfo(visibilityInfo);
return newSelector;
}
// joinSelector code follows
/** @type {number} */
let i;
/** @type {Selector[][]} */
let newPaths;
/** @type {boolean} */
let hadParentSelector;
newPaths = [];
hadParentSelector = replaceParentSelector(newPaths, context, selector);
if (!hadParentSelector) {
if (context.length > 0) {
newPaths = [];
for (i = 0; i < context.length; i++) {
const concatenated = context[i].map(deriveSelector.bind(this, selector.visibilityInfo()));
concatenated.push(selector);
newPaths.push(concatenated);
}
}
else {
newPaths = [[selector]];
}
}
for (i = 0; i < newPaths.length; i++) {
paths.push(newPaths[i]);
}
}
}
export default Ruleset;