chocola 2.0.0-next.5 → 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/compiler/component-processor.js +333 -667
- package/compiler/dom-processor.js +3 -19
- package/compiler/index.js +8 -2
- package/compiler/utils.js +52 -79
- 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 +121 -0
- package/parser/utils.js +13 -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
|
}
|
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,7 +209,7 @@ 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);
|
|
212
|
+
const { runtimeScript, scopesCss, hashMap } = processAllComponents(appElements, loadedComponents, pageSourcePath, srcIndexContent);
|
|
211
213
|
const runtimeFilename = await generateRuntimeScript(runtimeScript, paths.outDir);
|
|
212
214
|
await processAssets(doc, rootDir, config.srcDir, paths.outDir);
|
|
213
215
|
|
|
@@ -227,6 +229,10 @@ export default async function compile(rootDir, buildConfig) {
|
|
|
227
229
|
throwError(err.message || err);
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
const chocolaDir = path.join(rootDir, ".chocola");
|
|
233
|
+
await fs.mkdir(chocolaDir, { recursive: true });
|
|
234
|
+
await fs.writeFile(path.join(chocolaDir, "hashes.json"), JSON.stringify(hashMap, null, 2) + "\n");
|
|
235
|
+
|
|
230
236
|
!isHotReload && logSuccess(paths.outDir);
|
|
231
237
|
isHotReload && console.log("Dev server updated");
|
|
232
238
|
}
|
package/compiler/utils.js
CHANGED
|
@@ -1,79 +1,52 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
id
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const RBRACE_PH = "_%%CHOCOLA-RBRACE%%_";
|
|
54
|
-
|
|
55
|
-
export function protectCurlyBraces(html) {
|
|
56
|
-
return html
|
|
57
|
-
.replace(/&(?:lbrace|#123|#x7B);/gi, LBRACE_PH)
|
|
58
|
-
.replace(/&(?:rcub|#125|#x7D);/gi, RBRACE_PH);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function restoreCurlyBraces(html) {
|
|
62
|
-
return html
|
|
63
|
-
.replace(/_%%CHOCOLA-LBRACE%%_/g, "{")
|
|
64
|
-
.replace(/_%%CHOCOLA-RBRACE%%_/g, "}");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
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
|
-
}
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import chalk from "./chalk.js";
|
|
3
|
+
|
|
4
|
+
export function throwError(err) {
|
|
5
|
+
console.log(chalk.red.bold("Error!"), "A fatal error has occurred:\n");
|
|
6
|
+
throw new Error(err);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function genRandomId(collection = null, length = 10, lettersOnly = false) {
|
|
10
|
+
let id;
|
|
11
|
+
if (lettersOnly) {
|
|
12
|
+
id = Array.from({ length }, () => ID_LETTERS[Math.floor(Math.random() * ID_LETTERS.length)]).join("");
|
|
13
|
+
} else {
|
|
14
|
+
id = Math.random().toString(36).substring(2, length + 2);
|
|
15
|
+
}
|
|
16
|
+
if (!collection) return id;
|
|
17
|
+
if (collection.includes(id)) {
|
|
18
|
+
return genRandomId(collection, length, lettersOnly);
|
|
19
|
+
} else {
|
|
20
|
+
collection.push(id);
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function incrementAlfabet(letters) {
|
|
26
|
+
let arr = letters.split("");
|
|
27
|
+
let i = arr.length - 1;
|
|
28
|
+
|
|
29
|
+
while (i >= 0) {
|
|
30
|
+
let pos = arr[i].charCodeAt(0) - 97;
|
|
31
|
+
if (pos < 25) {
|
|
32
|
+
arr[i] = String.fromCharCode(pos + 97 + 1);
|
|
33
|
+
return arr.join("");
|
|
34
|
+
} else {
|
|
35
|
+
arr[i] = "a";
|
|
36
|
+
i--;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return "a" + arr.join("");
|
|
41
|
+
}
|
|
42
|
+
|
|
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;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ID_LETTERS = "abcdefghijklmnopqrstuvwxyz";
|
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,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
|
}
|