chocola 2.0.0-next.5 → 2.0.0-next.7
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 +4 -5
- package/compiler/chalk.js +32 -32
- package/compiler/component-processor.js +459 -667
- package/compiler/dom-processor.js +3 -20
- package/compiler/index.js +13 -4
- package/compiler/runtime-generator.js +22 -5
- package/compiler/utils.js +8 -35
- package/package.json +1 -1
- 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 +120 -0
- package/parser/utils.js +13 -0
- package/runtime/index.js +309 -0
- package/utils.js +15 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { JSDOM } from "jsdom";
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import {
|
|
4
|
+
import { protectCurlyBraces, restoreCurlyBraces } from "../utils.js";
|
|
5
|
+
import { extractContextFromElement } from "../parser/context.js";
|
|
6
|
+
import { throwError } from "./utils.js";
|
|
5
7
|
import { readMyFile } from "./fs.js";
|
|
6
8
|
|
|
7
9
|
export function createDOM(srcIndexContent) {
|
|
@@ -20,24 +22,6 @@ export function getAppElements(appContainer) {
|
|
|
20
22
|
return Array.from(appContainer.querySelectorAll("*"));
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export function extractContextFromElement(element) {
|
|
24
|
-
const ctx = {};
|
|
25
|
-
for (const attr of element.attributes) {
|
|
26
|
-
const key = attr.name;
|
|
27
|
-
const val = attr.value;
|
|
28
|
-
if (!val.includes("{")) { ctx[key] = val; continue; }
|
|
29
|
-
const matches = [...val.matchAll(/\{([^}]+)\}/g)];
|
|
30
|
-
if (matches.length === 1 && matches[0][0] === val) {
|
|
31
|
-
try {
|
|
32
|
-
ctx[key] = compileExpression(matches[0][1], false)();
|
|
33
|
-
continue;
|
|
34
|
-
} catch {}
|
|
35
|
-
}
|
|
36
|
-
ctx[key] = val;
|
|
37
|
-
}
|
|
38
|
-
return ctx;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
25
|
export async function serializeDOM(dom) {
|
|
42
26
|
return restoreCurlyBraces(dom.serialize());
|
|
43
27
|
}
|
|
@@ -70,7 +54,6 @@ export function getScriptElements(doc) {
|
|
|
70
54
|
|
|
71
55
|
export function appendRuntimeScript(doc, filename) {
|
|
72
56
|
const runtimeScriptEl = doc.createElement("script");
|
|
73
|
-
runtimeScriptEl.type = "module";
|
|
74
57
|
runtimeScriptEl.src = "./" + filename;
|
|
75
58
|
doc.body.appendChild(runtimeScriptEl);
|
|
76
59
|
}
|
package/compiler/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
2
3
|
import chalk from "./chalk.js";
|
|
3
4
|
import { loadConfig, resolvePaths } from "./config.js";
|
|
4
5
|
import {
|
|
@@ -15,7 +16,8 @@ import {
|
|
|
15
16
|
} from "./dom-processor.js";
|
|
16
17
|
import { processAllComponents } from "./component-processor.js";
|
|
17
18
|
import { generateRuntimeScript } from "./runtime-generator.js";
|
|
18
|
-
import { genRandomId, throwError
|
|
19
|
+
import { genRandomId, throwError } from "./utils.js";
|
|
20
|
+
import { compileExpression, hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "../parser/index.js";
|
|
19
21
|
import {
|
|
20
22
|
copyStaticDir,
|
|
21
23
|
getComponents,
|
|
@@ -207,8 +209,9 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
207
209
|
processPageConditionals(appContainer);
|
|
208
210
|
|
|
209
211
|
const appElements = getAppElements(appContainer);
|
|
210
|
-
const { runtimeScript, scopesCss } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
|
|
211
|
-
const
|
|
212
|
+
const { runtimeScript, scopesCss, hashMap, csrClasses } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
|
|
213
|
+
const csrSource = await fs.readFile(new URL("../runtime/index.js", import.meta.url), "utf-8");
|
|
214
|
+
const runtimeFilenames = await generateRuntimeScript(runtimeScript, paths.outDir, csrSource, csrClasses);
|
|
212
215
|
await processAssets(doc, rootDir, config.srcDir, paths.outDir);
|
|
213
216
|
|
|
214
217
|
if (scopesCss) {
|
|
@@ -217,7 +220,9 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
217
220
|
appendStylesheetLink(doc, fileName);
|
|
218
221
|
};
|
|
219
222
|
|
|
220
|
-
|
|
223
|
+
for (const name of runtimeFilenames) {
|
|
224
|
+
appendRuntimeScript(doc, name);
|
|
225
|
+
}
|
|
221
226
|
const html = await serializeDOM(dom);
|
|
222
227
|
await writeHTMLOutput(html, paths.outDir);
|
|
223
228
|
|
|
@@ -227,6 +232,10 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
227
232
|
throwError(err.message || err);
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
const chocolaDir = path.join(rootDir, ".chocola");
|
|
236
|
+
await fs.mkdir(chocolaDir, { recursive: true });
|
|
237
|
+
await fs.writeFile(path.join(chocolaDir, "hashes.json"), JSON.stringify(hashMap, null, 2) + "\n");
|
|
238
|
+
|
|
230
239
|
!isHotReload && logSuccess(paths.outDir);
|
|
231
240
|
isHotReload && console.log("Dev server updated");
|
|
232
241
|
}
|
|
@@ -2,12 +2,29 @@ import { promises as fs } from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { genRandomId } from "./utils.js";
|
|
4
4
|
|
|
5
|
-
export async function generateRuntimeScript(runtimeScript, outDirPath) {
|
|
5
|
+
export async function generateRuntimeScript(runtimeScript, outDirPath, csrSource, csrClasses) {
|
|
6
6
|
const fileIds = [];
|
|
7
|
-
const
|
|
8
|
-
const runtimeFileContents = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
|
|
7
|
+
const filenames = [];
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
if (csrSource) {
|
|
10
|
+
const name = "run-" + genRandomId(fileIds, 6) + ".js";
|
|
11
|
+
const source = csrSource.replace(/^export\s+\{?\s*[^}]+\s*}?\s*;?\s*$/m, "");
|
|
12
|
+
await fs.writeFile(path.join(outDirPath, name), source);
|
|
13
|
+
filenames.push(name);
|
|
14
|
+
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
if (csrClasses) {
|
|
17
|
+
const name = "run-" + genRandomId(fileIds, 6) + ".js";
|
|
18
|
+
await fs.writeFile(path.join(outDirPath, name), csrClasses);
|
|
19
|
+
filenames.push(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (runtimeScript) {
|
|
23
|
+
const name = "run-" + genRandomId(fileIds, 6) + ".js";
|
|
24
|
+
const content = `document.addEventListener("DOMContentLoaded", () => {${runtimeScript}})`;
|
|
25
|
+
await fs.writeFile(path.join(outDirPath, name), content);
|
|
26
|
+
filenames.push(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return filenames;
|
|
13
30
|
}
|
package/compiler/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import chalk from "./chalk.js";
|
|
2
3
|
|
|
3
4
|
export function throwError(err) {
|
|
@@ -5,16 +6,6 @@ export function throwError(err) {
|
|
|
5
6
|
throw new Error(err);
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
export function hasDelIfAttr(el) {
|
|
9
|
-
return el.hasAttribute("del:if");
|
|
10
|
-
}
|
|
11
|
-
export function getDelIfAttr(el) {
|
|
12
|
-
return el.getAttribute("del:if");
|
|
13
|
-
}
|
|
14
|
-
export function removeDelIfAttr(el) {
|
|
15
|
-
el.removeAttribute("del:if");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
export function genRandomId(collection = null, length = 10, lettersOnly = false) {
|
|
19
10
|
let id;
|
|
20
11
|
if (lettersOnly) {
|
|
@@ -49,31 +40,13 @@ export function incrementAlfabet(letters) {
|
|
|
49
40
|
return "a" + arr.join("");
|
|
50
41
|
}
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function restoreCurlyBraces(html) {
|
|
62
|
-
return html
|
|
63
|
-
.replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
|
|
64
|
-
.replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
|
|
43
|
+
export function deterministicHash(content, length = 8) {
|
|
44
|
+
const hash = createHash("sha256").update(content).digest();
|
|
45
|
+
let result = "";
|
|
46
|
+
for (let i = 0; i < length; i++) {
|
|
47
|
+
result += String.fromCharCode(97 + (hash[i % hash.length] % 26));
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
65
50
|
}
|
|
66
51
|
|
|
67
52
|
const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
|
68
|
-
const compiledCache = new Map();
|
|
69
|
-
export function compileExpression(expr, useCtx) {
|
|
70
|
-
const key = useCtx ? `ctx:${expr}` : `raw:${expr}`;
|
|
71
|
-
let fn = compiledCache.get(key);
|
|
72
|
-
if (!fn) {
|
|
73
|
-
fn = useCtx
|
|
74
|
-
? new Function("ctx", `with(ctx) { return (${expr}); }`)
|
|
75
|
-
: new Function(`"use strict"; return (${expr})`);
|
|
76
|
-
compiledCache.set(key, fn);
|
|
77
|
-
}
|
|
78
|
-
return fn;
|
|
79
|
-
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export function extractPropsDefaults(script) {
|
|
2
|
+
if (!script) return [];
|
|
3
|
+
const propsRegex = /export\s+let\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*(?:=\s*([^;]+))?;/g;
|
|
4
|
+
let props = [];
|
|
5
|
+
let match;
|
|
6
|
+
while ((match = propsRegex.exec(script)) !== null) {
|
|
7
|
+
props.push({ name: match[1].trim(), defaultValue: match[2]?.trim() });
|
|
8
|
+
}
|
|
9
|
+
return props;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function extractRuntime(script, compName) {
|
|
13
|
+
const startRegex = /(?:async\s+)?function\s+\$runtime\(([^)]*)\)\s*\{/;
|
|
14
|
+
const match = script.match(startRegex);
|
|
15
|
+
if (!match) return null;
|
|
16
|
+
|
|
17
|
+
const startIndexBrace = match.index + match[0].length - 1;
|
|
18
|
+
let bracesCount = 0;
|
|
19
|
+
let inString = false;
|
|
20
|
+
let stringChar = null;
|
|
21
|
+
|
|
22
|
+
for (let i = startIndexBrace; i < script.length; i++) {
|
|
23
|
+
const ch = script[i];
|
|
24
|
+
|
|
25
|
+
if (inString) {
|
|
26
|
+
if (ch === '\\') { i++; continue; }
|
|
27
|
+
if (ch === stringChar) inString = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
32
|
+
inString = true;
|
|
33
|
+
stringChar = ch;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (ch === '{') {
|
|
38
|
+
bracesCount++;
|
|
39
|
+
} else if (ch === '}') {
|
|
40
|
+
bracesCount--;
|
|
41
|
+
if (bracesCount === 0) {
|
|
42
|
+
return script.substring(match.index, i + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(`${compName} $runtime function has unclosed curly braces.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function extractTopLevelFunctions(script, excludeName) {
|
|
51
|
+
const funcs = [];
|
|
52
|
+
let i = 0;
|
|
53
|
+
let depth = 0;
|
|
54
|
+
let inStr = false;
|
|
55
|
+
let strChar = null;
|
|
56
|
+
|
|
57
|
+
while (i < script.length) {
|
|
58
|
+
const ch = script[i];
|
|
59
|
+
|
|
60
|
+
if (inStr) {
|
|
61
|
+
if (ch === '\\') i++;
|
|
62
|
+
else if (ch === strChar) inStr = false;
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
68
|
+
inStr = true;
|
|
69
|
+
strChar = ch;
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ch === '{') { depth++; i++; continue; }
|
|
75
|
+
if (ch === '}') { depth--; i++; continue; }
|
|
76
|
+
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
const rest = script.slice(i);
|
|
79
|
+
const m = rest.match(/^(?:async\s+)?function\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\(/);
|
|
80
|
+
if (m) {
|
|
81
|
+
const name = m[1];
|
|
82
|
+
const pastParen = i + m[0].length;
|
|
83
|
+
|
|
84
|
+
let parenDepth = 1;
|
|
85
|
+
let p = pastParen;
|
|
86
|
+
while (p < script.length && parenDepth > 0) {
|
|
87
|
+
if (script[p] === '(') parenDepth++;
|
|
88
|
+
if (script[p] === ')') parenDepth--;
|
|
89
|
+
p++;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
while (p < script.length && script[p] !== '{') p++;
|
|
93
|
+
if (p >= script.length) { i++; continue; }
|
|
94
|
+
|
|
95
|
+
let bDepth = 1;
|
|
96
|
+
let bodyStr = false;
|
|
97
|
+
let bodyStrChar = null;
|
|
98
|
+
let end = p + 1;
|
|
99
|
+
while (end < script.length && bDepth > 0) {
|
|
100
|
+
const c = script[end];
|
|
101
|
+
if (bodyStr) {
|
|
102
|
+
if (c === '\\') end++;
|
|
103
|
+
else if (c === bodyStrChar) bodyStr = false;
|
|
104
|
+
end++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
108
|
+
bodyStr = true;
|
|
109
|
+
bodyStrChar = c;
|
|
110
|
+
end++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === '{') bDepth++;
|
|
114
|
+
if (c === '}') bDepth--;
|
|
115
|
+
end++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (name !== excludeName) {
|
|
119
|
+
funcs.push(script.slice(i, end));
|
|
120
|
+
}
|
|
121
|
+
i = end;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
i++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return funcs;
|
|
129
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { compileExpression } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
export function extractContextFromElement(element) {
|
|
4
|
+
const ctx = {};
|
|
5
|
+
for (const attr of element.attributes) {
|
|
6
|
+
const key = attr.name;
|
|
7
|
+
const val = attr.value;
|
|
8
|
+
if (!val.includes("{")) { ctx[key] = val; continue; }
|
|
9
|
+
const matches = [...val.matchAll(/\{([^}]+)\}/g)];
|
|
10
|
+
if (matches.length === 1 && matches[0][0] === val) {
|
|
11
|
+
try {
|
|
12
|
+
ctx[key] = compileExpression(matches[0][1], false)();
|
|
13
|
+
continue;
|
|
14
|
+
} catch {}
|
|
15
|
+
}
|
|
16
|
+
ctx[key] = val;
|
|
17
|
+
}
|
|
18
|
+
return ctx;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasDelIfAttr(el) {
|
|
22
|
+
return el.hasAttribute("del:if");
|
|
23
|
+
}
|
|
24
|
+
export function getDelIfAttr(el) {
|
|
25
|
+
return el.getAttribute("del:if");
|
|
26
|
+
}
|
|
27
|
+
export function removeDelIfAttr(el) {
|
|
28
|
+
el.removeAttribute("del:if");
|
|
29
|
+
}
|
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,120 @@
|
|
|
1
|
+
import { compileExpression } from "./utils.js";
|
|
2
|
+
import { hasDelIfAttr, getDelIfAttr, removeDelIfAttr } from "./context.js";
|
|
3
|
+
|
|
4
|
+
export const reservedAttrs = ["if", "del:if", "elif", "else"];
|
|
5
|
+
|
|
6
|
+
export function getLineNumber(sourceContent, parentContent, idxInParent) {
|
|
7
|
+
if (sourceContent === parentContent) {
|
|
8
|
+
return parentContent.substring(0, idxInParent).split("\n").length;
|
|
9
|
+
}
|
|
10
|
+
const templateStartMatch = sourceContent.match(/<template[^>]*>/);
|
|
11
|
+
if (templateStartMatch) {
|
|
12
|
+
const contentStart = templateStartMatch.index + templateStartMatch[0].length;
|
|
13
|
+
const beforeContent = sourceContent.substring(0, contentStart);
|
|
14
|
+
const linesInBefore = beforeContent.split("\n").length;
|
|
15
|
+
const linesInTemplate = parentContent.substring(0, idxInParent).split("\n").length;
|
|
16
|
+
return linesInBefore + linesInTemplate - 1;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateChainStructure(parent, sourceFile, sourceContent, parentContent) {
|
|
22
|
+
const children = [...parent.children];
|
|
23
|
+
let chainActive = false;
|
|
24
|
+
|
|
25
|
+
for (const child of children) {
|
|
26
|
+
const hasIf = child.hasAttribute("if");
|
|
27
|
+
const hasDelIf = hasDelIfAttr(child);
|
|
28
|
+
const hasElif = child.hasAttribute("elif");
|
|
29
|
+
const hasElse = child.hasAttribute("else");
|
|
30
|
+
|
|
31
|
+
if (hasElif || hasElse) {
|
|
32
|
+
if (!chainActive) {
|
|
33
|
+
const tag = child.tagName.toLowerCase();
|
|
34
|
+
const attr = hasElif ? "elif" : "else";
|
|
35
|
+
let loc = sourceFile;
|
|
36
|
+
if (sourceContent && parentContent) {
|
|
37
|
+
const idx = parentContent.indexOf(child.outerHTML);
|
|
38
|
+
if (idx !== -1) {
|
|
39
|
+
const lineNum = getLineNumber(sourceContent, parentContent, idx);
|
|
40
|
+
if (lineNum !== null) loc = `${sourceFile}:${lineNum}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${loc}\n <${tag}> has ${attr} without a preceding if/del-if sibling`);
|
|
44
|
+
}
|
|
45
|
+
if (hasElse) {
|
|
46
|
+
chainActive = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (hasIf || hasDelIf) {
|
|
51
|
+
chainActive = true;
|
|
52
|
+
} else if (!hasElif && !hasElse) {
|
|
53
|
+
chainActive = false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validateChainStructure(child, sourceFile, sourceContent, parentContent);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function applyConditionalToElement(child, ctxProxy, condChain, hasIf, hasDelIf, hasElif, hasElse) {
|
|
61
|
+
if (hasIf) {
|
|
62
|
+
const expr = child.getAttribute("if").slice(1, -1);
|
|
63
|
+
const fn = compileExpression(expr, true);
|
|
64
|
+
condChain.active = true;
|
|
65
|
+
if (fn(ctxProxy)) {
|
|
66
|
+
condChain.rendered = true;
|
|
67
|
+
} else {
|
|
68
|
+
child.style.display = "none";
|
|
69
|
+
condChain.rendered = false;
|
|
70
|
+
}
|
|
71
|
+
child.removeAttribute("if");
|
|
72
|
+
} else if (hasDelIf) {
|
|
73
|
+
const expr = getDelIfAttr(child).slice(1, -1);
|
|
74
|
+
const fn = compileExpression(expr, true);
|
|
75
|
+
condChain.active = true;
|
|
76
|
+
if (fn(ctxProxy)) {
|
|
77
|
+
condChain.rendered = true;
|
|
78
|
+
} else {
|
|
79
|
+
child.remove();
|
|
80
|
+
condChain.rendered = false;
|
|
81
|
+
}
|
|
82
|
+
removeDelIfAttr(child);
|
|
83
|
+
} else if (hasElif) {
|
|
84
|
+
const expr = child.getAttribute("elif").slice(1, -1);
|
|
85
|
+
const fn = compileExpression(expr, true);
|
|
86
|
+
if (fn(ctxProxy)) {
|
|
87
|
+
condChain.rendered = true;
|
|
88
|
+
} else {
|
|
89
|
+
child.remove();
|
|
90
|
+
}
|
|
91
|
+
child.removeAttribute("elif");
|
|
92
|
+
} else if (hasElse) {
|
|
93
|
+
condChain.rendered = true;
|
|
94
|
+
condChain.active = false;
|
|
95
|
+
child.removeAttribute("else");
|
|
96
|
+
} else {
|
|
97
|
+
condChain.active = false;
|
|
98
|
+
condChain.rendered = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function interpolateNode(root, ctxProxy) {
|
|
103
|
+
const stack = [root];
|
|
104
|
+
while (stack.length) {
|
|
105
|
+
const node = stack.pop();
|
|
106
|
+
if (node.nodeType === 3) {
|
|
107
|
+
node.textContent = node.textContent.replace(/\{([^}]+)\}/g, (_, expr) => {
|
|
108
|
+
try {
|
|
109
|
+
return compileExpression(expr, true)(ctxProxy);
|
|
110
|
+
} catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
116
|
+
stack.push(node.childNodes[i]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
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
|
+
}
|