sommark 4.0.2 → 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 +2 -2
- package/cli/commands/build.js +3 -1
- package/cli/commands/help.js +4 -1
- package/cli/commands/init.js +25 -6
- package/cli/commands/show.js +20 -10
- 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 +28 -15
- 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 +85 -13
- 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/core/validator.js
CHANGED
|
@@ -18,18 +18,80 @@ import { transpilerError } from "./errors.js";
|
|
|
18
18
|
const runValidations = (node, target, instance) => {
|
|
19
19
|
if (!target || !target.options) return;
|
|
20
20
|
const rules = target.options.rules || {};
|
|
21
|
-
const id = (target.id) ? (Array.isArray(target.id) ? target.id.join(" | ") : target.id) : "Unknown";
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
const id = (target.id) ? (Array.isArray(target.id) ? target.id.join(" | ") : target.id) : (node.id || "Unknown");
|
|
22
|
+
const errorRange = node.range ? {
|
|
23
|
+
start: node.range.start,
|
|
24
|
+
end: {
|
|
25
|
+
line: node.range.start.line,
|
|
26
|
+
character: node.range.start.character + (node.id || "").length + 2
|
|
27
|
+
}
|
|
28
|
+
} : null;
|
|
29
|
+
const context = instance ? { src: instance.src, range: errorRange, filename: instance.filename } : null;
|
|
30
|
+
|
|
31
|
+
// -- Structural Integrity (Empty Body / Self-Closing) ----------------- //
|
|
32
|
+
const isEmptyBodyTarget = rules.is_empty_body || rules.is_self_closing;
|
|
33
|
+
|
|
34
|
+
// -- Node Type Validation --------------------------------------------- //
|
|
35
|
+
if (target.options.type) {
|
|
36
|
+
const allowedTypes = Array.isArray(target.options.type) ? target.options.type : [target.options.type];
|
|
37
|
+
const hasAny = allowedTypes.includes("any");
|
|
38
|
+
if (!hasAny && !allowedTypes.includes(node.structure)) {
|
|
39
|
+
const isReserved = ["import", "$use-module", "slot", "for-each"].includes(id.toLowerCase());
|
|
40
|
+
const msg = isReserved
|
|
41
|
+
? `<$yellow:Reserved keyword$> <$blue:'${id}'$> <$yellow:is strictly defined as a [${allowedTypes.join(", ")}] structure node, but was used as a [${node.structure}] structure node.$>`
|
|
42
|
+
: `<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is defined as type(s) [${allowedTypes.join(", ")}], but was used as a [${node.structure}] structure node.$>`;
|
|
43
|
+
|
|
44
|
+
transpilerError(
|
|
45
|
+
[
|
|
46
|
+
"{N}",
|
|
47
|
+
msg
|
|
48
|
+
],
|
|
49
|
+
context
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isEmptyBodyTarget && node.type === "Block" && !node.isSelfClosing && node.body) {
|
|
55
|
+
const hasContent = node.body.some(child => {
|
|
56
|
+
if (child.type === "Text") {
|
|
57
|
+
return (child.text || "").trim().length > 0;
|
|
58
|
+
}
|
|
59
|
+
return true; // Any other node type (Block, Inline, etc.) counts as content
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (hasContent) {
|
|
63
|
+
transpilerError(
|
|
64
|
+
[
|
|
65
|
+
"{N}",
|
|
66
|
+
"<$red:[Validation Error]:$>{N}",
|
|
67
|
+
`<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is defined as an empty-body component and cannot have children.$>`
|
|
68
|
+
],
|
|
69
|
+
context
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// -- Arguments Validation (Required Args) ----------------------------- //
|
|
75
|
+
const isStructural = node.type === "Block" || node.type === "AtBlock";
|
|
76
|
+
if (isStructural && rules.required_args && Array.isArray(rules.required_args)) {
|
|
77
|
+
const missingArgs = rules.required_args.filter(arg => {
|
|
78
|
+
// Check if the argument exists in named args or as a positional arg (if arg is a number)
|
|
79
|
+
if (typeof arg === "number") {
|
|
80
|
+
return node.args[arg] === undefined;
|
|
81
|
+
}
|
|
82
|
+
return node.args[arg] === undefined;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (missingArgs.length > 0) {
|
|
86
|
+
transpilerError(
|
|
87
|
+
[
|
|
88
|
+
"{N}",
|
|
89
|
+
`<$yellow:Identifier$> <$blue:'${id}'$> <$yellow:is missing required arguments:$> <$red:${missingArgs.join(", ")}$>{N}`,
|
|
90
|
+
`<$blue:Please ensure these arguments are provided in the template usage.$>`
|
|
91
|
+
],
|
|
92
|
+
context
|
|
93
|
+
);
|
|
94
|
+
}
|
|
33
95
|
}
|
|
34
96
|
};
|
|
35
97
|
|
|
@@ -60,7 +122,17 @@ export function validateAST(ast, mapperFile, instance) {
|
|
|
60
122
|
|
|
61
123
|
// 1. Identify Target
|
|
62
124
|
if (node.id) {
|
|
63
|
-
|
|
125
|
+
let target = null;
|
|
126
|
+
const lowerId = node.id.toLowerCase();
|
|
127
|
+
if (["import", "$use-module", "slot", "for-each"].includes(lowerId)) {
|
|
128
|
+
target = {
|
|
129
|
+
id: lowerId,
|
|
130
|
+
options: { type: "Block" }
|
|
131
|
+
};
|
|
132
|
+
} else {
|
|
133
|
+
target = mapperFile.get(node.id) || (mapperFile.getUnknownTag ? mapperFile.getUnknownTag(node) : null);
|
|
134
|
+
}
|
|
135
|
+
|
|
64
136
|
if (target) {
|
|
65
137
|
runValidations(node, target, instance);
|
|
66
138
|
}
|
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
|
+
}
|