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/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({ src, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], importStack = [] }) {
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]: new Mapper() };
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
- const ast = await this.parse(src);
214
- let result = await transpiler({ ast, format: this.targetFormat, mapperFile: this.mapperFile });
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
- return result;
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
- return await new SomMark({ src, filename, format: htmlFormat }).lex();
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 (!src) {
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
- return await new SomMark({ src, filename, format: htmlFormat }).parse();
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
- * The easiest way to process SomMark code.
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 knownProps = ["src", "format", "filename", "mapperFile", "removeComments", "placeholders", "customProps"];
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
- if (!src) {
287
- runtimeError([`{line}<$red:Missing Source:$> <$yellow:The 'src' argument is required for transpilation.$>{line}`]);
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({ src, format, filename, mapperFile, removeComments, placeholders, customProps });
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 => lexer(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, options = {}) => {
310
- const { format = htmlFormat, filename = "anonymous", mapperFile = null, removeComments = true, placeholders = {}, customProps = [] } = options;
311
- return new SomMark({ src, format, filename, mapperFile, removeComments, placeholders, customProps }).parseSync();
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;
@@ -1,6 +1,7 @@
1
1
  import Mapper from "../mapper.js";
2
2
  import { VOID_ELEMENTS } from "../../constants/void_elements.js";
3
3
  import { registerSharedOutputs } from "../shared/index.js";
4
+ import kebabize from "../../helpers/kebabize.js";
4
5
 
5
6
  /**
6
7
  * Helper to format an HTML tag with attributes and content.
@@ -10,17 +11,17 @@ import { registerSharedOutputs } from "../shared/index.js";
10
11
  * @param {string} content - The text or tags inside this tag.
11
12
  * @returns {string} - The finished HTML string.
12
13
  */
13
- const renderHtmlTag = function (id, args, content) {
14
+ const renderHtmlTag = function (id, args, content, isSelfClosing) {
14
15
  const element = this.tag(id);
15
16
 
16
- element.smartAttributes(args, this.customProps);
17
+ element.smartAttributes(args, this.customProps, this.options);
17
18
 
18
19
  let finalContent = content;
19
20
  if (id.toLowerCase() === "script" && args.scoped === true) {
20
21
  finalContent = `(function(){\n${content}\n})();`;
21
22
  }
22
23
 
23
- if (VOID_ELEMENTS.has(id.toLowerCase())) {
24
+ if (VOID_ELEMENTS.has(id.toLowerCase()) || isSelfClosing) {
24
25
  return element.selfClose();
25
26
  }
26
27
 
@@ -40,6 +41,22 @@ const HTML = Mapper.define({
40
41
  return `<!-- ${text} -->`;
41
42
  },
42
43
 
44
+ /**
45
+ * Natively formats runtime logic for HTML.
46
+ * Global logic is placed in a raw script tag.
47
+ * Block-level logic is wrapped in a self-executing function to isolate scope and provide `self` reference.
48
+ */
49
+ runtimeLogic(code, isGlobal, parentId) {
50
+ if (isGlobal) {
51
+ return this.tag("script").body(`\n${code.split("\n").filter(line => line.trim() !== "").join("\n")}\n`);
52
+ } else {
53
+ const selfDefinition = parentId
54
+ ? `const self = document.querySelector('[data-sommark-id="${parentId}"]');`
55
+ : `const self = document.currentScript.parentElement;`;
56
+ return this.tag("script").body(`\n(async function(){${selfDefinition}\nif (self) {\n${code.split("\n").filter(line => line.trim() !== "").join("\n")}\n}\n})();\n`);
57
+ }
58
+ },
59
+
43
60
  /**
44
61
  * Formats plain text and makes sure it's safe for HTML if needed.
45
62
  */
@@ -66,9 +83,6 @@ const HTML = Mapper.define({
66
83
  if (options?.escape !== false) {
67
84
  out = this.escapeHTML(out);
68
85
  }
69
- if (out.includes('\n')) {
70
- out = '\n' + out + '\n';
71
- }
72
86
  return out;
73
87
  },
74
88
 
@@ -83,7 +97,7 @@ const HTML = Mapper.define({
83
97
  const isCodeStyleOrScript = ["code", "style", "script"].includes(id);
84
98
 
85
99
  return {
86
- render: function ({ args, content }) { return renderHtmlTag.call(this, id, args, content); },
100
+ render: function ({ args, content, isSelfClosing }) { return renderHtmlTag.call(this, id, args, content, isSelfClosing); },
87
101
  options: {
88
102
  type: isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline"],
89
103
  escape: !isCodeStyleOrScript,
@@ -91,9 +105,9 @@ const HTML = Mapper.define({
91
105
  }
92
106
  };
93
107
  },
94
-
108
+
95
109
  options: {
96
- // trimAndWrapBlocks: false // Default to false for high-fidelity
110
+ trimAndWrapBlocks: true
97
111
  }
98
112
  });
99
113
 
@@ -127,7 +141,34 @@ HTML.register(
127
141
  type: "Block"
128
142
  }
129
143
  );
144
+ // Inline CSS tag (Moved from shared)
145
+ HTML.register("css", ({ args, content }) => {
146
+ // Compile style from named arguments (keys that are not numeric digits)
147
+ const namedStyle = Object.keys(args)
148
+ .filter(k => isNaN(parseInt(k)))
149
+ .map(k => `${kebabize(k)}:${args[k]}`)
150
+ .join(";");
151
+
152
+ // Fetch positional style string (index 0) or "style" key if present
153
+ let positionalStyle = HTML.safeArg({ args, index: 0, key: "style", fallBack: "" });
154
+
155
+ // Filter out positional styles that are just duplicates of named arguments
156
+ const hasDuplicateNamed = Object.keys(args)
157
+ .filter(k => isNaN(parseInt(k)))
158
+ .some(k => args[k] === positionalStyle);
159
+
160
+ if (hasDuplicateNamed) {
161
+ positionalStyle = "";
162
+ }
130
163
 
164
+ // Combine both together
165
+ let style = [positionalStyle, namedStyle].filter(s => s.trim()).join(";");
166
+
167
+ style = style.split(";").filter(s => s.trim()).map(s => s.trim().split(":").map(s => s.trim()).join(":")).join(";");
168
+ return HTML.tag("span").attributes({ style }).body(content);
169
+ }, {
170
+ type: "Inline"
171
+ });
131
172
  registerSharedOutputs(HTML);
132
173
 
133
174
  export default HTML;
@@ -9,7 +9,7 @@ import { getPositionalArgs, matchedValue, safeArg } from "../../helpers/utils.js
9
9
  /**
10
10
  * Returns a string representing the specified indentation level.
11
11
  */
12
- function getIndent(depth) {
12
+ export function getIndent(depth) {
13
13
  return " ".repeat(depth);
14
14
  }
15
15
 
@@ -18,21 +18,35 @@ function getIndent(depth) {
18
18
  * @param {string} str - The string to escape.
19
19
  * @param {boolean} [trim=false] - Whether to trim the string.
20
20
  */
21
- function escapeString(str, trim = false) {
21
+ export function escapeString(str, trim = false) {
22
22
  let out = String(str);
23
23
  if (trim) out = out.trim();
24
24
  return JSON.stringify(out);
25
25
  }
26
26
 
27
+ import evaluator from "../../core/evaluator.js";
28
+
27
29
  /**
28
30
  * Recursively extracts text content from a node, ignoring structural metadata.
31
+ * Evaluates StaticLogic nodes using the Evaluator to inject build-time values.
29
32
  */
30
- function getNodeText(node) {
33
+ async function getNodeText(node) {
31
34
  if (!node.body) return "";
32
35
  let text = "";
33
36
  for (const child of node.body) {
34
- if (child.type === "Text") text += child.text;
35
- else if (child.type === "Block") text += getNodeText(child);
37
+ if (child.type === "Text") text += child.text || "";
38
+ else if (child.type === "StaticLogic") {
39
+ try {
40
+ const result = await evaluator.execute(child.code);
41
+ if (result !== undefined && typeof result !== "object") {
42
+ text += String(result);
43
+ }
44
+ } catch (err) {
45
+ console.error(`\x1b[31mLogic Error in JSON mapper:\x1b[0m ${err.message}`);
46
+ console.error(`\x1b[33mCode:\x1b[0m \x1b[34m${child.code}\x1b[0m`);
47
+ }
48
+ }
49
+ else if (child.type === "Block") text += await getNodeText(child);
36
50
  }
37
51
  return text;
38
52
  }
@@ -40,7 +54,9 @@ function getNodeText(node) {
40
54
  /**
41
55
  * Resolves the key-value pairing for a JSON member.
42
56
  */
43
- function renderMember(args, value) {
57
+ export function renderMember(args, value, inArray = false) {
58
+ if (inArray) return value;
59
+
44
60
  const posArgs = getPositionalArgs(args);
45
61
  const key = args.key || posArgs[0]; // The 'key' rule determines the member name
46
62
 
@@ -53,37 +69,63 @@ function renderMember(args, value) {
53
69
  /**
54
70
  * Formats a given node and tracks its indentation.
55
71
  */
56
- async function renderNode(node, mapper, depth = 0) {
72
+ export async function renderNode(node, mapper, depth = 0, inArray = false) {
57
73
  const target = matchedValue(mapper.outputs, node.id) || mapper.getUnknownTag(node);
58
74
  if (!target) return "";
59
75
 
60
- const textContent = getNodeText(node);
61
- return await target.render.call(mapper, {
76
+ evaluator.pushScope();
77
+ const textContent = await getNodeText(node);
78
+ const output = await target.render.call(mapper, {
62
79
  nodeType: node.type,
63
80
  args: node.args,
64
81
  content: "",
65
82
  textContent,
66
83
  ast: node,
67
- depth
84
+ depth,
85
+ inArray
68
86
  });
87
+ await evaluator.popScope();
88
+ return output;
69
89
  }
70
90
 
71
91
  /**
72
92
  * Formats the children of a node into a neat list.
73
93
  */
74
- async function renderChildren(node, mapper, depth = 0) {
94
+ export async function renderChildren(node, mapper, depth = 0, inArray = false) {
75
95
  let results = [];
76
96
  const childIndent = getIndent(depth + 1);
77
97
 
78
98
  for (const child of node.body) {
79
99
  if (child.type === "Block") {
80
- const output = await renderNode(child, mapper, depth + 1);
100
+ const output = await renderNode(child, mapper, depth + 1, inArray);
81
101
  if (output) {
82
- results.push(childIndent + output);
102
+ results.push({ type: "Block", value: childIndent + output });
103
+ }
104
+ }
105
+ }
106
+
107
+ let finalOutput = "";
108
+ for (let i = 0; i < results.length; i++) {
109
+ const current = results[i];
110
+ finalOutput += current.value;
111
+
112
+ if (current.type === "Block") {
113
+ // Add comma if there is another Block later
114
+ let hasNextBlock = false;
115
+ for (let j = i + 1; j < results.length; j++) {
116
+ if (results[j].type === "Block") {
117
+ hasNextBlock = true;
118
+ break;
119
+ }
83
120
  }
121
+ if (hasNextBlock) finalOutput += ",";
122
+ }
123
+
124
+ if (i < results.length - 1) {
125
+ finalOutput += "\n";
84
126
  }
85
127
  }
86
- return results.join(",\n");
128
+ return finalOutput;
87
129
  }
88
130
 
89
131
  const Json = Mapper.define({});
@@ -91,52 +133,53 @@ const Json = Mapper.define({});
91
133
  /**
92
134
  * The JSON object node rule.
93
135
  */
94
- Json.register(["Object", "object"], async ({ args, ast, depth = 0 }) => {
95
- if (ast.body.length === 0) return renderMember(args, "{}");
96
- const content = await renderChildren(ast, Json, depth);
136
+ Json.register(["Object", "object"], async ({ args, ast, depth = 0, inArray = false }) => {
137
+ if (ast.body.length === 0) return renderMember(args, "{}", inArray);
138
+ const content = await renderChildren(ast, Json, depth, false);
97
139
  const val = `{\n${content}\n${getIndent(depth)}}`;
98
- return renderMember(args, val);
140
+ return renderMember(args, val, inArray);
99
141
  }, { type: "Block", handleAst: true });
100
142
 
101
143
  /**
102
144
  * The JSON array node rule.
103
145
  */
104
- Json.register(["Array", "array"], async ({ args, ast, depth = 0 }) => {
105
- if (ast.body.length === 0) return renderMember(args, "[]");
106
- const content = await renderChildren(ast, Json, depth);
146
+ Json.register(["Array", "array"], async ({ args, ast, depth = 0, inArray = false }) => {
147
+ if (ast.body.length === 0) return renderMember(args, "[]", inArray);
148
+ const content = await renderChildren(ast, Json, depth, true);
107
149
  const val = `[\n${content}\n${getIndent(depth)}]`;
108
- return renderMember(args, val);
150
+ return renderMember(args, val, inArray);
109
151
  }, { type: "Block", handleAst: true });
110
152
 
111
153
  /**
112
154
  * JSON Primitives
113
155
  */
114
- Json.register("string", ({ args, textContent }) => {
115
- const trim = safeArg({
116
- args,
117
- key: "trim",
118
- type: "boolean",
156
+ Json.register("string", ({ args, textContent, inArray }) => {
157
+ const trim = safeArg({
158
+ args,
159
+ key: "trim",
160
+ type: "boolean",
119
161
  setType: v => v === "true" || v === true,
120
- fallBack: false
162
+ fallBack: false
121
163
  });
122
- const val = escapeString(textContent, trim);
123
- return renderMember(args, val);
164
+ const raw = safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent });
165
+ const val = escapeString(raw, trim);
166
+ return renderMember(args, val, inArray);
124
167
  }, { type: "Block", handleAst: true });
125
168
 
126
- Json.register("number", ({ args, textContent }) => {
127
- const raw = textContent.trim();
169
+ Json.register("number", ({ args, textContent, inArray }) => {
170
+ const raw = String(safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent })).trim();
128
171
  const val = (isNaN(Number(raw)) || raw === "") ? "0" : raw;
129
- return renderMember(args, val);
172
+ return renderMember(args, val, inArray);
130
173
  }, { type: "Block", handleAst: true });
131
174
 
132
- Json.register("bool", ({ args, textContent }) => {
133
- const raw = textContent.trim().toLowerCase();
175
+ Json.register("bool", ({ args, textContent, inArray }) => {
176
+ const raw = String(safeArg({ args, index: inArray ? 0 : undefined, key: "value", fallBack: textContent })).trim().toLowerCase();
134
177
  const val = (raw === "true" || raw === "1") ? "true" : "false";
135
- return renderMember(args, val);
178
+ return renderMember(args, val, inArray);
136
179
  }, { type: "Block", handleAst: true });
137
180
 
138
- Json.register("null", ({ args }) => {
139
- return renderMember(args, "null");
181
+ Json.register("null", ({ args, inArray }) => {
182
+ return renderMember(args, "null", inArray);
140
183
  }, { type: "Block", handleAst: true });
141
184
 
142
185
  export default Json;