chocola 2.0.0-next.4 → 2.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/compiler/chalk.js +32 -0
- package/compiler/component-processor.js +110 -330
- package/compiler/dom-processor.js +8 -22
- package/compiler/index.js +84 -80
- package/compiler/pipeline.js +40 -13
- package/compiler/utils.js +52 -81
- package/dev/index.js +1 -1
- package/package.json +4 -9
- package/parser/component.js +129 -0
- package/parser/context.js +29 -0
- package/parser/css.js +96 -0
- package/parser/index.js +5 -0
- package/parser/template.js +121 -0
- package/parser/utils.js +13 -0
- package/utils.js +15 -0
package/parser/css.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
function hasCombinator(sel) {
|
|
2
|
+
let depth = 0;
|
|
3
|
+
for (const ch of sel) {
|
|
4
|
+
if (ch === '[') depth++;
|
|
5
|
+
else if (ch === ']') depth--;
|
|
6
|
+
else if (depth === 0 && (ch === ' ' || ch === '>' || ch === '+' || ch === '~')) return true;
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function scopeSelector(sel, cssId) {
|
|
12
|
+
const s = sel.trim();
|
|
13
|
+
if (s.startsWith("." + cssId)) return s;
|
|
14
|
+
if (hasCombinator(s)) return `.${cssId} ${s}`;
|
|
15
|
+
return `.${cssId}${s}, .${cssId} ${s}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function scopeCss(cssString, cssId) {
|
|
19
|
+
const keyframeRegex = /@(-webkit-)?keyframes\s+(\S+)/gi;
|
|
20
|
+
const animNames = new Set();
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = keyframeRegex.exec(cssString))) {
|
|
23
|
+
animNames.add(match[2]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (animNames.size > 0) {
|
|
27
|
+
const sorted = [...animNames].sort((a, b) => b.length - a.length);
|
|
28
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
29
|
+
for (const name of sorted) {
|
|
30
|
+
cssString = cssString.replace(
|
|
31
|
+
new RegExp(`(@(?:-webkit-)?keyframes\\s+)${esc(name)}`, "gi"),
|
|
32
|
+
`$1${name}-${cssId}`
|
|
33
|
+
);
|
|
34
|
+
cssString = cssString.replace(
|
|
35
|
+
new RegExp(`((?:animation|animation-name)\\s*:\\s*[^;{]*?)\\b${esc(name)}\\b`, "gi"),
|
|
36
|
+
`$1${name}-${cssId}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findMatchingBrace(s, openIndex) {
|
|
42
|
+
let depth = 0;
|
|
43
|
+
for (let i = openIndex; i < s.length; i++) {
|
|
44
|
+
const ch = s[i];
|
|
45
|
+
if (ch === '{') depth++;
|
|
46
|
+
else if (ch === '}') {
|
|
47
|
+
depth--;
|
|
48
|
+
if (depth === 0) return i;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return s.length - 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const scopeInnerAtRules = new Set(["@media", "@supports", "@container", "@layer"]);
|
|
55
|
+
|
|
56
|
+
function processBlock(str) {
|
|
57
|
+
str = str.replaceAll(":root", `.${cssId}`);
|
|
58
|
+
|
|
59
|
+
let out = "";
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < str.length) {
|
|
62
|
+
const braceIndex = str.indexOf('{', i);
|
|
63
|
+
if (braceIndex === -1) {
|
|
64
|
+
out += str.slice(i);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
const header = str.substring(i, braceIndex);
|
|
68
|
+
const endBrace = findMatchingBrace(str, braceIndex);
|
|
69
|
+
const inner = str.substring(braceIndex + 1, endBrace);
|
|
70
|
+
|
|
71
|
+
if (header.trim().startsWith("@")) {
|
|
72
|
+
const atKeyword = header.trim().split(/\s+/)[0].toLowerCase();
|
|
73
|
+
if (scopeInnerAtRules.has(atKeyword)) {
|
|
74
|
+
out += header + "{" + processBlock(inner) + "}";
|
|
75
|
+
} else {
|
|
76
|
+
out += header + "{" + inner + "}";
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
const innerScoped = processBlock(inner);
|
|
80
|
+
const selectors = header
|
|
81
|
+
.split(",")
|
|
82
|
+
.map(s => s.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
const scopedHeader = selectors.length > 0
|
|
85
|
+
? selectors.map(sel => scopeSelector(sel, cssId)).join(", ")
|
|
86
|
+
: header;
|
|
87
|
+
out += scopedHeader + "{" + innerScoped + "}";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
i = endBrace + 1;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return processBlock(cssString);
|
|
96
|
+
}
|
package/parser/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { extractPropsDefaults, extractRuntime, extractTopLevelFunctions } from "./component.js";
|
|
2
|
+
export { extractContextFromElement, hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
|
|
3
|
+
export { reservedAttrs, getLineNumber, validateChainStructure, applyConditionalToElement, interpolateNode } from "./template.js";
|
|
4
|
+
export { scopeCss } from "./css.js";
|
|
5
|
+
export { compileExpression } from "./utils.js";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { throwError } from "../compiler/utils.js";
|
|
2
|
+
import { compileExpression } from "./utils.js";
|
|
3
|
+
import { hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
|
|
4
|
+
|
|
5
|
+
export const reservedAttrs = ["if", "del:if", "elif", "else"];
|
|
6
|
+
|
|
7
|
+
export function getLineNumber(sourceContent, parentContent, idxInParent) {
|
|
8
|
+
if (sourceContent === parentContent) {
|
|
9
|
+
return parentContent.substring(0, idxInParent).split("\n").length;
|
|
10
|
+
}
|
|
11
|
+
const templateStartMatch = sourceContent.match(/<template[^>]*>/);
|
|
12
|
+
if (templateStartMatch) {
|
|
13
|
+
const contentStart = templateStartMatch.index + templateStartMatch[0].length;
|
|
14
|
+
const beforeContent = sourceContent.substring(0, contentStart);
|
|
15
|
+
const linesInBefore = beforeContent.split("\n").length;
|
|
16
|
+
const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
|
|
17
|
+
return linesInBefore + linesInTemplate - 1;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
|
|
23
|
+
const children = [...parent.children];
|
|
24
|
+
let chainActive = false;
|
|
25
|
+
|
|
26
|
+
for (const child of children) {
|
|
27
|
+
const hasIf = child.hasAttribute("if");
|
|
28
|
+
const hasDelIf = hasDelIfAttr(child);
|
|
29
|
+
const hasElif = child.hasAttribute("elif");
|
|
30
|
+
const hasElse = child.hasAttribute("else");
|
|
31
|
+
|
|
32
|
+
if (hasElif || hasElse) {
|
|
33
|
+
if (!chainActive) {
|
|
34
|
+
const tag = child.tagName.toLowerCase();
|
|
35
|
+
const attr = hasElif ? "elif" : "else";
|
|
36
|
+
let loc = sourceFile;
|
|
37
|
+
if (sourceContent && parentContent) {
|
|
38
|
+
const idx = parentContent.indexOf(child.outerHTML);
|
|
39
|
+
if (idx !== -1) {
|
|
40
|
+
const lineNum = getLineNumber(sourceContent, parentContent, idx);
|
|
41
|
+
if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throwError(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
|
|
45
|
+
}
|
|
46
|
+
if (hasElse) {
|
|
47
|
+
chainActive = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (hasIf || hasDelIf) {
|
|
52
|
+
chainActive = true;
|
|
53
|
+
} else if (!hasElif && !hasElse) {
|
|
54
|
+
chainActive = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
validateChainStructure(child, sourceFile, sourceContent, parentContent);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
|
|
62
|
+
if (hasIf) {
|
|
63
|
+
const expr = child.getAttribute("if").slice(1, -1);
|
|
64
|
+
const fn = compileExpression(expr, true);
|
|
65
|
+
condChain.active = true;
|
|
66
|
+
if (fn(ctxProxy)) {
|
|
67
|
+
condChain.rendered = true;
|
|
68
|
+
} else {
|
|
69
|
+
child.style.display = "none";
|
|
70
|
+
condChain.rendered = false;
|
|
71
|
+
}
|
|
72
|
+
child.removeAttribute("if");
|
|
73
|
+
} else if (hasDelIf) {
|
|
74
|
+
const expr = getDelIfAttr(child).slice(1, -1);
|
|
75
|
+
const fn = compileExpression(expr, true);
|
|
76
|
+
condChain.active = true;
|
|
77
|
+
if (fn(ctxProxy)) {
|
|
78
|
+
condChain.rendered = true;
|
|
79
|
+
} else {
|
|
80
|
+
child.remove();
|
|
81
|
+
condChain.rendered = false;
|
|
82
|
+
}
|
|
83
|
+
removeDelIfAttr(child);
|
|
84
|
+
} else if (hasElif) {
|
|
85
|
+
const expr = child.getAttribute("elif").slice(1, -1);
|
|
86
|
+
const fn = compileExpression(expr, true);
|
|
87
|
+
if (fn(ctxProxy)) {
|
|
88
|
+
condChain.rendered = true;
|
|
89
|
+
} else {
|
|
90
|
+
child.remove();
|
|
91
|
+
}
|
|
92
|
+
child.removeAttribute("elif");
|
|
93
|
+
} else if (hasElse) {
|
|
94
|
+
condChain.rendered = true;
|
|
95
|
+
condChain.active = false;
|
|
96
|
+
child.removeAttribute("else");
|
|
97
|
+
} else {
|
|
98
|
+
condChain.active = false;
|
|
99
|
+
condChain.rendered = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function interpolateNode(root, ctxProxy) {
|
|
104
|
+
const stack = [root];
|
|
105
|
+
while (stack.length) {
|
|
106
|
+
const node = stack.pop();
|
|
107
|
+
if (node.nodeType === 3) {
|
|
108
|
+
node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
|
|
109
|
+
try {
|
|
110
|
+
return compileExpression(expr, true)(ctxProxy);
|
|
111
|
+
} catch {
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
} else {
|
|
116
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
117
|
+
stack.push(node.childNodes[i]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
package/parser/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const compiledCache = new Map();
|
|
2
|
+
|
|
3
|
+
export function compileExpression(expr, useCtx) {
|
|
4
|
+
const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
|
|
5
|
+
let fn = compiledCache.get(key);
|
|
6
|
+
if (!fn) {
|
|
7
|
+
fn = useCtx
|
|
8
|
+
? new Function("ctx", `with(ctx) { return (${expr}); }`)
|
|
9
|
+
: new Function(`"use strict"; return (${expr})`);
|
|
10
|
+
compiledCache.set(key, fn);
|
|
11
|
+
}
|
|
12
|
+
return fn;
|
|
13
|
+
}
|
package/utils.js
CHANGED
|
@@ -9,4 +9,19 @@ export async function getChocolaConfig(__rootdir) {
|
|
|
9
9
|
} catch(err) {
|
|
10
10
|
throwError("An error occurred while fetching the Chocola config file:\n" + err);
|
|
11
11
|
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LBRACE_PH = "_%%CHOCOLA-LBRACE%%_";
|
|
15
|
+
const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
|
|
16
|
+
|
|
17
|
+
export function protectCurlyBraces(html) {
|
|
18
|
+
return html
|
|
19
|
+
.replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
|
|
20
|
+
.replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function restoreCurlyBraces(html) {
|
|
24
|
+
return html
|
|
25
|
+
.replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
|
|
26
|
+
.replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
|
|
12
27
|
}
|