Files
test-repo/src/language-css/parser-postcss.js
T
fisker Cheung 17377f3082 Split css parsers (#7932)
* Split css parsers

* pass function to parseWithParser

* Fix plugins parser

* Fix coverage fails

* coverage ignore

* Update project coverage target to 95%
2020-04-03 12:41:48 +08:00

626 lines
15 KiB
JavaScript

"use strict";
const createError = require("../common/parser-create-error");
const parseFrontMatter = require("../utils/front-matter");
const { hasPragma } = require("./pragma");
const { isLessParser, isSCSS, isSCSSNestedPropertyNode } = require("./utils");
const { calculateLoc, replaceQuotesInInlineComments } = require("./loc");
function parseValueNodes(nodes) {
let parenGroup = {
open: null,
close: null,
groups: [],
type: "paren_group",
};
const parenGroupStack = [parenGroup];
const rootParenGroup = parenGroup;
let commaGroup = {
groups: [],
type: "comma_group",
};
const commaGroupStack = [commaGroup];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
const isUnquotedDataURLCall =
node.type === "func" &&
node.value === "url" &&
node.group &&
node.group.groups &&
node.group.groups[0] &&
node.group.groups[0].groups &&
node.group.groups[0].groups.length > 2 &&
node.group.groups[0].groups[0].type === "word" &&
node.group.groups[0].groups[0].value === "data" &&
node.group.groups[0].groups[1].type === "colon" &&
node.group.groups[0].groups[1].value === ":";
if (isUnquotedDataURLCall) {
node.group.groups = [stringifyGroup(node)];
}
if (node.type === "paren" && node.value === "(") {
parenGroup = {
open: node,
close: null,
groups: [],
type: "paren_group",
};
parenGroupStack.push(parenGroup);
commaGroup = {
groups: [],
type: "comma_group",
};
commaGroupStack.push(commaGroup);
} else if (node.type === "paren" && node.value === ")") {
if (commaGroup.groups.length) {
parenGroup.groups.push(commaGroup);
}
parenGroup.close = node;
if (commaGroupStack.length === 1) {
throw new Error("Unbalanced parenthesis");
}
commaGroupStack.pop();
commaGroup = commaGroupStack[commaGroupStack.length - 1];
commaGroup.groups.push(parenGroup);
parenGroupStack.pop();
parenGroup = parenGroupStack[parenGroupStack.length - 1];
} else if (node.type === "comma") {
parenGroup.groups.push(commaGroup);
commaGroup = {
groups: [],
type: "comma_group",
};
commaGroupStack[commaGroupStack.length - 1] = commaGroup;
} else {
commaGroup.groups.push(node);
}
}
if (commaGroup.groups.length > 0) {
parenGroup.groups.push(commaGroup);
}
return rootParenGroup;
}
function stringifyGroup(node) {
if (node.group) {
return stringifyGroup(node.group);
}
if (node.groups) {
return node.groups.reduce((previousValue, currentValue, index) => {
return (
previousValue +
stringifyGroup(currentValue) +
(currentValue.type === "comma_group" && index !== node.groups.length - 1
? ","
: "")
);
}, "");
}
const before = node.raws && node.raws.before ? node.raws.before : "";
const value = node.value ? node.value : "";
const unit = node.unit ? node.unit : "";
const after = node.raws && node.raws.after ? node.raws.after : "";
return before + value + unit + after;
}
function flattenGroups(node) {
if (
node.type === "paren_group" &&
!node.open &&
!node.close &&
node.groups.length === 1
) {
return flattenGroups(node.groups[0]);
}
if (node.type === "comma_group" && node.groups.length === 1) {
return flattenGroups(node.groups[0]);
}
if (node.type === "paren_group" || node.type === "comma_group") {
return { ...node, groups: node.groups.map(flattenGroups) };
}
return node;
}
function addTypePrefix(node, prefix) {
if (node && typeof node === "object") {
delete node.parent;
for (const key in node) {
addTypePrefix(node[key], prefix);
if (key === "type" && typeof node[key] === "string") {
if (!node[key].startsWith(prefix)) {
node[key] = prefix + node[key];
}
}
}
}
return node;
}
function addMissingType(node) {
if (node && typeof node === "object") {
delete node.parent;
for (const key in node) {
addMissingType(node[key]);
}
if (!Array.isArray(node) && node.value && !node.type) {
node.type = "unknown";
}
}
return node;
}
function parseNestedValue(node) {
if (node && typeof node === "object") {
delete node.parent;
for (const key in node) {
parseNestedValue(node[key]);
if (key === "nodes") {
node.group = flattenGroups(parseValueNodes(node[key]));
delete node[key];
}
}
}
return node;
}
function parseValue(value) {
const valueParser = require("postcss-values-parser");
let result = null;
try {
result = valueParser(value, { loose: true }).parse();
} catch (e) {
return {
type: "value-unknown",
value,
};
}
const parsedResult = parseNestedValue(result);
return addTypePrefix(parsedResult, "value-");
}
function parseSelector(selector) {
// If there's a comment inside of a selector, the parser tries to parse
// the content of the comment as selectors which turns it into complete
// garbage. Better to print the whole selector as-is and not try to parse
// and reformat it.
if (/\/\/|\/\*/.test(selector)) {
return {
type: "selector-unknown",
value: selector.trim(),
};
}
const selectorParser = require("postcss-selector-parser");
let result = null;
try {
selectorParser((result_) => {
result = result_;
}).process(selector);
} catch (e) {
// Fail silently. It's better to print it as is than to try and parse it
// Note: A common failure is for SCSS nested properties. `background:
// none { color: red; }` is parsed as a NestedDeclaration by
// postcss-scss, while `background: { color: red; }` is parsed as a Rule
// with a selector ending with a colon. See:
// https://github.com/postcss/postcss-scss/issues/39
return {
type: "selector-unknown",
value: selector,
};
}
return addTypePrefix(result, "selector-");
}
function parseMediaQuery(params) {
const mediaParser = require("postcss-media-query-parser").default;
let result = null;
try {
result = mediaParser(params);
} catch (e) {
// Ignore bad media queries
return {
type: "selector-unknown",
value: params,
};
}
return addTypePrefix(addMissingType(result), "media-");
}
const DEFAULT_SCSS_DIRECTIVE = /(\s*?)(!default).*$/;
const GLOBAL_SCSS_DIRECTIVE = /(\s*?)(!global).*$/;
function parseNestedCSS(node, options) {
if (node && typeof node === "object") {
delete node.parent;
for (const key in node) {
parseNestedCSS(node[key], options);
}
if (!node.type) {
return node;
}
if (!node.raws) {
node.raws = {};
}
let selector = "";
if (typeof node.selector === "string") {
selector = node.raws.selector
? node.raws.selector.scss
? 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.scss
: node.raws.value.raw
: node.value;
value = value.trim();
node.raws.value = value;
}
let params = "";
if (typeof node.params === "string") {
params = node.raws.params
? node.raws.params.scss
? 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) {
if (selector.startsWith("@") && selector.endsWith(":")) {
return node;
}
// Ignore LESS mixins
if (node.mixin) {
node.selector = parseValue(selector);
return node;
}
// Check on SCSS nested property
if (isSCSSNestedPropertyNode(node)) {
node.isSCSSNesterProperty = true;
}
node.selector = parseSelector(selector);
return node;
}
if (value.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);
}
if (node.type === "css-atrule") {
if (isLessParser(options)) {
// 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+/)[0].trim();
node.customSelector = customSelector;
node.selector = parseSelector(
node.params.slice(customSelector.length).trim()
);
delete node.params;
return node;
}
if (isLessParser(options)) {
// Whitespace between variable and colon
if (node.name.includes(":") && !node.params) {
node.variable = true;
const parts = node.name.split(":");
node.name = parts[0];
node.value = parseValue(parts.slice(1).join(":"));
}
// Missing whitespace between variable and colon
if (
!["page", "nest"].includes(node.name) &&
node.params &&
node.params[0] === ":"
) {
node.variable = true;
node.value = parseValue(node.params.slice(1));
}
// Less variable
if (node.variable) {
delete node.params;
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*:[\s\S]+\)$/.test(params)) {
node.params = parseValue(params);
} else {
node.selector = parseSelector(params);
delete node.params;
}
return node;
}
if (lowercasedName === "import") {
node.import = true;
delete node.filename;
node.params = parseValue(params);
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
params = params.replace(/(\$\S+?)\s+?\.\.\./, "$1...");
// Remove unnecessary spaces before SCSS control, mixin and function directives
params = params.replace(/^(?!if)(\S+)\s+\(/, "$1(");
node.value = parseValue(params);
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 parsed = parseFrontMatter(text);
const { frontMatter } = parsed;
text = parsed.content;
let result;
try {
result = parse(text);
} catch (e) {
if (typeof e.line !== "number") {
throw e;
}
throw createError("(postcss) " + e.name + " " + e.reason, { start: e });
}
result = parseNestedCSS(addTypePrefix(result, "css-"), options);
calculateLoc(result, text);
if (frontMatter) {
result.nodes.unshift(frontMatter);
}
return result;
}
// TODO: make this only work on css
function parseCss(text, parsers, options) {
const isSCSSParser = isSCSS(options.parser, text);
const parseFunctions = isSCSSParser
? [parseScss, parseLess]
: [parseLess, parseScss];
let error;
for (const parse of parseFunctions) {
try {
return parse(text, parsers, options);
} catch (parseError) {
error = error || parseError;
}
}
/* istanbul ignore next */
if (error) {
throw error;
}
}
function parseLess(text, parsers, options) {
const lessParser = require("postcss-less");
return parseWithParser(
// Workaround for https://github.com/shellscape/postcss-less/issues/145
// See comments for `replaceQuotesInInlineComments` in `loc.js`.
(text) => lessParser.parse(replaceQuotesInInlineComments(text)),
text,
options
);
}
function parseScss(text, parsers, options) {
const { parse } = require("postcss-scss");
return parseWithParser(parse, text, options);
}
const postCssParser = {
astFormat: "postcss",
hasPragma,
locStart(node) {
if (node.source) {
return node.source.startOffset;
}
/* istanbul ignore next */
return null;
},
locEnd(node) {
if (node.source) {
return node.source.endOffset;
}
return null;
},
};
// Export as a plugin so we can reuse the same bundle for UMD loading
module.exports = {
parsers: {
css: {
...postCssParser,
parse: parseCss,
},
less: {
...postCssParser,
parse: parseLess,
},
scss: {
...postCssParser,
parse: parseScss,
},
},
};