sommark 4.0.3 → 4.2.0
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 +304 -73
- package/cli/cli.mjs +1 -1
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +2 -0
- package/cli/commands/init.js +25 -6
- package/cli/constants.js +2 -1
- package/cli/helpers/transpile.js +5 -2
- package/constants/html_props.js +1 -0
- package/core/evaluator.js +1061 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +16 -8
- package/core/helpers/lib.js +72 -0
- package/core/helpers/preprocessor.js +202 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/helpers/url.js +12 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +338 -60
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +352 -66
- package/core/validator.js +70 -7
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/fetch-fs.js +37 -0
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +97 -0
- package/helpers/utils.js +46 -0
- package/helpers/virtual-fs.js +29 -0
- package/index.browser.js +87 -0
- package/index.js +23 -332
- package/index.shared.js +443 -0
- package/mappers/languages/html.js +50 -9
- package/mappers/languages/json.js +81 -38
- package/mappers/languages/jsonc.js +82 -0
- package/mappers/languages/markdown.js +88 -48
- package/mappers/languages/mdx.js +50 -15
- package/mappers/languages/text.js +67 -0
- package/mappers/languages/xml.js +6 -6
- package/mappers/mapper.js +36 -4
- package/mappers/shared/index.js +12 -13
- package/package.json +11 -2
- package/core/formatter.js +0 -215
package/core/formats.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A list of supported output formats for SomMark.
|
|
3
3
|
*/
|
|
4
|
+
export const textFormat = "text";
|
|
5
|
+
export const htmlFormat = "html";
|
|
6
|
+
export const markdownFormat = "markdown";
|
|
7
|
+
export const mdxFormat = "mdx";
|
|
8
|
+
export const jsonFormat = "json";
|
|
9
|
+
export const jsoncFormat = "jsonc";
|
|
10
|
+
export const xmlFormat = "xml";
|
|
11
|
+
|
|
4
12
|
const formats = {
|
|
5
|
-
textFormat
|
|
6
|
-
htmlFormat
|
|
7
|
-
markdownFormat
|
|
8
|
-
mdxFormat
|
|
9
|
-
jsonFormat
|
|
10
|
-
|
|
13
|
+
textFormat,
|
|
14
|
+
htmlFormat,
|
|
15
|
+
markdownFormat,
|
|
16
|
+
mdxFormat,
|
|
17
|
+
jsonFormat,
|
|
18
|
+
jsoncFormat,
|
|
19
|
+
xmlFormat
|
|
11
20
|
};
|
|
12
21
|
|
|
13
|
-
export const { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, xmlFormat } = formats;
|
|
14
22
|
export default formats;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import path from "
|
|
1
|
+
import path from "pathe";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
|
|
@@ -40,7 +40,7 @@ export async function findAndLoadConfig(targetPath) {
|
|
|
40
40
|
try {
|
|
41
41
|
const absoluteTarget = path.resolve(cwd, targetPath);
|
|
42
42
|
const stats = await fs.stat(absoluteTarget);
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
// If target is a .js file, it might be an explicit config (legacy/internal support)
|
|
45
45
|
if (stats.isFile() && absoluteTarget.endsWith(".js") && !absoluteTarget.endsWith("smark.config.js")) {
|
|
46
46
|
configPath = absoluteTarget;
|
|
@@ -74,7 +74,7 @@ export async function findAndLoadConfig(targetPath) {
|
|
|
74
74
|
// No config found in CWD
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
const defaultConfig = {
|
|
79
79
|
outputFile: "output",
|
|
80
80
|
outputDir: startDir,
|
|
@@ -82,6 +82,14 @@ export async function findAndLoadConfig(targetPath) {
|
|
|
82
82
|
removeComments: true,
|
|
83
83
|
placeholders: {},
|
|
84
84
|
customProps: [],
|
|
85
|
+
importAliases: {},
|
|
86
|
+
fallbackTarget: "style",
|
|
87
|
+
outputValidator: null,
|
|
88
|
+
baseDir: null,
|
|
89
|
+
showSpinner: false,
|
|
90
|
+
generateRuntimeOutput: false,
|
|
91
|
+
hideRuntimeOutput: false,
|
|
92
|
+
security: {},
|
|
85
93
|
};
|
|
86
94
|
|
|
87
95
|
if (configPath) {
|
|
@@ -89,12 +97,12 @@ export async function findAndLoadConfig(targetPath) {
|
|
|
89
97
|
if (loadedConfig) {
|
|
90
98
|
// Support both mapperFile and mappingFile (backwards compatibility)
|
|
91
99
|
const finalMapper = loadedConfig.mapperFile || loadedConfig.mappingFile || defaultConfig.mapperFile;
|
|
92
|
-
|
|
93
|
-
const finalConfig = {
|
|
94
|
-
...defaultConfig,
|
|
95
|
-
...loadedConfig,
|
|
100
|
+
|
|
101
|
+
const finalConfig = {
|
|
102
|
+
...defaultConfig,
|
|
103
|
+
...loadedConfig,
|
|
96
104
|
mapperFile: finalMapper,
|
|
97
|
-
resolvedConfigPath: configPath
|
|
105
|
+
resolvedConfigPath: configPath
|
|
98
106
|
};
|
|
99
107
|
if (loadedConfig.outputDir) {
|
|
100
108
|
const configDir = path.dirname(configPath);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SomMark Evaluator APIs
|
|
3
|
+
*
|
|
4
|
+
* Provides built-in utility methods safely bound to the sandbox VM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Host-defined compile that will be injected securely
|
|
8
|
+
let hostCompile = null;
|
|
9
|
+
|
|
10
|
+
export function registerHostCompile(fn) {
|
|
11
|
+
hostCompile = fn;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Host-defined settings that will be injected securely
|
|
15
|
+
let hostSettings = {};
|
|
16
|
+
|
|
17
|
+
export function registerHostSettings(settings) {
|
|
18
|
+
hostSettings = settings || {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const version = "4.2.0";
|
|
22
|
+
|
|
23
|
+
const SomMark = {
|
|
24
|
+
version,
|
|
25
|
+
|
|
26
|
+
get settings() {
|
|
27
|
+
return hostSettings;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// Secure recursive compile implementation
|
|
31
|
+
compile: async (src, options = {}) => {
|
|
32
|
+
if (!hostCompile) {
|
|
33
|
+
throw new Error("Compilation capability is not initialized.");
|
|
34
|
+
}
|
|
35
|
+
return hostCompile(src, options);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Wrap string as safe raw HTML to skip automatic escaping
|
|
39
|
+
raw: (html) => {
|
|
40
|
+
return { __raw: String(html) };
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Register custom tag handlers programmatically within sandboxed environments
|
|
44
|
+
register: (id, render, options = {}) => {
|
|
45
|
+
throw new Error("SomMark.register can only be invoked within the sandboxed template logic environment.");
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Retrieve active tags by ID
|
|
49
|
+
get: (id) => {
|
|
50
|
+
throw new Error("SomMark.get can only be invoked within the sandboxed template logic environment.");
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Remove registered output handlers
|
|
54
|
+
removeOutput: (id) => {
|
|
55
|
+
throw new Error("SomMark.removeOutput can only be invoked within the sandboxed template logic environment.");
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Check if tag IDs are registered
|
|
59
|
+
includesId: (ids) => {
|
|
60
|
+
throw new Error("SomMark.includesId can only be invoked within the sandboxed template logic environment.");
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Programmatic HTML/XML tag generation utility
|
|
64
|
+
tag: (tagName) => {
|
|
65
|
+
throw new Error("SomMark.tag can only be invoked within the sandboxed template logic environment.");
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Freeze the entire Standard Library to make it completely immutable and tamper-proof
|
|
70
|
+
Object.freeze(SomMark);
|
|
71
|
+
|
|
72
|
+
export default SomMark;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import path from "pathe";
|
|
2
|
+
import * as acorn from "acorn";
|
|
3
|
+
import evaluator from "../evaluator.js";
|
|
4
|
+
import { transpilerError } from "../errors.js";
|
|
5
|
+
|
|
6
|
+
let _nodeFsCache;
|
|
7
|
+
async function getNodeFs() {
|
|
8
|
+
if (_nodeFsCache !== undefined) return _nodeFsCache;
|
|
9
|
+
try {
|
|
10
|
+
const m = await import("node:fs");
|
|
11
|
+
const raw = m.default || m;
|
|
12
|
+
_nodeFsCache = {
|
|
13
|
+
exists: (p) => raw.promises.access(p).then(() => true).catch(() => false),
|
|
14
|
+
readFile: (p, enc) => raw.promises.readFile(p, enc),
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
_nodeFsCache = null;
|
|
18
|
+
}
|
|
19
|
+
return _nodeFsCache;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Preprocesses a runtime JS block, parsing it with Acorn to locate
|
|
24
|
+
* SomMark.static("...") and SomMark.import("...") calls, evaluating/loading them,
|
|
25
|
+
* and replacing them inline with LSP/runtime safety.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} code - The raw javascript runtime code.
|
|
28
|
+
* @param {string|null} filename - Active template filename for relative imports.
|
|
29
|
+
* @param {Object} security - Security restrictions from the engine configuration.
|
|
30
|
+
* @returns {Promise<string>} - The preprocessed code.
|
|
31
|
+
*/
|
|
32
|
+
export async function preprocessRuntimeLogic(code, filename = null, security = {}, instance = null) {
|
|
33
|
+
let ast;
|
|
34
|
+
try {
|
|
35
|
+
ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// Fallback: If code is not a fully valid JS block (e.g. an expression fragment),
|
|
38
|
+
// let it pass through to the standard renderer untouched.
|
|
39
|
+
return code;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const matches = [];
|
|
43
|
+
|
|
44
|
+
// Recursive AST Traversal to find SomMark.static and SomMark.import calls
|
|
45
|
+
function traverse(node) {
|
|
46
|
+
if (!node || typeof node !== "object") return;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
node.type === "CallExpression" &&
|
|
50
|
+
node.callee.type === "MemberExpression" &&
|
|
51
|
+
node.callee.object.name === "SomMark" &&
|
|
52
|
+
node.arguments.length > 0
|
|
53
|
+
) {
|
|
54
|
+
const propName = node.callee.property.name;
|
|
55
|
+
if (propName === "static" || propName === "import") {
|
|
56
|
+
matches.push(node);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const key of Object.keys(node)) {
|
|
61
|
+
const child = node[key];
|
|
62
|
+
if (Array.isArray(child)) {
|
|
63
|
+
for (const item of child) traverse(item);
|
|
64
|
+
} else {
|
|
65
|
+
traverse(child);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
traverse(ast);
|
|
71
|
+
|
|
72
|
+
let preprocessedCode = code;
|
|
73
|
+
|
|
74
|
+
if (matches.length > 0) {
|
|
75
|
+
// Sort matches right-to-left to prevent character offset drifting
|
|
76
|
+
matches.sort((a, b) => b.start - a.start);
|
|
77
|
+
|
|
78
|
+
// Execute/Import and replace inline
|
|
79
|
+
for (const match of matches) {
|
|
80
|
+
const propName = match.callee.property.name;
|
|
81
|
+
const argNode = match.arguments[0];
|
|
82
|
+
let argValue = "";
|
|
83
|
+
|
|
84
|
+
if (argNode.type === "Literal") {
|
|
85
|
+
argValue = String(argNode.value);
|
|
86
|
+
} else if (argNode.type === "TemplateLiteral") {
|
|
87
|
+
argValue = argNode.quasis.map((q) => q.value.cooked).join("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (propName === "static") {
|
|
91
|
+
if (!argValue) {
|
|
92
|
+
transpilerError([
|
|
93
|
+
`<$red:SomMark.static Argument Error:$> The argument to SomMark.static must be a string.{line}`
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If the code contains a top-level return statement, wrap it in an async IIFE
|
|
98
|
+
let finalStaticCode = argValue.trim();
|
|
99
|
+
if (finalStaticCode.includes("return")) {
|
|
100
|
+
finalStaticCode = `(async () => {\n${argValue}\n})()`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Run securely inside the active QuickJS VM sandbox
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = await evaluator.execute(finalStaticCode);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
transpilerError([
|
|
109
|
+
`<$red:SomMark.static Execution Error:$> ${err.message}{line}`,
|
|
110
|
+
`<$yellow:Static Code:$> <$blue:${argValue}$>{line}`
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Serialize the return value safely
|
|
115
|
+
let serialized = "";
|
|
116
|
+
if (result === undefined) {
|
|
117
|
+
serialized = "undefined";
|
|
118
|
+
} else if (typeof result === "object") {
|
|
119
|
+
serialized = JSON.stringify(result);
|
|
120
|
+
} else if (typeof result === "string") {
|
|
121
|
+
serialized = JSON.stringify(result); // Automatically escapes quotes and special chars
|
|
122
|
+
} else {
|
|
123
|
+
serialized = String(result);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Slice out SomMark.static(...) and splice in the serialized value
|
|
127
|
+
preprocessedCode =
|
|
128
|
+
preprocessedCode.slice(0, match.start) +
|
|
129
|
+
serialized +
|
|
130
|
+
preprocessedCode.slice(match.end);
|
|
131
|
+
} else if (propName === "import") {
|
|
132
|
+
if (!argValue) {
|
|
133
|
+
transpilerError([
|
|
134
|
+
`<$red:SomMark.import Argument Error:$> The argument to SomMark.import must be a static, non-empty string literal.{line}`
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Resolve the file path relative to the template's base directory
|
|
139
|
+
let baseDir = instance?.cwd || "/";
|
|
140
|
+
if (filename && filename !== "anonymous") {
|
|
141
|
+
baseDir = path.dirname(path.resolve(filename));
|
|
142
|
+
}
|
|
143
|
+
const resolvedPath = path.resolve(baseDir, argValue);
|
|
144
|
+
|
|
145
|
+
const fsImpl = instance?.fs || await getNodeFs();
|
|
146
|
+
|
|
147
|
+
// File presence validation
|
|
148
|
+
if (!fsImpl || !await fsImpl.exists(resolvedPath)) {
|
|
149
|
+
transpilerError([
|
|
150
|
+
`<$red:SomMark.import File Error:$> File not found: <$magenta:${argValue}$>{line}`,
|
|
151
|
+
`<$yellow:Resolved Path:$> <$blue:${resolvedPath}$>{line}`
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Security Extension restriction validation
|
|
156
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
157
|
+
if (security?.allowedExtensions && !security.allowedExtensions.includes(ext)) {
|
|
158
|
+
transpilerError([
|
|
159
|
+
`<$red:Security Error:$> File extension <$yellow:${ext}$> is not allowed by security policy.{line}`,
|
|
160
|
+
`<$yellow:Import path:$> <$blue:${argValue}$>{line}`
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let serialized = "";
|
|
165
|
+
const content = await fsImpl.readFile(resolvedPath, "utf-8");
|
|
166
|
+
|
|
167
|
+
if (ext === ".json") {
|
|
168
|
+
// Validate JSON structure
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(content);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
transpilerError([
|
|
174
|
+
`<$red:JSON Parse Error:$> Failed to parse JSON file <$magenta:${argValue}$>:{line}`,
|
|
175
|
+
`<$yellow:Error:$> ${err.message}{line}`
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
serialized = JSON.stringify(parsed);
|
|
179
|
+
} else {
|
|
180
|
+
// Fallback for plain text, .smark, and other extensions: Serialize as JSON-escaped string
|
|
181
|
+
serialized = JSON.stringify(content);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Slice out SomMark.import(...) and splice in the serialized content
|
|
185
|
+
preprocessedCode =
|
|
186
|
+
preprocessedCode.slice(0, match.start) +
|
|
187
|
+
serialized +
|
|
188
|
+
preprocessedCode.slice(match.end);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// LSP / Linter Safety Guard Injection
|
|
194
|
+
// If the resulting code references the global SomMark keyword, prepend fallback declarations to prevent crashes.
|
|
195
|
+
const hasSomMarkRef = /\bSomMark\b/.test(preprocessedCode);
|
|
196
|
+
if (hasSomMarkRef) {
|
|
197
|
+
const safetyGuard = `/* global SomMark */\nif (typeof globalThis.SomMark === 'undefined') { globalThis.SomMark = { static: (c) => c, import: (c) => c }; }\n`;
|
|
198
|
+
preprocessedCode = safetyGuard + preprocessedCode;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return preprocessedCode;
|
|
202
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps runtime JavaScript logic inside target-specific scoped structures.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} code - The preprocessed javascript code.
|
|
5
|
+
* @param {string} format - The active compilation target format (e.g., 'html', 'mdx').
|
|
6
|
+
* @param {string|null} parentId - The unique tracking identifier of the parent element.
|
|
7
|
+
* @param {boolean} isGlobal - Whether the script is at the root level or block-level.
|
|
8
|
+
* @returns {string} - The formatted, target-scoped runtime code.
|
|
9
|
+
*/
|
|
10
|
+
export function wrapRuntimeLogic(code, format, parentId, isGlobal) {
|
|
11
|
+
const trimmedCode = code
|
|
12
|
+
.split("\n")
|
|
13
|
+
.filter(line => line.trim() !== "")
|
|
14
|
+
.join("\n");
|
|
15
|
+
|
|
16
|
+
if (isGlobal || !parentId) {
|
|
17
|
+
return `\n${trimmedCode}\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lowerFormat = format?.toLowerCase();
|
|
21
|
+
if (lowerFormat === "html" || lowerFormat === "mdx") {
|
|
22
|
+
const selfDefinition = `const self = document.querySelector('[data-sommark-id="${parentId}"]');`;
|
|
23
|
+
return `\n(async function(){${selfDefinition}\nif (self) {\n${trimmedCode}\n}\n})();\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Fallback/Default for other formats: return the raw code untouched
|
|
27
|
+
return `\n${trimmedCode}\n`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function fileURLToPath(url) {
|
|
2
|
+
if (typeof url !== "string") return url;
|
|
3
|
+
if (url.startsWith("file://")) {
|
|
4
|
+
let p = url.slice(7);
|
|
5
|
+
// On Windows (starts with /C:/ or similar), remove the leading slash:
|
|
6
|
+
if (/^\/[a-zA-Z]:/.test(p)) {
|
|
7
|
+
p = p.slice(1);
|
|
8
|
+
}
|
|
9
|
+
return p;
|
|
10
|
+
}
|
|
11
|
+
return url;
|
|
12
|
+
}
|
package/core/labels.js
CHANGED
|
@@ -7,8 +7,13 @@ export const BLOCK = "Block",
|
|
|
7
7
|
INLINE = "Inline",
|
|
8
8
|
ATBLOCK = "AtBlock",
|
|
9
9
|
COMMENT = "Comment",
|
|
10
|
+
COMMENT_BLOCK = "CommentBlock",
|
|
10
11
|
IMPORT = "Import",
|
|
11
|
-
USE_MODULE = "$use-module"
|
|
12
|
+
USE_MODULE = "$use-module",
|
|
13
|
+
SLOT = "Slot",
|
|
14
|
+
STATIC_LOGIC = "StaticLogic",
|
|
15
|
+
RUNTIME_LOGIC = "RuntimeLogic",
|
|
16
|
+
FOR_EACH = "ForEach";
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Names for symbols used to separate parts of the code (like commas and colons).
|
|
@@ -36,4 +41,6 @@ export const block_id = "Block Identifier",
|
|
|
36
41
|
atblock_key = "AtBlock Key",
|
|
37
42
|
at_end = "Atblock End",
|
|
38
43
|
/** Reserved keyword for closing blocks */
|
|
39
|
-
end_keyword = "end"
|
|
44
|
+
end_keyword = "end",
|
|
45
|
+
slot_keyword = "slot",
|
|
46
|
+
for_each_keyword = "for-each";
|