sommark 4.0.3 → 4.1.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 +274 -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 +785 -0
- package/core/formats.js +15 -7
- package/core/helpers/config-loader.js +8 -0
- package/core/helpers/lib.js +75 -0
- package/core/helpers/preprocessor.js +185 -0
- package/core/helpers/runtimeOutput.js +28 -0
- package/core/labels.js +9 -2
- package/core/lexer.js +228 -61
- package/core/modules.js +331 -55
- package/core/parser.js +275 -55
- package/core/tokenTypes.js +11 -0
- package/core/transpiler.js +341 -59
- package/core/validator.js +70 -7
- package/formatter/tag.js +31 -7
- package/grammar.ebnf +21 -10
- package/helpers/safeDataParser.js +3 -3
- package/helpers/spinner.js +91 -0
- package/helpers/utils.js +46 -0
- package/index.js +125 -38
- 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 +6 -1
- package/core/formatter.js +0 -215
package/formatter/tag.js
CHANGED
|
@@ -69,6 +69,8 @@ class TagBuilder {
|
|
|
69
69
|
const id = this.tagName.toLowerCase();
|
|
70
70
|
const isCodeStyleOrScript = ["style", "script"].includes(id);
|
|
71
71
|
let inline_style = "";
|
|
72
|
+
const useClassFallback = options.fallbackTarget === "class";
|
|
73
|
+
const classSet = new Set();
|
|
72
74
|
|
|
73
75
|
// 1. Initial CSS Variable/Style processing
|
|
74
76
|
if (!isCodeStyleOrScript && args.style) {
|
|
@@ -83,12 +85,20 @@ class TagBuilder {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
// 2.
|
|
88
|
+
// 2. Pre-collect native classes if using class fallback
|
|
89
|
+
if (useClassFallback) {
|
|
90
|
+
if (args.class) {
|
|
91
|
+
String(args.class).split(/\s+/).filter(Boolean).forEach(c => classSet.add(c));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Attribute Dispatching
|
|
87
96
|
const keys = Object.keys(args).filter(arg => isNaN(parseInt(arg)));
|
|
88
97
|
keys.forEach(key => {
|
|
89
98
|
if (!isNaN(parseInt(key))) return; // Skip numeric positional arguments
|
|
90
99
|
if (key === "style") return;
|
|
91
100
|
if (isCodeStyleOrScript && key === "scoped") return;
|
|
101
|
+
if (useClassFallback && key === "class") return;
|
|
92
102
|
|
|
93
103
|
const isDimensionAttributeSupported = ["img", "video", "svg", "canvas", "iframe", "object", "embed"].includes(id);
|
|
94
104
|
const isWidthOrHeight = key === "width" || key === "height";
|
|
@@ -99,21 +109,35 @@ class TagBuilder {
|
|
|
99
109
|
|
|
100
110
|
const k = isEvent ? key.toLowerCase() : (isNative || isCustom) ? key : kebabize(key);
|
|
101
111
|
|
|
102
|
-
if (isCodeStyleOrScript) {
|
|
103
|
-
// Specialized tags:
|
|
104
|
-
|
|
112
|
+
if (isCodeStyleOrScript || options.fallbackTarget === false) {
|
|
113
|
+
// Specialized tags or fallback disabled: render standard attributes, no styling fallback
|
|
114
|
+
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
115
|
+
this.#attr.push(`${k}="${escapeHTML(String(val))}"`);
|
|
105
116
|
} else {
|
|
106
|
-
// Standard elements: process smart
|
|
117
|
+
// Standard elements: process smart fallbacks
|
|
107
118
|
if (isEvent || ((isNative || isCustom) && (!isWidthOrHeight || isDimensionAttributeSupported)) || isDataOrAria) {
|
|
108
119
|
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
109
120
|
this.#attr.push(`${k}="${escapeHTML(String(val))}"`);
|
|
110
121
|
} else {
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
if (useClassFallback) {
|
|
123
|
+
const val = args[key];
|
|
124
|
+
if (val === true || val === "true") {
|
|
125
|
+
classSet.add(k);
|
|
126
|
+
} else if (val !== false && val !== "false" && val !== null && val !== undefined) {
|
|
127
|
+
classSet.add(`${k}-${val}`);
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
131
|
+
inline_style += `${k}:${val};`;
|
|
132
|
+
}
|
|
113
133
|
}
|
|
114
134
|
}
|
|
115
135
|
});
|
|
116
136
|
|
|
137
|
+
if (useClassFallback && classSet.size > 0) {
|
|
138
|
+
this.#attr.push(`class="${escapeHTML([...classSet].join(" "))}"`);
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
if (inline_style) {
|
|
118
142
|
// V4 DYNAMIC CSS: Automatically wrap CSS variables in var()
|
|
119
143
|
const processedStyle = inline_style.replace(/(^|[^\w\-_$])(--[\w\-_$]+)(?![\w\-_$]|:)/g, "$1var($2)");
|
package/grammar.ebnf
CHANGED
|
@@ -18,18 +18,28 @@ EscapeChar = "\", ? any character except WhiteSpace ?;
|
|
|
18
18
|
|
|
19
19
|
(* "Junk" refers to whitespace and comments which the parser skips in headers *)
|
|
20
20
|
Comment = "#", { ? any character except "\n" ? }, "\n";
|
|
21
|
-
|
|
21
|
+
CommentBlock = "###", { ? any character ? }, "###";
|
|
22
|
+
Junk = { WhiteSpace | Comment | CommentBlock };
|
|
22
23
|
|
|
23
24
|
(* Prefix Layers for Dynamic Data *)
|
|
24
25
|
PrefixJS = "js{", { ? any character ? }, "}";
|
|
25
26
|
PrefixP = "p{", { ? any character ? }, "}";
|
|
26
|
-
|
|
27
|
+
PrefixV = "v{", { ? any character ? }, "}";
|
|
28
|
+
PrefixLayer = PrefixJS | PrefixP | PrefixV;
|
|
29
|
+
|
|
30
|
+
(* Static / Runtime Logic Blocks *)
|
|
31
|
+
LogicBlock = ( "static" | "runtime" ), [ " " ], "${", { ? any character ? }, "}$";
|
|
27
32
|
|
|
28
33
|
(* ========================================== *)
|
|
29
34
|
(* Block Syntax (Containers) *)
|
|
30
35
|
(* ========================================== *)
|
|
31
36
|
|
|
32
|
-
Block =
|
|
37
|
+
Block = StandardBlock | SelfClosingBlock;
|
|
38
|
+
|
|
39
|
+
StandardBlock = BlockOpen, BlockBody, BlockEnd;
|
|
40
|
+
|
|
41
|
+
(* Self-Closing Blocks end with an exclamation mark (!) *)
|
|
42
|
+
SelfClosingBlock = "[", Junk, BlockID, Junk, [ "=", Junk, BlockArgs, Junk ], "!", Junk, "]";
|
|
33
43
|
|
|
34
44
|
(* Flexible Header with Junk-Skipping. Blocks allow colons in ID. *)
|
|
35
45
|
BlockOpen = "[", Junk, BlockID, Junk, [ "=", Junk, BlockArgs, Junk ], "]";
|
|
@@ -41,7 +51,7 @@ BlockArgs = BlockArg, { Junk, ",", Junk, BlockArg };
|
|
|
41
51
|
BlockArg = [ BlockKey, Junk, ":", Junk ], Value;
|
|
42
52
|
|
|
43
53
|
BlockKey = BlockID | QuotedString;
|
|
44
|
-
Value = QuotedString | PrefixLayer | RawValue;
|
|
54
|
+
Value = QuotedString | PrefixLayer | LogicBlock | RawValue;
|
|
45
55
|
|
|
46
56
|
QuotedString = ("'" | '"'), { ? any character ? | EscapeChar }, ("'" | '"');
|
|
47
57
|
RawValue = { ? any char except ",", Junk, "]" ? | EscapeChar };
|
|
@@ -50,15 +60,16 @@ RawValue = { ? any char except ",", Junk, "]" ? | EscapeChar };
|
|
|
50
60
|
(* Inline Statement Syntax *)
|
|
51
61
|
(* ========================================== *)
|
|
52
62
|
|
|
53
|
-
(* Inlines use SimpleID (NO COLONS allowed in ID) *)
|
|
54
|
-
InlineStatement = "(", InlineContent, ")", Junk, "->", Junk, "(", SimpleID, [ Junk, ":", Junk, InlineArgs ], ")";
|
|
63
|
+
(* Inlines use SimpleID (NO COLONS allowed in ID unless key-value) *)
|
|
64
|
+
InlineStatement = "(", InlineContent, ")", Junk, "->", Junk, "(", SimpleID, [ Junk, ( "=" | ":" ), Junk, InlineArgs ], ")";
|
|
55
65
|
|
|
56
66
|
(* Content allows balanced parentheses and placeholders *)
|
|
57
|
-
InlineContent = { ? any char except ")" ? | BalancedParen | EscapeChar | PrefixP };
|
|
67
|
+
InlineContent = { ? any char except ")" ? | BalancedParen | EscapeChar | PrefixP | PrefixV };
|
|
58
68
|
BalancedParen = "(", InlineContent, ")";
|
|
59
69
|
|
|
60
|
-
(* Inline Arguments: Positional
|
|
61
|
-
InlineArgs =
|
|
70
|
+
(* Inline Arguments: Positional and Named support *)
|
|
71
|
+
InlineArgs = InlineArg, { Junk, ",", Junk, InlineArg };
|
|
72
|
+
InlineArg = [ SimpleID, Junk, ":", Junk ], Value;
|
|
62
73
|
|
|
63
74
|
(* ========================================== *)
|
|
64
75
|
(* At-Block Syntax (Raw Containers) *)
|
|
@@ -83,4 +94,4 @@ AtBody = { ? any character ? | EscapeChar };
|
|
|
83
94
|
Document = { Block | InlineStatement | AtBlock | TextContent | Comment | WhiteSpace };
|
|
84
95
|
|
|
85
96
|
(* Text Content: Plain text with fallback behavior for symbols *)
|
|
86
|
-
TextContent = { ? any char except "[", "@_", "(", "#" ? | EscapeChar | PrefixP };
|
|
97
|
+
TextContent = { ? any char except "[", "@_", "(", "#" ? | EscapeChar | PrefixP | PrefixV | LogicBlock };
|
|
@@ -28,7 +28,7 @@ export function safeDataParse(str) {
|
|
|
28
28
|
if (char === '{') return parseObject();
|
|
29
29
|
if (char === '[') return parseArray();
|
|
30
30
|
if (char === '"' || char === "'") return parseString();
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
// Primitives or Unquoted identifiers
|
|
33
33
|
return parsePrimitiveOrIdentifier();
|
|
34
34
|
}
|
|
@@ -97,12 +97,12 @@ export function safeDataParse(str) {
|
|
|
97
97
|
index++;
|
|
98
98
|
}
|
|
99
99
|
const token = s.slice(start, index);
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
if (token === "true") return true;
|
|
102
102
|
if (token === "false") return false;
|
|
103
103
|
if (token === "null") return null;
|
|
104
104
|
if (!isNaN(Number(token))) return Number(token);
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
return token; // Fallback to string if it looks like an identifier
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Premium, dependency-free interactive terminal spinner for compilation feedback
|
|
2
|
+
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
let spinnerIndex = 0;
|
|
4
|
+
let activeSpinner = null;
|
|
5
|
+
let spinnerDepth = 0;
|
|
6
|
+
|
|
7
|
+
let originalStdoutWrite = null;
|
|
8
|
+
let originalStderrWrite = null;
|
|
9
|
+
let redrawTimeout = null;
|
|
10
|
+
|
|
11
|
+
export function startSpinner() {
|
|
12
|
+
if (process.stdout.isTTY && !activeSpinner) {
|
|
13
|
+
// Hide terminal cursor for a clean premium visual feel
|
|
14
|
+
process.stdout.write("\x1b[?25l");
|
|
15
|
+
|
|
16
|
+
// Print the first frame immediately
|
|
17
|
+
const frame = spinnerFrames[spinnerIndex];
|
|
18
|
+
process.stdout.write(`\r\x1b[38;5;39m${frame}\x1b[0m \x1b[1mSomMark:\x1b[0m \x1b[38;5;208mCompiling template...\x1b[0m`);
|
|
19
|
+
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
20
|
+
|
|
21
|
+
// Intercept stdout and stderr writes to keep the spinner on the very bottom line
|
|
22
|
+
originalStdoutWrite = process.stdout.write;
|
|
23
|
+
process.stdout.write = function(chunk, encoding, callback) {
|
|
24
|
+
originalStdoutWrite.call(process.stdout, "\r\x1b[K");
|
|
25
|
+
const res = originalStdoutWrite.call(process.stdout, chunk, encoding, callback);
|
|
26
|
+
if (activeSpinner) {
|
|
27
|
+
if (redrawTimeout) clearTimeout(redrawTimeout);
|
|
28
|
+
redrawTimeout = setTimeout(() => {
|
|
29
|
+
if (activeSpinner && originalStdoutWrite) {
|
|
30
|
+
const f = spinnerFrames[spinnerIndex];
|
|
31
|
+
originalStdoutWrite.call(process.stdout, `\r\x1b[38;5;39m${f}\x1b[0m \x1b[1mSomMark:\x1b[0m \x1b[38;5;208mCompiling template...\x1b[0m`);
|
|
32
|
+
}
|
|
33
|
+
}, 0);
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
originalStderrWrite = process.stderr.write;
|
|
39
|
+
process.stderr.write = function(chunk, encoding, callback) {
|
|
40
|
+
if (originalStdoutWrite) {
|
|
41
|
+
originalStdoutWrite.call(process.stdout, "\r\x1b[K");
|
|
42
|
+
}
|
|
43
|
+
const res = originalStderrWrite.call(process.stderr, chunk, encoding, callback);
|
|
44
|
+
if (activeSpinner) {
|
|
45
|
+
if (redrawTimeout) clearTimeout(redrawTimeout);
|
|
46
|
+
redrawTimeout = setTimeout(() => {
|
|
47
|
+
if (activeSpinner && originalStdoutWrite) {
|
|
48
|
+
const f = spinnerFrames[spinnerIndex];
|
|
49
|
+
originalStdoutWrite.call(process.stdout, `\r\x1b[38;5;39m${f}\x1b[0m \x1b[1mSomMark:\x1b[0m \x1b[38;5;208mCompiling template...\x1b[0m`);
|
|
50
|
+
}
|
|
51
|
+
}, 0);
|
|
52
|
+
}
|
|
53
|
+
return res;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
activeSpinner = setInterval(() => {
|
|
57
|
+
const frame = spinnerFrames[spinnerIndex];
|
|
58
|
+
if (originalStdoutWrite) {
|
|
59
|
+
originalStdoutWrite.call(process.stdout, `\r\x1b[38;5;39m${frame}\x1b[0m \x1b[1mSomMark:\x1b[0m \x1b[38;5;208mCompiling template...\x1b[0m`);
|
|
60
|
+
} else {
|
|
61
|
+
process.stdout.write(`\r\x1b[38;5;39m${frame}\x1b[0m \x1b[1mSomMark:\x1b[0m \x1b[38;5;208mCompiling template...\x1b[0m`);
|
|
62
|
+
}
|
|
63
|
+
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
64
|
+
}, 80);
|
|
65
|
+
}
|
|
66
|
+
spinnerDepth++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function stopSpinner() {
|
|
70
|
+
spinnerDepth--;
|
|
71
|
+
if (spinnerDepth <= 0) {
|
|
72
|
+
spinnerDepth = 0;
|
|
73
|
+
if (activeSpinner) {
|
|
74
|
+
clearInterval(activeSpinner);
|
|
75
|
+
activeSpinner = null;
|
|
76
|
+
|
|
77
|
+
// Restore original writes
|
|
78
|
+
if (originalStdoutWrite) {
|
|
79
|
+
process.stdout.write = originalStdoutWrite;
|
|
80
|
+
originalStdoutWrite = null;
|
|
81
|
+
}
|
|
82
|
+
if (originalStderrWrite) {
|
|
83
|
+
process.stderr.write = originalStderrWrite;
|
|
84
|
+
originalStderrWrite = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Clear the spinner line and restore the terminal cursor
|
|
88
|
+
process.stdout.write("\r\x1b[K\x1b[?25h");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
package/helpers/utils.js
CHANGED
|
@@ -168,3 +168,49 @@ export function levenshtein(a, b) {
|
|
|
168
168
|
}
|
|
169
169
|
return matrix[b.length][a.length];
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
// -- Unresolved Placeholder Helpers ---------------------------------------- //
|
|
173
|
+
|
|
174
|
+
export const UNRESOLVED_PREFIX = "SOMMARK_UNRESOLVED";
|
|
175
|
+
export const UNRESOLVED_SUFFIX = "SOMMARK";
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Official method to get the unique envelope for an unresolved prefix value.
|
|
179
|
+
* @param {string} prefix - The layer ('p' or 'v').
|
|
180
|
+
* @param {string} expectedValue - The placeholder key.
|
|
181
|
+
* @returns {string} - The unique envelope string.
|
|
182
|
+
*/
|
|
183
|
+
export function getPrefixValue(prefix, expectedValue) {
|
|
184
|
+
if (!prefix || (prefix !== "p" && prefix !== "v")) {
|
|
185
|
+
sommarkError([
|
|
186
|
+
`<$red:getPrefixValue Error:$> {N}`,
|
|
187
|
+
`<$yellow:prefix must be 'p' or 'v'. Received:$> <$cyan:'${prefix}'$>`
|
|
188
|
+
]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!expectedValue || typeof expectedValue !== "string" || expectedValue.trim() === "") {
|
|
192
|
+
sommarkError([
|
|
193
|
+
`<$red:getPrefixValue Error:$> {N}`,
|
|
194
|
+
`<$yellow:expectedValue must be a non-empty string. Received:$> <$cyan:'${expectedValue}'$>`
|
|
195
|
+
]);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return `${UNRESOLVED_PREFIX}_${prefix}_${expectedValue}_${UNRESOLVED_SUFFIX}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Performs a final pass on a string to clean up any remaining unresolved envelopes.
|
|
203
|
+
* @param {string} content - The content to clean.
|
|
204
|
+
* @param {string|null} [fallback] - The format to use for missing keys (e.g., "$layer{$key}" or "").
|
|
205
|
+
* @returns {string} - The cleaned content.
|
|
206
|
+
*/
|
|
207
|
+
export function cleanUnresolved(content, fallback = null) {
|
|
208
|
+
const pattern = new RegExp(`${UNRESOLVED_PREFIX}_(p|v)_(.+?)_${UNRESOLVED_SUFFIX}`, "g");
|
|
209
|
+
return content.replace(pattern, (match, layer, key) => {
|
|
210
|
+
if (fallback !== null) {
|
|
211
|
+
return fallback.replace("$layer", layer).replace("$key", key);
|
|
212
|
+
}
|
|
213
|
+
// Default: Keep as is or restore classic
|
|
214
|
+
return `${layer}{${key}}`;
|
|
215
|
+
});
|
|
216
|
+
}
|
package/index.js
CHANGED
|
@@ -2,21 +2,24 @@ import lexer from "./core/lexer.js";
|
|
|
2
2
|
import parser from "./core/parser.js";
|
|
3
3
|
import transpiler from "./core/transpiler.js";
|
|
4
4
|
import Mapper from "./mappers/mapper.js";
|
|
5
|
+
import { registerSharedOutputs } from "./mappers/shared/index.js";
|
|
5
6
|
import HTML from "./mappers/languages/html.js";
|
|
6
7
|
import MARKDOWN from "./mappers/languages/markdown.js";
|
|
7
8
|
import MDX from "./mappers/languages/mdx.js";
|
|
8
9
|
import Json from "./mappers/languages/json.js";
|
|
10
|
+
import Jsonc from "./mappers/languages/jsonc.js";
|
|
9
11
|
import XML from "./mappers/languages/xml.js";
|
|
12
|
+
import TEXT from "./mappers/languages/text.js";
|
|
10
13
|
import { runtimeError } from "./core/errors.js";
|
|
11
|
-
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, xmlFormat } from "./core/formats.js";
|
|
14
|
+
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, jsoncFormat, xmlFormat } from "./core/formats.js";
|
|
12
15
|
import TOKEN_TYPES from "./core/tokenTypes.js";
|
|
13
16
|
import * as labels from "./core/labels.js";
|
|
14
17
|
import { resolveModules } from "./core/modules.js";
|
|
15
|
-
import { formatAST } from "./core/formatter.js";
|
|
16
18
|
import { validateAST } from "./core/validator.js";
|
|
17
19
|
import { enableColor } from "./helpers/colorize.js";
|
|
18
20
|
import { safeArg } from "./helpers/utils.js";
|
|
19
|
-
|
|
21
|
+
import { startSpinner, stopSpinner } from "./helpers/spinner.js";
|
|
22
|
+
import { preprocessRuntimeLogic } from "./core/helpers/preprocessor.js";
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* The SomMark Core Engine.
|
|
@@ -33,28 +36,60 @@ class SomMark {
|
|
|
33
36
|
* @param {Mapper|null} [options.mapperFile=null] - Custom rules for formatting.
|
|
34
37
|
* @param {string} [options.filename="anonymous"] - The name of the file, used for errors and settings.
|
|
35
38
|
* @param {boolean} [options.removeComments=true] - If true, comments will be removed from the final code.
|
|
36
|
-
* @param {Object} [options.placeholders={}] - Values to use for {placeholders}.
|
|
39
|
+
* @param {Object} [options.placeholders={}] - Values to use for p{placeholders}.
|
|
37
40
|
* @param {Array<string>} [options.customProps=[]] - Allowed custom HTML attributes.
|
|
41
|
+
* @param {Object} [options.importAliases={}] - Custom path aliases for modules.
|
|
38
42
|
* @param {Array<string>} [options.importStack=[]] - Tracking for circular dependencies.
|
|
43
|
+
* @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
|
|
39
44
|
*/
|
|
40
|
-
constructor(
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = "style", outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, generateRuntimeOutput = false, hideRuntimeOutput = false, moduleIdentityToken = null } = options;
|
|
47
|
+
this.rawSettings = options;
|
|
41
48
|
this.src = src;
|
|
49
|
+
this.ast = ast;
|
|
42
50
|
this.targetFormat = format;
|
|
43
51
|
this.mapperFile = mapperFile;
|
|
44
52
|
this.filename = filename;
|
|
45
53
|
this.removeComments = removeComments;
|
|
46
54
|
this.placeholders = placeholders;
|
|
47
55
|
this.customProps = customProps;
|
|
56
|
+
this.generateRuntimeOutput = generateRuntimeOutput;
|
|
57
|
+
this.hideRuntimeOutput = hideRuntimeOutput;
|
|
58
|
+
|
|
59
|
+
// Validate fallbackTarget
|
|
60
|
+
const VALID_FALLBACK_TARGETS = new Set(["style", "class", false]);
|
|
61
|
+
if (!VALID_FALLBACK_TARGETS.has(fallbackTarget)) {
|
|
62
|
+
runtimeError([
|
|
63
|
+
`{line}<$red:Invalid fallbackTarget$>: <$green:'${fallbackTarget}'$> <$yellow:is not a valid value.$>`,
|
|
64
|
+
`{N}<$yellow:Use$> <$green:'style'$><$yellow:,$> <$green:'class'$><$yellow:, or$> <$green:false$><$yellow:.$>{line}`
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
this.fallbackTarget = fallbackTarget;
|
|
68
|
+
this.outputValidator = outputValidator;
|
|
69
|
+
this.importAliases = importAliases;
|
|
48
70
|
this.importStack = importStack;
|
|
71
|
+
this.baseDir = baseDir;
|
|
72
|
+
this.moduleCache = moduleCache || new Map();
|
|
73
|
+
this.showSpinner = showSpinner;
|
|
74
|
+
this.security = {
|
|
75
|
+
allowRaw: security?.allowRaw !== false,
|
|
76
|
+
maxDepth: security?.maxDepth ?? 5,
|
|
77
|
+
timeout: security?.timeout ?? 5000,
|
|
78
|
+
sanitize: typeof security?.sanitize === "function" ? security.sanitize : null,
|
|
79
|
+
allowFetch: security?.allowFetch !== false,
|
|
80
|
+
allowHttp: security?.allowHttp === true,
|
|
81
|
+
allowedOrigins: Array.isArray(security?.allowedOrigins) ? security.allowedOrigins.map(o => o.toLowerCase()) : null,
|
|
82
|
+
allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null
|
|
83
|
+
};
|
|
49
84
|
this.warnings = [];
|
|
50
85
|
this._prepared = false;
|
|
51
86
|
|
|
52
87
|
// Create a random token to safely wrap data
|
|
53
|
-
this.moduleIdentityToken = `$_SM_MOD_${Math.random().toString(36).slice(2, 7)}_$`;
|
|
88
|
+
this.moduleIdentityToken = moduleIdentityToken || `$_SM_MOD_${Math.random().toString(36).slice(2, 7)}_$`;
|
|
54
89
|
|
|
55
90
|
this.Mapper = Mapper;
|
|
56
91
|
|
|
57
|
-
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [xmlFormat]: XML, [textFormat]:
|
|
92
|
+
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [jsoncFormat]: Jsonc, [xmlFormat]: XML, [textFormat]: TEXT };
|
|
58
93
|
|
|
59
94
|
if (!this.mapperFile && this.targetFormat) {
|
|
60
95
|
const DefaultMapper = mapperFiles[this.targetFormat];
|
|
@@ -70,6 +105,9 @@ class SomMark {
|
|
|
70
105
|
this.mapperFile.options.moduleIdentityToken = this.moduleIdentityToken;
|
|
71
106
|
this.mapperFile.options.filename = this.filename;
|
|
72
107
|
|
|
108
|
+
this.mapperFile.options.usePrivateAttributes = this.usePrivateAttributes;
|
|
109
|
+
this.mapperFile.options.fallbackTarget = this.fallbackTarget;
|
|
110
|
+
|
|
73
111
|
// Initialize custom props whitelist
|
|
74
112
|
if (this.customProps && this.customProps.length > 0) {
|
|
75
113
|
const props = Array.isArray(this.customProps) ? this.customProps : [this.customProps];
|
|
@@ -161,6 +199,13 @@ class SomMark {
|
|
|
161
199
|
return tokens;
|
|
162
200
|
}
|
|
163
201
|
|
|
202
|
+
lexSync(src = this.src) {
|
|
203
|
+
this._ensurePrepared();
|
|
204
|
+
if (src !== this.src) this.src = src;
|
|
205
|
+
let tokens = lexer(this.src, this.filename);
|
|
206
|
+
return tokens;
|
|
207
|
+
}
|
|
208
|
+
|
|
164
209
|
/**
|
|
165
210
|
* Organizes the code into a tree structure.
|
|
166
211
|
* Also handles modules and checks for errors.
|
|
@@ -170,7 +215,7 @@ class SomMark {
|
|
|
170
215
|
*/
|
|
171
216
|
async parse(src = this.src) {
|
|
172
217
|
const tokens = await this.lex(src);
|
|
173
|
-
let ast = parser(tokens, this.filename, this.placeholders);
|
|
218
|
+
let ast = parser(tokens, this.filename, this.placeholders, {});
|
|
174
219
|
|
|
175
220
|
ast = await resolveModules(ast, {
|
|
176
221
|
mapperFile: this.mapperFile,
|
|
@@ -191,7 +236,7 @@ class SomMark {
|
|
|
191
236
|
this._ensurePrepared();
|
|
192
237
|
if (src !== this.src) this.src = src;
|
|
193
238
|
const tokens = lexer(this.src, this.filename);
|
|
194
|
-
let ast = parser(tokens, this.filename, this.placeholders);
|
|
239
|
+
let ast = parser(tokens, this.filename, this.placeholders, {});
|
|
195
240
|
|
|
196
241
|
if (this.mapperFile) {
|
|
197
242
|
validateAST(ast, this.mapperFile, this);
|
|
@@ -210,24 +255,30 @@ class SomMark {
|
|
|
210
255
|
if (src !== this.src) this.src = src;
|
|
211
256
|
this._ensurePrepared();
|
|
212
257
|
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
if (this.showSpinner) startSpinner();
|
|
259
|
+
try {
|
|
260
|
+
const ast = this.ast || await this.parse(src);
|
|
261
|
+
let result = await transpiler({
|
|
262
|
+
ast,
|
|
263
|
+
format: this.targetFormat,
|
|
264
|
+
mapperFile: this.mapperFile,
|
|
265
|
+
security: this.security,
|
|
266
|
+
settings: this.rawSettings,
|
|
267
|
+
generateRuntimeOutput: this.generateRuntimeOutput,
|
|
268
|
+
hideRuntimeOutput: this.hideRuntimeOutput
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (this.outputValidator && typeof this.outputValidator === "function") {
|
|
272
|
+
await this.outputValidator(result);
|
|
273
|
+
}
|
|
215
274
|
|
|
216
|
-
|
|
275
|
+
return result;
|
|
276
|
+
} finally {
|
|
277
|
+
if (this.showSpinner) stopSpinner();
|
|
278
|
+
}
|
|
217
279
|
}
|
|
218
280
|
|
|
219
281
|
|
|
220
|
-
async format(options = {}) {
|
|
221
|
-
const tokens = await this.lex();
|
|
222
|
-
const ast = parser(tokens, this.filename);
|
|
223
|
-
return formatAST(ast, options);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
formatSync(options = {}) {
|
|
227
|
-
const tokens = lexer(this.src, this.filename);
|
|
228
|
-
const ast = parser(tokens, this.filename);
|
|
229
|
-
return formatAST(ast, options);
|
|
230
|
-
}
|
|
231
282
|
}
|
|
232
283
|
|
|
233
284
|
/**
|
|
@@ -239,7 +290,13 @@ class SomMark {
|
|
|
239
290
|
* @returns {Promise<Array<Object>>} - The list of tokens.
|
|
240
291
|
*/
|
|
241
292
|
const lex = async (src, filename = "anonymous") => {
|
|
242
|
-
|
|
293
|
+
if (src === undefined || src === null) {
|
|
294
|
+
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for tokenization.$>{line}`]);
|
|
295
|
+
}
|
|
296
|
+
if (typeof src !== "string") {
|
|
297
|
+
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
298
|
+
}
|
|
299
|
+
return lexer(src, filename);
|
|
243
300
|
};
|
|
244
301
|
|
|
245
302
|
/**
|
|
@@ -251,14 +308,17 @@ const lex = async (src, filename = "anonymous") => {
|
|
|
251
308
|
* @returns {Promise<Array<Object>>} - The final code tree.
|
|
252
309
|
*/
|
|
253
310
|
async function parse(src, filename = "anonymous") {
|
|
254
|
-
if (
|
|
311
|
+
if (src === undefined || src === null) {
|
|
255
312
|
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for parsing.$>{line}`]);
|
|
256
313
|
}
|
|
257
|
-
|
|
314
|
+
if (typeof src !== "string") {
|
|
315
|
+
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
316
|
+
}
|
|
317
|
+
return await new SomMark({ src, filename, format: textFormat }).parse();
|
|
258
318
|
}
|
|
259
319
|
|
|
260
320
|
/**
|
|
261
|
-
*
|
|
321
|
+
* Transpiles SomMark code to a target format.
|
|
262
322
|
*
|
|
263
323
|
* @param {Object} options - Transpilation options.
|
|
264
324
|
* @param {string} options.src - Raw source code.
|
|
@@ -268,14 +328,16 @@ async function parse(src, filename = "anonymous") {
|
|
|
268
328
|
* @param {boolean} [options.removeComments=true] - Strip comments.
|
|
269
329
|
* @param {Object} [options.placeholders={}] - Global placeholders.
|
|
270
330
|
* @param {Array<string>} [options.customProps=[]] - Custom attribute whitelist.
|
|
331
|
+
* @param {Object} [options.importAliases={}] - Custom path aliases for modules.
|
|
271
332
|
* @returns {Promise<string>} - Transpiled output.
|
|
272
333
|
*/
|
|
273
334
|
async function transpile(options = {}) {
|
|
274
|
-
const { src, format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true, placeholders = {}, customProps = [] } = options;
|
|
275
335
|
if (typeof options !== "object" || options === null) {
|
|
276
336
|
runtimeError([`{line}<$red:Invalid Options:$> <$yellow:The options argument must be a non-null object.$>{line}`]);
|
|
277
337
|
}
|
|
278
|
-
const
|
|
338
|
+
const { src, ast, format } = options;
|
|
339
|
+
|
|
340
|
+
const knownProps = ["src", "ast", "format", "filename", "mapperFile", "removeComments", "placeholders", "customProps", "importAliases", "fallbackTarget", "usePrivateAttributes", "outputValidator", "showSpinner", "security"];
|
|
279
341
|
Object.keys(options).forEach(key => {
|
|
280
342
|
if (!knownProps.includes(key)) {
|
|
281
343
|
runtimeError([
|
|
@@ -283,11 +345,16 @@ async function transpile(options = {}) {
|
|
|
283
345
|
]);
|
|
284
346
|
}
|
|
285
347
|
});
|
|
286
|
-
|
|
287
|
-
|
|
348
|
+
|
|
349
|
+
if (format === undefined || format === null) {
|
|
350
|
+
runtimeError([`{line}<$red:Missing Target Format:$> <$yellow:The 'format' parameter is required for transpilation (e.g. 'html', 'markdown', 'xml', 'mdx', 'json', etc.).$>{line}`]);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if ((src === undefined || src === null) && (ast === undefined || ast === null)) {
|
|
354
|
+
runtimeError([`{line}<$red:Missing Input:$> <$yellow:Either 'src' or 'ast' must be provided for transpilation.$>{line}`]);
|
|
288
355
|
}
|
|
289
356
|
|
|
290
|
-
const sm = new SomMark(
|
|
357
|
+
const sm = new SomMark(options);
|
|
291
358
|
return await sm.transpile();
|
|
292
359
|
}
|
|
293
360
|
|
|
@@ -295,9 +362,18 @@ async function transpile(options = {}) {
|
|
|
295
362
|
* A quick, synchronous way to get tokens.
|
|
296
363
|
*
|
|
297
364
|
* @param {string} src - Raw source code.
|
|
365
|
+
* @param {string} [filename="anonymous"] - Filename for error context.
|
|
298
366
|
* @returns {Array<Object>} - The list of tokens.
|
|
299
367
|
*/
|
|
300
|
-
const lexSync = src =>
|
|
368
|
+
const lexSync = (src, filename = "anonymous") => {
|
|
369
|
+
if (src === undefined || src === null) {
|
|
370
|
+
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for tokenization.$>{line}`]);
|
|
371
|
+
}
|
|
372
|
+
if (typeof src !== "string") {
|
|
373
|
+
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
374
|
+
}
|
|
375
|
+
return lexer(src, filename);
|
|
376
|
+
};
|
|
301
377
|
|
|
302
378
|
/**
|
|
303
379
|
* A quick, synchronous way to get the code tree.
|
|
@@ -306,18 +382,28 @@ const lexSync = src => lexer(src);
|
|
|
306
382
|
* @param {Object} [options={}] - Parsing options.
|
|
307
383
|
* @returns {Array<Object>} - The code tree.
|
|
308
384
|
*/
|
|
309
|
-
const parseSync = (src,
|
|
310
|
-
|
|
311
|
-
|
|
385
|
+
const parseSync = (src, filename = "anonymous") => {
|
|
386
|
+
if (src === undefined || src === null) {
|
|
387
|
+
runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for parsing.$>{line}`]);
|
|
388
|
+
}
|
|
389
|
+
if (typeof src !== "string") {
|
|
390
|
+
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
391
|
+
}
|
|
392
|
+
const tokens = lexer(src, filename);
|
|
393
|
+
return parser(tokens, filename);
|
|
312
394
|
};
|
|
313
395
|
|
|
314
396
|
import { findAndLoadConfig } from "./core/helpers/config-loader.js";
|
|
315
397
|
|
|
398
|
+
import Evaluator from "./core/evaluator.js";
|
|
399
|
+
|
|
316
400
|
export {
|
|
401
|
+
registerSharedOutputs,
|
|
317
402
|
HTML,
|
|
318
403
|
MARKDOWN,
|
|
319
404
|
MDX,
|
|
320
405
|
Json,
|
|
406
|
+
Jsonc,
|
|
321
407
|
XML,
|
|
322
408
|
Mapper,
|
|
323
409
|
FORMATS,
|
|
@@ -326,11 +412,12 @@ export {
|
|
|
326
412
|
transpile,
|
|
327
413
|
lexSync,
|
|
328
414
|
parseSync,
|
|
329
|
-
formatAST,
|
|
330
415
|
TOKEN_TYPES,
|
|
331
416
|
labels,
|
|
332
417
|
enableColor,
|
|
333
418
|
safeArg,
|
|
334
|
-
findAndLoadConfig
|
|
419
|
+
findAndLoadConfig,
|
|
420
|
+
Evaluator,
|
|
421
|
+
preprocessRuntimeLogic
|
|
335
422
|
};
|
|
336
423
|
export default SomMark;
|