Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
cd packages/less && npm run typecheck && cd ../..
pnpm test
26 changes: 23 additions & 3 deletions packages/less/lib/less/tree/anonymous.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
// @ts-check
/** @import { EvalContext, CSSOutput, FileInfo, VisibilityInfo } from './node.js' */
import Node from './node.js';

class Anonymous extends Node {
get type() { return 'Anonymous'; }

/**
* @param {string | null} value
* @param {number} [index]
* @param {FileInfo} [currentFileInfo]
* @param {boolean} [mapLines]
* @param {boolean} [rulesetLike]
* @param {VisibilityInfo} [visibilityInfo]
*/
constructor(value, index, currentFileInfo, mapLines, rulesetLike, visibilityInfo) {
super();
this.value = value;
Expand All @@ -14,22 +24,32 @@ class Anonymous extends Node {
this.copyVisibilityInfo(visibilityInfo);
}

/** @returns {Anonymous} */
eval() {
return new Anonymous(this.value, this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo());
return new Anonymous(/** @type {string | null} */ (this.value), this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo());
}

/**
* @param {Node} other
* @returns {number | undefined}
*/
compare(other) {
return other.toCSS && this.toCSS() === other.toCSS() ? 0 : undefined;
return other.toCSS && this.toCSS(/** @type {EvalContext} */ ({})) === other.toCSS(/** @type {EvalContext} */ ({})) ? 0 : undefined;
}

/** @returns {boolean} */
isRulesetLike() {
return this.rulesetLike;
}

/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
this.nodeVisible = Boolean(this.value);
if (this.nodeVisible) {
output.add(this.value, this._fileInfo, this._index, this.mapLines);
output.add(/** @type {string} */ (this.value), this._fileInfo, this._index, this.mapLines);
}
}
}
Expand Down
27 changes: 21 additions & 6 deletions packages/less/lib/less/tree/assignment.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor } from './node.js' */
import Node from './node.js';

class Assignment extends Node {
get type() { return 'Assignment'; }

/**
* @param {string} key
* @param {Node} val
*/
constructor(key, val) {
super();
this.key = key;
this.value = val;
}

/** @param {TreeVisitor} visitor */
accept(visitor) {
this.value = visitor.visit(this.value);
this.value = visitor.visit(/** @type {Node} */ (this.value));
}

/**
* @param {EvalContext} context
* @returns {Assignment}
*/
eval(context) {
if (this.value.eval) {
return new Assignment(this.key, this.value.eval(context));
if (/** @type {Node} */ (this.value).eval) {
return new Assignment(this.key, /** @type {Node} */ (this.value).eval(context));
}
return this;
}

/**
* @param {EvalContext} context
* @param {CSSOutput} output
*/
genCSS(context, output) {
output.add(`${this.key}=`);
if (this.value.genCSS) {
this.value.genCSS(context, output);
if (/** @type {Node} */ (this.value).genCSS) {
/** @type {Node} */ (this.value).genCSS(context, output);
} else {
output.add(this.value);
output.add(/** @type {string} */ (/** @type {unknown} */ (this.value)));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/less/lib/less/tree/atrule-syntax.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
export const MediaSyntaxOptions = {
queryInParens: true
};
Expand Down
119 changes: 93 additions & 26 deletions packages/less/lib/less/tree/atrule.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
// @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,
Expand All @@ -22,69 +48,88 @@ class AtRule extends Node {
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' && rule.rules) allRulesetDeclarations = allRulesetDeclarations && this.declarationsBlock(rule.rules, true);
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 = rules[0].rules ? rules[0].rules : rules;
this.declarations = /** @type {RulesetLikeNode} */ (rules[0]).rules ? /** @type {RulesetLikeNode} */ (rules[0]).rules : rules;
} else {
this.rules = rules;
}
} else {
const allDeclarations = this.declarationsBlock(rules.rules);
const allDeclarations = this.declarationsBlock(/** @type {Node[]} */ (rules.rules));

if (allDeclarations && !isRooted && !value) {
this.simpleBlock = true;
this.declarations = rules.rules;
} else {
this.rules = [rules];
this.rules[0].selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
/** @type {RulesetLikeNode} */ (this.rules[0]).selectors = (new Selector([], null, null, index, currentFileInfo)).createEmptySelectors();
}
}
if (!this.simpleBlock) {
for (i = 0; i < this.rules.length; i++) {
this.rules[i].allowImports = true;
/** @type {RulesetLikeNode} */ (this.rules[i]).allowImports = true;
}
}
this.setParent(selectors, this);
this.setParent(this.rules, this);
this.setParent(selectors, /** @type {Node} */ (/** @type {unknown} */ (this)));
this.setParent(this.rules, /** @type {Node} */ (/** @type {unknown} */ (this)));
Comment thread
matthew-dean marked this conversation as resolved.
}
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 (node) { return (node.type === 'Declaration' || node.type === 'Comment') && !node.merge}).length === rules.length;
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 (node) { return (node.type === 'Declaration' || node.type === 'Comment'); }).length === rules.length;
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 (node) { return (node.type === 'Keyword' || node.type === 'Comment'); }).length === rules.length;
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;

Expand All @@ -94,34 +139,43 @@ class AtRule extends Node {
this.declarations = visitor.visitArray(declarations);
}
if (value) {
this.value = visitor.visit(value);
this.value = visitor.visit(/** @type {Node} */ (value));
}
}

/** @override @returns {boolean} */
isRulesetLike() {
return this.rules || !this.isCharset();
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(this.name, this.fileInfo(), this.getIndex());
output.add(/** @type {string} */ (this.name), this.fileInfo(), this.getIndex());
if (value) {
output.add(' ');
value.genCSS(context, output);
/** @type {Node} */ (value).genCSS(context, output);
}
if (this.simpleBlock) {
this.outputRuleset(context, output, this.declarations);
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;

Expand All @@ -134,31 +188,36 @@ class AtRule extends Node {
context.mediaBlocks = [];

if (value) {
value = value.eval(context);
value = /** @type {Node} */ (value).eval(context);
}

if (rules) {
rules = this.evalRoot(context, rules);
}
if (Array.isArray(rules) && rules[0].rules && Array.isArray(rules[0].rules) && rules[0].rules.length) {
const allMergeableDeclarations = this.declarationsBlock(rules[0].rules, true);
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(rules[0].rules);
rules = rules[0].rules;
rules.forEach(rule => rule.merge = false);
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) {
rules[0].functionRegistry = context.frames[0].functionRegistry.inherit();
rules = rules.map(function (rule) { return rule.eval(context); });
/** @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 new AtRule(this.name, value, rules, this.getIndex(), this.fileInfo(), this.debugInfo, this.isRooted, this.visibilityInfo());
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;
Expand All @@ -168,10 +227,11 @@ class AtRule extends Node {
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 = context.frames[index];
const frame = /** @type {RulesetLikeNode} */ (context.frames[index]);
if (
frame.type === 'Ruleset' &&
frame.rules &&
Expand All @@ -184,6 +244,7 @@ class AtRule extends Node {
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 === '&'
)
);
Expand All @@ -202,11 +263,12 @@ class AtRule extends Node {
(this.isRooted && ampersandCount > 0 && noAmpersandCount === 0 && noAmpersands)
|| !mixedAmpersands
) {
rules[0].root = true;
/** @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
Expand All @@ -228,6 +290,11 @@ class AtRule extends Node {
}
}

/**
* @param {EvalContext} context
* @param {CSSOutput} output
* @param {Node[]} rules
*/
outputRuleset(context, output, rules) {
const ruleCnt = rules.length;
let i;
Expand Down
Loading