Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tools/sass-variable-explainer/parse.ts
6438 views
1
// our attempt at factoring out a clean parser
2
// that works in Deno and on the web
3
4
import { walk } from "./ast-utils.ts";
5
let counter = 1;
6
export const makeParserModule = (
7
parse: any,
8
) => {
9
return {
10
getSassAst: (contents: string) => {
11
// scss-parser doesn't support the `...` operator and it breaks their parser oO, so we remove it.
12
// our analysis doesn't need to know about it.
13
contents = contents.replaceAll("...", "_dot_dot_dot");
14
// it also doesn't like some valid ways to do '@import url'
15
contents = contents.replaceAll("@import url", "//@import url");
16
17
// https://github.com/quarto-dev/quarto-cli/issues/11121
18
// It also doesn't like empty rules
19
20
// that long character class rule matches everything in \s except for \n
21
// using the explanation from regex101.com as a reference
22
contents = contents.replaceAll(
23
/^[\t\f\v \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]*([^\n{]+)([{])\s*([}])$/mg,
24
"$1$2 /* empty rule */ $3",
25
);
26
27
// it also really doesn't like statements that don't end in a semicolon
28
// so, in case you are reading this code to understand why the parser is failing,
29
// ensure that your SCSS has semicolons at the end of every statement.
30
// we try to work around this by adding semicolons at the end of declarations that don't have them
31
contents = contents.replaceAll(
32
/^(?!(?=\/\/)|(?=\s*[@#$]))(.*[^}/\s\n;])([\s\n]*)}(\n|$)/mg,
33
"$1;$2}$3",
34
);
35
// It also doesn't like values that follow a colon directly without a space
36
contents = contents.replaceAll(
37
/(^\s*[A-Za-z0-9-]+):([^ \n])/mg,
38
"$1: $2",
39
);
40
41
// This is relatively painful, because unfortunately the error message of scss-parser
42
// is not helpful.
43
44
// Create an AST from a string of SCSS
45
// and convert it to a plain JSON object
46
const ast = JSON.parse(JSON.stringify(parse(contents)));
47
48
if (!(ast.type === "stylesheet")) {
49
throw new Error("Expected AST to have type 'stylesheet'");
50
}
51
if (!Array.isArray(ast.value)) {
52
throw new Error("Expected AST to have an array value");
53
}
54
55
// rename 'value' to 'children'
56
// because they also use 'value' for the value of a property
57
58
// this is the only place we'll use 'walk' instead of the
59
// more explicit 'mapDeep' and 'filterValuesDeep' functions
60
// below, which will then assume 'children'
61
62
walk(ast, (node: any) => {
63
if (Array.isArray(node)) {
64
return true;
65
}
66
if (["value", "identifier", "operator"].includes(node?.type)) {
67
return true;
68
}
69
if (!node?.value || !Array.isArray(node.value)) {
70
return true;
71
}
72
node.children = node.value;
73
delete node.value;
74
return true;
75
});
76
77
return ast;
78
},
79
};
80
};
81
82