less
Version:
Leaner CSS
341 lines (308 loc) • 12.9 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 Selector from './selector.js';
import Ruleset from './ruleset.js';
import Anonymous from './anonymous.js';
import NestableAtRulePrototype from './nested-at-rule.js';
import mergeRules from './merge-rules.js';
/**
* @typedef {Node & {
* rules?: Node[],
* selectors?: Selector[],
* root?: boolean,
* allowImports?: boolean,
* functionRegistry?: FunctionRegistry,
* merge?: boolean,
* debugInfo?: { lineNumber: number, fileName: string },
* elements?: import('./element.js').default[]
* }} RulesetLikeNode
*/
class AtRule extends Node {
get type() { return 'AtRule'; }
/**
* @param {string} [name]
* @param {Node | string} [value]
* @param {Node[] | Ruleset} [rules]
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {{ lineNumber: number, fileName: string }} [debugInfo]
* @param {boolean} [isRooted]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(
name,
value,
rules,
index,
currentFileInfo,
debugInfo,
isRooted,
visibilityInfo
) {
super();
let i;
var selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
/** @type {string | undefined} */
this.name = name;
this.value = (value instanceof Node) ? value : (value ? new Anonymous(value) : value);
/** @type {boolean | undefined} */
this.simpleBlock = undefined;
/** @type {RulesetLikeNode[] | undefined} */
this.declarations = undefined;
/** @type {RulesetLikeNode[] | undefined} */
this.rules = undefined;
if (rules) {
if (Array.isArray(rules)) {
const allDeclarations = this.declarationsBlock(rules);
let allRulesetDeclarations = true;
rules.forEach(rule => {
if (rule.type === 'Ruleset' && /** @type {RulesetLikeNode} */ (rule).rules) allRulesetDeclarations = allRulesetDeclarations && this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rule).rules), true);
});
if (allDeclarations && !isRooted) {
this.simpleBlock = true;
this.declarations = rules;
} else if (allRulesetDeclarations && rules.length === 1 && !isRooted && !value) {
this.simpleBlock = true;
this.declarations = /** @type {RulesetLikeNode} */ (rules[0]).rules ? /** @type {RulesetLikeNode} */ (rules[0]).rules : rules;
} else {
this.rules = rules;
}
} else {
const allDeclarations = this.declarationsBlock(/** @type {Node[]} */ (rules.rules));
if (allDeclarations && !isRooted && !value) {
this.simpleBlock = true;
this.declarations = rules.rules;
} else {
this.rules = [rules];
/** @type {RulesetLikeNode} */ (this.rules[0]).selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
}
}
if (!this.simpleBlock) {
for (i = 0; i < this.rules.length; i++) {
/** @type {RulesetLikeNode} */ (this.rules[i]).allowImports = true;
}
}
if (this.declarations) {
this.setParent(this.declarations, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
if (this.rules) {
this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this)));
}
}
this._index = index;
this._fileInfo = currentFileInfo;
/** @type {{ lineNumber: number, fileName: string } | undefined} */
this.debugInfo = debugInfo;
/** @type {boolean} */
this.isRooted = isRooted || false;
this.copyVisibilityInfo(visibilityInfo);
this.allowRoot = true;
}
/**
* @param {Node[]} rules
* @param {boolean} [mergeable]
* @returns {boolean}
*/
declarationsBlock(rules, mergeable = false) {
if (!mergeable) {
return rules.filter(function (/** @type {Node & { merge?: boolean }} */ node) { return (node.type === 'Declaration' || node.type === 'Comment') && !node.merge}).length === rules.length;
} else {
return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Declaration' || node.type === 'Comment'); }).length === rules.length;
}
}
/**
* @param {Node[]} rules
* @returns {boolean}
*/
keywordList(rules) {
if (!Array.isArray(rules)) {
return false;
} else {
return rules.filter(function (/** @type {Node} */ node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length;
}
}
/** @param {TreeVisitor} visitor */
accept(visitor) {
const value = this.value, rules = this.rules, declarations = this.declarations;
if (rules) {
this.rules = visitor.visitArray(rules);
} else if (declarations) {
this.declarations = visitor.visitArray(declarations);
}
if (value) {
this.value = visitor.visit(/** @type {Node} */ (value));
}
}
/** @override @returns {boolean} */
isRulesetLike() {
return /** @type {boolean} */ (/** @type {unknown} */ (this.rules || !this.isCharset()));
}
isCharset() {
return '@charset' === this.name;
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
const value = this.value, rules = this.rules || this.declarations;
output.add(/** @type {string} */ (this.name), this.fileInfo(), this.getIndex());
if (value) {
output.add(' ');
/** @type {Node} */ (value).genCSS(context, output);
}
if (this.simpleBlock) {
this.outputRuleset(context, output, /** @type {Node[]} */ (this.declarations));
} else if (rules) {
this.outputRuleset(context, output, rules);
} else {
output.add(';');
}
}
/**
* @param {EvalContext} context
* @returns {Node}
*/
eval(context) {
let mediaPathBackup, mediaBlocksBackup, value = this.value, rules = this.rules || this.declarations;
// media stored inside other atrule should not bubble over it
// backpup media bubbling information
mediaPathBackup = context.mediaPath;
mediaBlocksBackup = context.mediaBlocks;
// deleted media bubbling information
context.mediaPath = [];
context.mediaBlocks = [];
if (value) {
value = /** @type {Node} */ (value).eval(context);
}
if (rules) {
rules = this.evalRoot(context, rules);
}
if (Array.isArray(rules) && /** @type {RulesetLikeNode} */ (rules[0]).rules && Array.isArray(/** @type {RulesetLikeNode} */ (rules[0]).rules) && /** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules).length) {
const allMergeableDeclarations = this.declarationsBlock(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules), true);
if (allMergeableDeclarations && !this.isRooted && !value) {
mergeRules(/** @type {Node[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules));
rules = /** @type {RulesetLikeNode[]} */ (/** @type {RulesetLikeNode} */ (rules[0]).rules);
rules.forEach(/** @param {RulesetLikeNode} rule */ rule => { rule.merge = false; });
}
}
if (this.simpleBlock && rules) {
/** @type {RulesetLikeNode} */ (rules[0]).functionRegistry = /** @type {RulesetLikeNode} */ (context.frames[0]).functionRegistry.inherit();
rules = rules.map(function (/** @type {Node} */ rule) { return rule.eval(context); });
}
// restore media bubbling information
context.mediaPath = mediaPathBackup;
context.mediaBlocks = mediaBlocksBackup;
return /** @type {Node} */ (/** @type {unknown} */ (new AtRule(this.name, value, rules, this.getIndex(), this.fileInfo(), this.debugInfo, this.isRooted, this.visibilityInfo())));
}
/**
* @param {EvalContext} context
* @param {Node[]} rules
* @returns {Node[]}
*/
evalRoot(context, rules) {
let ampersandCount = 0;
let noAmpersandCount = 0;
let noAmpersands = true;
if (!this.simpleBlock) {
rules = [rules[0].eval(context)];
}
/** @type {Selector[]} */
let precedingSelectors = [];
if (context.frames.length > 0) {
for (let index = 0; index < context.frames.length; index++) {
const frame = /** @type {RulesetLikeNode} */ (context.frames[index]);
if (
frame.type === 'Ruleset' &&
frame.rules &&
frame.rules.length > 0
) {
if (frame && !frame.root && frame.selectors && frame.selectors.length > 0) {
precedingSelectors = precedingSelectors.concat(frame.selectors);
}
}
if (precedingSelectors.length > 0) {
const allAmpersandElements = precedingSelectors.every(
sel => sel.elements && sel.elements.length > 0 && sel.elements.every(
/** @param {import('./element.js').default} el */
el => el.value === '&'
)
);
if (allAmpersandElements) {
noAmpersands = false;
noAmpersandCount++;
} else {
ampersandCount++;
}
}
}
}
const mixedAmpersands = ampersandCount > 0 && noAmpersandCount > 0 && !noAmpersands;
if (
(this.isRooted && ampersandCount > 0 && noAmpersandCount === 0 && noAmpersands)
|| !mixedAmpersands
) {
/** @type {RulesetLikeNode} */ (rules[0]).root = true;
}
return rules;
}
/** @param {string} name */
variable(name) {
if (this.rules) {
// assuming that there is only one rule at this point - that is how parser constructs the rule
return Ruleset.prototype.variable.call(this.rules[0], name);
}
}
find() {
if (this.rules) {
// assuming that there is only one rule at this point - that is how parser constructs the rule
return Ruleset.prototype.find.apply(this.rules[0], arguments);
}
}
rulesets() {
if (this.rules) {
// assuming that there is only one rule at this point - that is how parser constructs the rule
return Ruleset.prototype.rulesets.apply(this.rules[0]);
}
}
/**
* @param {EvalContext} context
* @param {CSSOutput} output
* @param {Node[]} rules
*/
outputRuleset(context, output, rules) {
const ruleCnt = rules.length;
let i;
context.tabLevel = (context.tabLevel | 0) + 1;
// Compressed
if (context.compress) {
output.add('{');
for (i = 0; i < ruleCnt; i++) {
rules[i].genCSS(context, output);
}
output.add('}');
context.tabLevel--;
return;
}
// Non-compressed
const tabSetStr = `\n${Array(context.tabLevel).join(' ')}`, tabRuleStr = `${tabSetStr} `;
if (!ruleCnt) {
output.add(` {${tabSetStr}}`);
} else {
output.add(` {${tabRuleStr}`);
rules[0].genCSS(context, output);
for (i = 1; i < ruleCnt; i++) {
output.add(tabRuleStr);
rules[i].genCSS(context, output);
}
output.add(`${tabSetStr}}`);
}
context.tabLevel--;
}
}
// Apply shared methods from NestableAtRulePrototype that AtRule doesn't override
const { evalFunction, evalTop, evalNested, permute, bubbleSelectors } = NestableAtRulePrototype;
Object.assign(AtRule.prototype, { evalFunction, evalTop, evalNested, permute, bubbleSelectors });
export default AtRule;