451 lines
12 KiB
JavaScript
451 lines
12 KiB
JavaScript
import postcssParse from "postcss/lib/parse";
|
|
import postcssLess from "postcss-less";
|
|
import postcssScssParse from "postcss-scss/lib/scss-parse";
|
|
import createError from "../common/parser-create-error.js";
|
|
import { parseFrontMatter } from "../main/front-matter/index.js";
|
|
import isObject from "../utilities/is-object.js";
|
|
import {
|
|
calculateLoc,
|
|
locEnd,
|
|
locStart,
|
|
replaceQuotesInInlineComments,
|
|
} from "./loc.js";
|
|
import parseMediaQuery from "./parse/parse-media-query.js";
|
|
import parseSelector from "./parse/parse-selector.js";
|
|
import parseValue from "./parse/parse-value.js";
|
|
import { addTypePrefix } from "./parse/utilities.js";
|
|
import { hasIgnorePragma, hasPragma } from "./pragma.js";
|
|
import isModuleRuleName from "./utilities/is-module-rule-name.js";
|
|
import isSCSSNestedPropertyNode from "./utilities/is-scss-nested-property-node.js";
|
|
|
|
const DEFAULT_SCSS_DIRECTIVE = /(\s*)(!default).*$/u;
|
|
const GLOBAL_SCSS_DIRECTIVE = /(\s*)(!global).*$/u;
|
|
|
|
function parseNestedCSS(node, options) {
|
|
if (isObject(node)) {
|
|
delete node.parent;
|
|
|
|
for (const key in node) {
|
|
parseNestedCSS(node[key], options);
|
|
}
|
|
|
|
if (!node.type) {
|
|
return node;
|
|
}
|
|
|
|
/* c8 ignore next */
|
|
node.raws ??= {};
|
|
|
|
// Custom properties looks like declarations
|
|
if (
|
|
node.type === "css-decl" &&
|
|
typeof node.prop === "string" &&
|
|
node.prop.startsWith("--") &&
|
|
typeof node.value === "string" &&
|
|
node.value.startsWith("{")
|
|
) {
|
|
let rules;
|
|
if (node.value.trimEnd().endsWith("}")) {
|
|
const textBefore = options.originalText.slice(
|
|
0,
|
|
node.source.start.offset,
|
|
);
|
|
const nodeText =
|
|
"a".repeat(node.prop.length) +
|
|
options.originalText.slice(
|
|
node.source.start.offset + node.prop.length,
|
|
node.source.end.offset,
|
|
);
|
|
const fakeContent = textBefore.replaceAll(/[^\n]/gu, " ") + nodeText;
|
|
let parse;
|
|
if (options.parser === "scss") {
|
|
parse = parseScss;
|
|
} else if (options.parser === "less") {
|
|
parse = parseLess;
|
|
} else {
|
|
parse = parseCss;
|
|
}
|
|
let ast;
|
|
try {
|
|
ast = parse(fakeContent, { ...options });
|
|
} catch {
|
|
// noop
|
|
}
|
|
if (ast?.nodes?.length === 1 && ast.nodes[0].type === "css-rule") {
|
|
rules = ast.nodes[0].nodes;
|
|
}
|
|
}
|
|
if (rules) {
|
|
node.value = {
|
|
type: "css-rule",
|
|
nodes: rules,
|
|
};
|
|
} else {
|
|
node.value = {
|
|
type: "value-unknown",
|
|
value: node.raws.value.raw,
|
|
};
|
|
}
|
|
return node;
|
|
}
|
|
|
|
let selector = "";
|
|
|
|
if (typeof node.selector === "string") {
|
|
selector = node.raws.selector
|
|
? (node.raws.selector.scss ?? node.raws.selector.raw)
|
|
: node.selector;
|
|
|
|
if (node.raws.between && node.raws.between.trim().length > 0) {
|
|
selector += node.raws.between;
|
|
}
|
|
|
|
node.raws.selector = selector;
|
|
}
|
|
|
|
let value = "";
|
|
|
|
if (typeof node.value === "string") {
|
|
value = node.raws.value
|
|
? (node.raws.value.scss ?? node.raws.value.raw)
|
|
: node.value;
|
|
|
|
node.raws.value = value.trim();
|
|
}
|
|
|
|
let params = "";
|
|
|
|
if (typeof node.params === "string") {
|
|
params = node.raws.params
|
|
? (node.raws.params.scss ?? node.raws.params.raw)
|
|
: node.params;
|
|
|
|
if (node.raws.afterName && node.raws.afterName.trim().length > 0) {
|
|
params = node.raws.afterName + params;
|
|
}
|
|
|
|
if (node.raws.between && node.raws.between.trim().length > 0) {
|
|
params = params + node.raws.between;
|
|
}
|
|
|
|
params = params.trim();
|
|
|
|
node.raws.params = params;
|
|
}
|
|
|
|
// Ignore LESS mixin declaration
|
|
if (selector.trim().length > 0) {
|
|
// TODO: confirm this code is dead
|
|
/* c8 ignore next 3 */
|
|
if (selector.startsWith("@") && selector.endsWith(":")) {
|
|
return node;
|
|
}
|
|
|
|
// TODO: confirm this code is dead
|
|
/* c8 ignore next 6 */
|
|
// Ignore LESS mixins
|
|
if (node.mixin) {
|
|
node.selector = parseValue(selector, options);
|
|
|
|
return node;
|
|
}
|
|
|
|
// Check on SCSS nested property
|
|
if (isSCSSNestedPropertyNode(node, options)) {
|
|
node.isSCSSNesterProperty = true;
|
|
}
|
|
|
|
node.selector = parseSelector(selector);
|
|
|
|
return node;
|
|
}
|
|
|
|
if (value.trim().length > 0) {
|
|
const defaultSCSSDirectiveIndex = value.match(DEFAULT_SCSS_DIRECTIVE);
|
|
|
|
if (defaultSCSSDirectiveIndex) {
|
|
value = value.slice(0, defaultSCSSDirectiveIndex.index);
|
|
node.scssDefault = true;
|
|
|
|
if (defaultSCSSDirectiveIndex[0].trim() !== "!default") {
|
|
node.raws.scssDefault = defaultSCSSDirectiveIndex[0];
|
|
}
|
|
}
|
|
|
|
const globalSCSSDirectiveIndex = value.match(GLOBAL_SCSS_DIRECTIVE);
|
|
|
|
if (globalSCSSDirectiveIndex) {
|
|
value = value.slice(0, globalSCSSDirectiveIndex.index);
|
|
node.scssGlobal = true;
|
|
|
|
if (globalSCSSDirectiveIndex[0].trim() !== "!global") {
|
|
node.raws.scssGlobal = globalSCSSDirectiveIndex[0];
|
|
}
|
|
}
|
|
|
|
if (value.startsWith("progid:")) {
|
|
return {
|
|
type: "value-unknown",
|
|
value,
|
|
};
|
|
}
|
|
|
|
node.value = parseValue(value, options);
|
|
}
|
|
|
|
if (
|
|
options.parser === "less" &&
|
|
node.type === "css-decl" &&
|
|
value.startsWith("extend(")
|
|
) {
|
|
// extend is missing
|
|
node.extend ||= node.raws.between === ":";
|
|
|
|
// `:extend()` is parsed as value
|
|
if (node.extend && !node.selector) {
|
|
delete node.value;
|
|
node.selector = parseSelector(value.slice("extend(".length, -1));
|
|
}
|
|
}
|
|
|
|
if (node.type === "css-atrule") {
|
|
if (options.parser === "less") {
|
|
// mixin
|
|
if (node.mixin) {
|
|
const source =
|
|
node.raws.identifier +
|
|
node.name +
|
|
node.raws.afterName +
|
|
node.raws.params;
|
|
node.selector = parseSelector(source);
|
|
delete node.params;
|
|
return node;
|
|
}
|
|
|
|
// function
|
|
if (node.function) {
|
|
return node;
|
|
}
|
|
}
|
|
|
|
// only css support custom-selector
|
|
if (options.parser === "css" && node.name === "custom-selector") {
|
|
const customSelector = node.params.match(/:--\S+\s+/u)[0].trim();
|
|
node.customSelector = customSelector;
|
|
node.selector = parseSelector(
|
|
node.params.slice(customSelector.length).trim(),
|
|
);
|
|
delete node.params;
|
|
return node;
|
|
}
|
|
|
|
if (options.parser === "less") {
|
|
// postcss-less doesn't recognize variables in some cases.
|
|
// `@color: blue;` is recognized fine, but the cases below aren't:
|
|
|
|
// `@color:blue;`
|
|
if (node.name.includes(":")) {
|
|
node.variable = true;
|
|
const parts = node.name.split(":");
|
|
node.name = parts[0];
|
|
let value = parts.slice(1).join(":");
|
|
|
|
// `@fooBackground:rgba(255, 255, 255, 1);`
|
|
if (node.params) {
|
|
value += node.params;
|
|
}
|
|
|
|
node.value = parseValue(value, options);
|
|
}
|
|
|
|
// `@color :blue;`
|
|
if (
|
|
!["page", "nest", "keyframes"].includes(node.name) &&
|
|
node.params?.[0] === ":"
|
|
) {
|
|
node.variable = true;
|
|
const text = node.params.slice(1);
|
|
if (text) {
|
|
node.value = parseValue(text, options);
|
|
}
|
|
node.raws.afterName += ":";
|
|
}
|
|
|
|
// Less variable
|
|
if (node.variable) {
|
|
delete node.params;
|
|
|
|
if (!node.value) {
|
|
delete node.value;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.type === "css-atrule" && params.length > 0) {
|
|
const { name } = node;
|
|
const lowercasedName = node.name.toLowerCase();
|
|
|
|
if (name === "warn" || name === "error") {
|
|
node.params = {
|
|
type: "media-unknown",
|
|
value: params,
|
|
};
|
|
|
|
return node;
|
|
}
|
|
|
|
if (name === "extend" || name === "nest") {
|
|
node.selector = parseSelector(params);
|
|
delete node.params;
|
|
|
|
return node;
|
|
}
|
|
|
|
if (name === "at-root") {
|
|
if (/^\(\s*(?:without|with)\s*:.+\)$/su.test(params)) {
|
|
node.params = parseValue(params, options);
|
|
} else {
|
|
node.selector = parseSelector(params);
|
|
delete node.params;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
if (isModuleRuleName(lowercasedName)) {
|
|
node.import = true;
|
|
delete node.filename;
|
|
node.params = parseValue(params, options);
|
|
return node;
|
|
}
|
|
|
|
if (
|
|
[
|
|
"namespace",
|
|
"supports",
|
|
"if",
|
|
"else",
|
|
"for",
|
|
"each",
|
|
"while",
|
|
"debug",
|
|
"mixin",
|
|
"include",
|
|
"function",
|
|
"return",
|
|
"define-mixin",
|
|
"add-mixin",
|
|
].includes(name)
|
|
) {
|
|
// Remove unnecessary spaces in SCSS variable arguments
|
|
// Move spaces after the `...`, so we can keep the range correct
|
|
params = params.replace(/(\$\S+?)(\s+)?\.{3}/u, "$1...$2");
|
|
// Remove unnecessary spaces before SCSS control, mixin and function directives
|
|
// Move spaces after the `(`, so we can keep the range correct
|
|
// Only match the first function call at the beginning, not nested ones
|
|
params = params.replace(/^(?!if)([^"'\s(]+)(\s+)\(/u, "$1($2");
|
|
|
|
node.value = parseValue(params, options);
|
|
delete node.params;
|
|
|
|
return node;
|
|
}
|
|
|
|
if (["media", "custom-media"].includes(lowercasedName)) {
|
|
if (params.includes("#{")) {
|
|
// Workaround for media at rule with scss interpolation
|
|
return {
|
|
type: "media-unknown",
|
|
value: params,
|
|
};
|
|
}
|
|
|
|
node.params = parseMediaQuery(params);
|
|
|
|
return node;
|
|
}
|
|
|
|
node.params = params;
|
|
|
|
return node;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function parseWithParser(parse, text, options) {
|
|
const { frontMatter, content: textToParse } = parseFrontMatter(text);
|
|
|
|
let result;
|
|
|
|
try {
|
|
result = parse(textToParse, {
|
|
// Prevent file access https://github.com/postcss/postcss/blob/4f4e2932fc97e2c117e1a4b15f0272ed551ed59d/lib/previous-map.js#L18
|
|
map: false,
|
|
});
|
|
} catch (/** @type {any} */ error) {
|
|
const { name, reason, line, column } = error;
|
|
/* c8 ignore 3 */
|
|
if (typeof line !== "number") {
|
|
throw error;
|
|
}
|
|
|
|
throw createError(`${name}: ${reason}`, {
|
|
loc: { start: { line, column } },
|
|
cause: error,
|
|
});
|
|
}
|
|
|
|
options.originalText = text;
|
|
result = parseNestedCSS(addTypePrefix(result, "css-"), options);
|
|
|
|
calculateLoc(result, text);
|
|
|
|
if (frontMatter) {
|
|
result.frontMatter = {
|
|
...frontMatter,
|
|
type: "front-matter",
|
|
source: {
|
|
startOffset: frontMatter.start.index,
|
|
endOffset: frontMatter.end.index,
|
|
},
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function parseCss(text, options = {}) {
|
|
return parseWithParser(postcssParse.default, text, options);
|
|
}
|
|
|
|
function parseLess(text, options = {}) {
|
|
return parseWithParser(
|
|
// Workaround for https://github.com/shellscape/postcss-less/issues/145
|
|
// See comments for `replaceQuotesInInlineComments` in `loc.js`.
|
|
(text) => postcssLess.parse(replaceQuotesInInlineComments(text)),
|
|
text,
|
|
options,
|
|
);
|
|
}
|
|
|
|
function parseScss(text, options = {}) {
|
|
return parseWithParser(postcssScssParse, text, options);
|
|
}
|
|
|
|
const postCssParser = {
|
|
astFormat: "postcss",
|
|
hasPragma,
|
|
hasIgnorePragma,
|
|
locStart,
|
|
locEnd,
|
|
};
|
|
|
|
export const css = { ...postCssParser, parse: parseCss };
|
|
export const less = { ...postCssParser, parse: parseLess };
|
|
export const scss = { ...postCssParser, parse: parseScss };
|