sommark 5.0.4 → 5.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.
@@ -1029,6 +1029,7 @@ function makeBlockNode() {
1029
1029
  structure: "Block",
1030
1030
  id: "",
1031
1031
  props: {},
1032
+ directives: {},
1032
1033
  body: [],
1033
1034
  depth: 0,
1034
1035
  range: {
@@ -1514,9 +1515,11 @@ function parseBlock(tokens, i, filename = null, placeholders = {}, variables = {
1514
1515
  i = valueIndex;
1515
1516
 
1516
1517
  // Store Argument
1517
- blockNode.props[String(argIndex++)] = v;
1518
- if (k) {
1519
- blockNode.props[k] = v;
1518
+ if (k && k.startsWith("smark-")) {
1519
+ blockNode.directives[k.slice(6)] = v; // strip "smark-" prefix
1520
+ } else {
1521
+ blockNode.props[String(argIndex++)] = v;
1522
+ if (k) blockNode.props[k] = v;
1520
1523
  }
1521
1524
  k = "";
1522
1525
  v = "";
package/esbuild.js ADDED
@@ -0,0 +1,64 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ /**
6
+ * esbuild plugin for SomMark.
7
+ *
8
+ * Handles QuickJS WASM assets that esbuild cannot process automatically.
9
+ * QuickJS loads its .wasm files via `new URL("*.wasm", import.meta.url)`
10
+ * inside pre-built package files. esbuild's file loader does not intercept
11
+ * these patterns, so this plugin rewrites the URLs and copies the .wasm
12
+ * files to the output directory.
13
+ *
14
+ * @example
15
+ * // esbuild.config.mjs
16
+ * import { sommarkEsbuild } from "sommark/esbuild";
17
+ * await esbuild.build({ plugins: [sommarkEsbuild()] });
18
+ */
19
+ export function sommarkEsbuild() {
20
+ const collected = new Map();
21
+
22
+ return {
23
+ name: "sommark",
24
+ setup(build) {
25
+ build.onStart(() => collected.clear());
26
+
27
+ build.onLoad({ filter: /emscripten-module(\.browser)?\.m?js$/ }, (args) => {
28
+ const source = readFileSync(args.path, "utf8");
29
+ const dir = dirname(args.path);
30
+
31
+ const pattern = /new URL\(['"]([^'"]+\.wasm)['"]\s*,\s*import\.meta\.url\)/g;
32
+ let result = source;
33
+ let match;
34
+
35
+ while ((match = pattern.exec(source)) !== null) {
36
+ const [full, wasmFile] = match;
37
+ const wasmPath = join(dir, wasmFile);
38
+ if (!existsSync(wasmPath)) continue;
39
+
40
+ let outputName;
41
+ if (collected.has(wasmPath)) {
42
+ outputName = collected.get(wasmPath).name;
43
+ } else {
44
+ const content = readFileSync(wasmPath);
45
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
46
+ outputName = `emscripten-module-${hash}.wasm`;
47
+ collected.set(wasmPath, { name: outputName, content });
48
+ }
49
+
50
+ result = result.replace(full, `new URL("./${outputName}", import.meta.url)`);
51
+ }
52
+
53
+ return result !== source ? { contents: result, loader: "js" } : undefined;
54
+ });
55
+
56
+ build.onEnd(({ errors }) => {
57
+ if (errors.length > 0) return;
58
+ for (const { name, content } of collected.values()) {
59
+ writeFileSync(join(build.initialOptions.outdir, name), content);
60
+ }
61
+ });
62
+ },
63
+ };
64
+ }
package/index.browser.js CHANGED
@@ -1,8 +1,11 @@
1
- import SomMark, { setDefaultFs, setDefaultCwd } from "./index.shared.js";
1
+ import SomMark, { setDefaultFs, setDefaultCwd, setDefaultEnv, setDefaultAsyncLocalStorage } from "./index.shared.js";
2
+ import { AsyncLocalStorage } from "./async-hooks.js";
2
3
  export * from "./index.shared.js";
3
4
 
4
5
  setDefaultFs(null);
5
6
  setDefaultCwd("/");
7
+ setDefaultEnv(null);
8
+ setDefaultAsyncLocalStorage(AsyncLocalStorage);
6
9
 
7
10
  /**
8
11
  * Resolves a relative path into a full URL using the current document location.
package/index.js CHANGED
@@ -1,6 +1,9 @@
1
- import SomMark, { setDefaultFs, setDefaultCwd, setDefaultFindAndLoadConfig, setDefaultResolvePath } from "./index.shared.js";
1
+ import SomMark, { setDefaultFs, setDefaultCwd, setDefaultFindAndLoadConfig, setDefaultResolvePath, setDefaultEnv, setDefaultAsyncLocalStorage } from "./index.shared.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  export * from "./index.shared.js";
3
4
 
5
+ setDefaultAsyncLocalStorage(AsyncLocalStorage);
6
+
4
7
  // Node-specific filesystem import
5
8
  let nodeFs = null;
6
9
  if (typeof process !== "undefined" && process.versions?.node) {
@@ -32,4 +35,32 @@ if (typeof process !== "undefined" && process.versions?.node) {
32
35
  }
33
36
  setDefaultFindAndLoadConfig(findAndLoadConfigFn);
34
37
 
38
+ // Load .env file from cwd and merge with process.env (process.env wins)
39
+ {
40
+ let mergedEnv = { ...process.env };
41
+ if (nodeFs) {
42
+ try {
43
+ const dotenvPath = nodeFs.promises
44
+ ? await nodeFs.promises.readFile(process.cwd() + "/.env", "utf8")
45
+ : null;
46
+ if (dotenvPath) {
47
+ for (const line of dotenvPath.split("\n")) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith("#")) continue;
50
+ const eq = trimmed.indexOf("=");
51
+ if (eq === -1) continue;
52
+ const key = trimmed.slice(0, eq).trim();
53
+ let val = trimmed.slice(eq + 1).trim();
54
+ if ((val.startsWith('"') && val.endsWith('"')) ||
55
+ (val.startsWith("'") && val.endsWith("'"))) {
56
+ val = val.slice(1, -1);
57
+ }
58
+ if (!(key in mergedEnv)) mergedEnv[key] = val;
59
+ }
60
+ }
61
+ } catch { /* .env file is optional */ }
62
+ }
63
+ setDefaultEnv(mergedEnv);
64
+ }
65
+
35
66
  export default SomMark;
package/index.shared.js CHANGED
@@ -42,6 +42,14 @@ export function setDefaultFs(fs) {
42
42
  Evaluator.setDefaultFs(fs);
43
43
  }
44
44
 
45
+ export function setDefaultEnv(env) {
46
+ Evaluator.setDefaultEnv(env);
47
+ }
48
+
49
+ export function setDefaultAsyncLocalStorage(cls) {
50
+ Evaluator.setDefaultAsyncLocalStorage(cls);
51
+ }
52
+
45
53
  export function setDefaultResolvePath(fn) {
46
54
  defaultResolvePath = fn;
47
55
  }
@@ -113,7 +121,8 @@ class SomMark {
113
121
  allowFetch: security?.allowFetch !== false,
114
122
  allowHttp: security?.allowHttp === true,
115
123
  allowedOrigins: Array.isArray(security?.allowedOrigins) ? security.allowedOrigins.map(o => o.toLowerCase()) : null,
116
- allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null
124
+ allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null,
125
+ env: Array.isArray(security?.env) ? security.env : []
117
126
  };
118
127
  this.warnings = [];
119
128
  this._prepared = false;
@@ -29,28 +29,18 @@ const MARKDOWN = Mapper.define({
29
29
  },
30
30
 
31
31
  /**
32
- * Provides a fallback for unknown tags by using the HTML mapper instead.
33
- */
32
+ * Provides a fallback for unknown tags by rendering them as HTML elements.
33
+ * Passes child nodes to the transpiler, which handles all node types (such as ForEach).
34
+ **/
34
35
  getUnknownTag(node) {
35
- const id = node.id.toLowerCase();
36
-
36
+ const id = node.id;
37
37
  return {
38
- render: async ({ props, ast, isSelfClosing, renderChild }) => {
38
+ options: { trimAndWrapBlocks: true },
39
+ render: ({ props, content, isSelfClosing }) => {
39
40
  const element = this.tag(id).smartAttributes(props, this.customProps, this.options);
40
41
  if (isSelfClosing || VOID_ELEMENTS.has(id)) return element.selfClose();
41
-
42
- let rawContent = "";
43
- for (const child of (ast.body || [])) {
44
- if (child.type === TEXT) rawContent += this.text(child.text);
45
- else if (child.type === BLOCK) rawContent += await renderChild(child);
46
- }
47
- rawContent = rawContent.trim();
48
-
49
- const meaningful = (ast.body || []).filter(c => c.type !== TEXT || c.text.trim());
50
- const finalContent = meaningful.length <= 1 ? rawContent : `\n${rawContent}\n`;
51
- return element.body(finalContent);
52
- },
53
- options: { handleAst: true }
42
+ return element.body(content);
43
+ }
54
44
  };
55
45
  }
56
46
  });
@@ -62,10 +52,14 @@ registerSharedOutputs(MARKDOWN);
62
52
  /**
63
53
  * Quote - Renders blockquote content or GFM alerts.
64
54
  */
65
- MARKDOWN.register("quote", ({ props, content }) => {
66
- const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
67
- return md.quote(content, type);
68
- }, { resolve: true });
55
+ MARKDOWN.register(
56
+ "quote",
57
+ ({ props, content }) => {
58
+ const type = safeArg({ props, index: 0, key: "type", fallBack: "" });
59
+ return md.quote(content, type);
60
+ },
61
+ { resolve: true }
62
+ );
69
63
 
70
64
  /**
71
65
  * Headings - Renders H1-H6 block headings.
@@ -145,12 +139,12 @@ MARKDOWN.register(
145
139
  "link",
146
140
  ({ props, content, isSelfClosing }) => {
147
141
  if (isSelfClosing) {
148
- const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
149
- const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
142
+ const text = safeArg({ props, index: 0, key: "text", fallBack: "" });
143
+ const src = safeArg({ props, index: 1, key: "src", fallBack: "" });
150
144
  const title = safeArg({ props, index: 2, key: "title", fallBack: "" });
151
145
  return md.url("link", text, src, title);
152
146
  }
153
- const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
147
+ const src = safeArg({ props, index: 0, key: "src", fallBack: "" });
154
148
  const title = safeArg({ props, index: 1, key: "title", fallBack: "" });
155
149
  return md.url("link", content, src, title);
156
150
  },
@@ -188,10 +182,14 @@ MARKDOWN.register(
188
182
  * Escape - Escapes special Markdown characters.
189
183
  * Self-closing: [escape = "text" !] or [escape = text: "text" !]
190
184
  */
191
- MARKDOWN.register(["escape", "e"], function ({ props, content, isSelfClosing }) {
192
- const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
193
- return this.md.escape(text);
194
- }, { resolve: true });
185
+ MARKDOWN.register(
186
+ ["escape", "e"],
187
+ function ({ props, content, isSelfClosing }) {
188
+ const text = isSelfClosing ? safeArg({ props, index: 0, key: "text", fallBack: "" }) : content;
189
+ return this.md.escape(text);
190
+ },
191
+ { resolve: true }
192
+ );
195
193
 
196
194
  const ROW_SEP = "\x1E";
197
195
  const CELL_SEP = "\x1F";
@@ -207,12 +205,16 @@ MARKDOWN.register(
207
205
  const headers = [];
208
206
  const rows = [];
209
207
 
210
- const extractRows = async (sectionNode) => {
208
+ const extractRows = async sectionNode => {
211
209
  const sectionRows = [];
212
- for (const child of (sectionNode.body || [])) {
210
+ for (const child of sectionNode.body || []) {
213
211
  if (child.type === BLOCK && child.id?.toLowerCase() === "row") {
214
212
  const rendered = await renderChild(child, { inTable: true });
215
- const cells = rendered.split(ROW_SEP)[0]?.split(CELL_SEP).filter(c => c !== "") ?? [];
213
+ const cells =
214
+ rendered
215
+ .split(ROW_SEP)[0]
216
+ ?.split(CELL_SEP)
217
+ .filter(c => c !== "") ?? [];
216
218
  if (cells.length > 0) sectionRows.push(cells);
217
219
  } else if (child.type === FOR_EACH) {
218
220
  const rendered = await renderChild(child, { inTable: true });
@@ -246,25 +248,29 @@ MARKDOWN.register(
246
248
  */
247
249
  MARKDOWN.register(["header", "body"], ({ content }) => content);
248
250
 
249
- MARKDOWN.register("row", async function ({ ast, renderChild, inTable }) {
250
- if (!inTable) {
251
- let result = "";
252
- for (const child of ast.body) {
253
- if (child.type === TEXT) result += this.text(child.text);
254
- else if (child.type === BLOCK) result += await renderChild(child);
251
+ MARKDOWN.register(
252
+ "row",
253
+ async function ({ ast, renderChild, inTable }) {
254
+ if (!inTable) {
255
+ let result = "";
256
+ for (const child of ast.body) {
257
+ if (child.type === TEXT) result += this.text(child.text);
258
+ else if (child.type === BLOCK) result += await renderChild(child);
259
+ }
260
+ return result;
255
261
  }
256
- return result;
257
- }
258
- let cells = "";
259
- for (const child of ast.body) {
260
- if (child.type !== BLOCK) continue;
261
- const id = child.id?.toLowerCase();
262
- if (id === "cell" || id === "th" || id === "td") {
263
- cells += await renderChild(child, { inTable: true });
262
+ let cells = "";
263
+ for (const child of ast.body) {
264
+ if (child.type !== BLOCK) continue;
265
+ const id = child.id?.toLowerCase();
266
+ if (id === "cell" || id === "th" || id === "td") {
267
+ cells += await renderChild(child, { inTable: true });
268
+ }
264
269
  }
265
- }
266
- return cells + ROW_SEP;
267
- }, { handleAst: true });
270
+ return cells + ROW_SEP;
271
+ },
272
+ { handleAst: true }
273
+ );
268
274
 
269
275
  MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
270
276
  return inTable ? content.trim() + CELL_SEP : content;
@@ -274,34 +280,42 @@ MARKDOWN.register(["cell", "th", "td"], ({ content, inTable }) => {
274
280
  * Lists - Authoritative Native AST List resolution.
275
281
  * Supports Ordered (Number) and Unordered (Dotlex) lists with deep nesting.
276
282
  */
277
- MARKDOWN.register(["list", "List"], async function ({ ast, props, renderChild }) {
278
- const indicator = safeArg({ props, index: 0, fallBack: "dot" });
279
- const isOrdered = indicator === "number" || indicator === "ol";
280
- const marker = isOrdered ? "" : (indicator === "dot" ? "-" : indicator);
281
- const items = [];
283
+ MARKDOWN.register(
284
+ ["list", "List"],
285
+ async function ({ ast, props, renderChild }) {
286
+ const indicator = safeArg({ props, index: 0, fallBack: "dot" });
287
+ const isOrdered = indicator === "number" || indicator === "ol";
288
+ const marker = isOrdered ? "" : indicator === "dot" ? "-" : indicator;
289
+ const items = [];
282
290
 
283
- for (const node of ast.body) {
284
- if (node.type !== BLOCK) continue;
285
- const id = node.id?.toLowerCase();
286
- if (id === "item") {
287
- items.push((await renderChild(node)).trim());
291
+ for (const node of ast.body) {
292
+ if (node.type !== BLOCK) continue;
293
+ const id = node.id?.toLowerCase();
294
+ if (id === "item") {
295
+ items.push((await renderChild(node)).trim());
296
+ }
288
297
  }
289
- }
290
298
 
291
- return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
292
- }, { handleAst: true, trimAndWrapBlocks: false });
299
+ return isOrdered ? md.orderedList(items, 0) : md.unorderedList(items, 0, marker);
300
+ },
301
+ { handleAst: true, trimAndWrapBlocks: false }
302
+ );
293
303
 
294
304
  /**
295
305
  * List Helpers - Internal tags for list structural organization.
296
306
  */
297
- MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
298
- let result = "";
299
- for (const child of ast.body) {
300
- if (child.type === TEXT) result += this.text(child.text);
301
- else if (child.type === BLOCK) result += await renderChild(child);
302
- }
303
- return result.trim();
304
- }, { handleAst: true, trimAndWrapBlocks: false });
307
+ MARKDOWN.register(
308
+ ["item", "Item"],
309
+ async function ({ ast, renderChild }) {
310
+ let result = "";
311
+ for (const child of ast.body) {
312
+ if (child.type === TEXT) result += this.text(child.text);
313
+ else if (child.type === BLOCK) result += await renderChild(child);
314
+ }
315
+ return result.trim();
316
+ },
317
+ { handleAst: true, trimAndWrapBlocks: false }
318
+ );
305
319
 
306
320
  /**
307
321
  * Todo - Renders task list items with status markers.
@@ -311,17 +325,21 @@ MARKDOWN.register(["item", "Item"], async function ({ ast, renderChild }) {
311
325
  * [todo = "Add feature", "x" !] positional self-closing (task, status)
312
326
  * [todo = "x"]Add feature[end] status in prop, task in body
313
327
  */
314
- MARKDOWN.register("todo", ({ props, content, isSelfClosing }) => {
315
- let status, task;
328
+ MARKDOWN.register(
329
+ "todo",
330
+ ({ props, content, isSelfClosing }) => {
331
+ let status, task;
316
332
 
317
- if (isSelfClosing) {
318
- task = safeArg({ props, index: 0, key: "task", fallBack: "" });
319
- status = safeArg({ props, index: 1, key: "status", fallBack: "" });
320
- } else {
321
- status = safeArg({ props, index: 0, fallBack: "" });
322
- task = content;
323
- }
333
+ if (isSelfClosing) {
334
+ task = safeArg({ props, index: 0, key: "task", fallBack: "" });
335
+ status = safeArg({ props, index: 1, key: "status", fallBack: "" });
336
+ } else {
337
+ status = safeArg({ props, index: 0, fallBack: "" });
338
+ task = content;
339
+ }
324
340
 
325
- return md.todo(status, task);
326
- }, { trimAndWrapBlocks: false });
341
+ return md.todo(status, task);
342
+ },
343
+ { trimAndWrapBlocks: false }
344
+ );
327
345
  export default MARKDOWN;
@@ -4,100 +4,115 @@ import { registerSharedOutputs } from "../shared/index.js";
4
4
  /**
5
5
  * Renders a standard XML tag based on the provided identifier and arguments.
6
6
  * Ensures strict attribute quoting and handles self-closing tags for empty bodies.
7
- *
7
+ *
8
8
  * @param {string} id - The XML tag identifier (case-sensitive).
9
9
  * @param {Object} props - Key-value pairs to be rendered as XML attributes.
10
10
  * @param {string} content - The rendered inner content of the tag.
11
11
  * @returns {string} The fully rendered XML tag string.
12
12
  */
13
13
  const renderXmlTag = function (id, props, content, isSelfClosing) {
14
- // XML is case-sensitive, so we use the exact id provided
15
- const element = this.tag(id);
14
+ // XML is case-sensitive, so we use the exact id provided
15
+ const element = this.tag(id);
16
16
 
17
- // Filter out positional indices (numeric keys) for XML attributes
18
- const namedArgs = {};
19
- Object.keys(props).forEach(key => {
20
- if (isNaN(parseInt(key))) {
21
- namedArgs[key] = props[key];
22
- }
23
- });
17
+ // Filter out positional indices (numeric keys) for XML attributes
18
+ const namedArgs = {};
19
+ Object.keys(props).forEach(key => {
20
+ if (isNaN(parseInt(key))) {
21
+ namedArgs[key] = props[key];
22
+ }
23
+ });
24
24
 
25
- // In XML, attributes must always have values (strict = true)
26
- element.attributes(namedArgs, true);
25
+ // In XML, attributes must always have values (strict = true)
26
+ element.attributes(namedArgs, true);
27
27
 
28
- const hasBody = typeof content === "string" && content.trim().length > 0;
28
+ const hasBody = typeof content === "string" && content.trim().length > 0;
29
29
 
30
- if (isSelfClosing || !hasBody) {
31
- return element.selfClose();
32
- }
30
+ if (isSelfClosing || !hasBody) {
31
+ return element.selfClose();
32
+ }
33
33
 
34
- return element.body(content);
34
+ return element.body(content);
35
35
  };
36
36
 
37
37
  /**
38
38
  * The XML Mapper used for creating XML pages.
39
39
  */
40
40
  const XML = Mapper.define({
41
- /**
42
- * Renders a comment in XML format.
43
- * @param {string} text - The comment content.
44
- * @returns {string}
45
- */
46
- comment(text) {
47
- return `<!-- ${text} -->`;
48
- },
41
+ /**
42
+ * Renders a comment in XML format.
43
+ * @param {string} text - The comment content.
44
+ * @returns {string}
45
+ */
46
+ comment(text) {
47
+ return `<!-- ${text} -->`;
48
+ },
49
49
 
50
- /**
51
- * Resolves unknown tags by preserving their original case and applying XML rules.
52
- * @param {Object} node - The AST node representing the unknown tag.
53
- * @returns {Object} Renderer definition for the tag.
54
- */
55
- getUnknownTag(node) {
56
- const id = node.id;
57
- return {
58
- render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
59
- options: {}
60
- };
61
- }
50
+ /**
51
+ * Resolves unknown tags by preserving their original case and applying XML rules.
52
+ * @param {Object} node - The AST node representing the unknown tag.
53
+ * @returns {Object} Renderer definition for the tag.
54
+ */
55
+ getUnknownTag(node) {
56
+ const id = node.id;
57
+ return {
58
+ render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
59
+ options: {}
60
+ };
61
+ },
62
+ options: {
63
+ trimAndWrapBlocks: true
64
+ }
62
65
  });
63
66
 
64
67
  /**
65
68
  * Registers the XML declaration as a self-closing block.
66
69
  * Usage: [xml = version: "1.0", encoding: "UTF-8"]
67
70
  */
68
- XML.register("xml", ({ props }) => {
69
- const version = props.version || "1.0";
70
- const encoding = props.encoding || "UTF-8";
71
- return `<?xml version="${version}" encoding="${encoding}"?>`;
72
- }, { rules: { is_empty_body: true } });
71
+ XML.register(
72
+ "xml",
73
+ ({ props }) => {
74
+ const version = props.version || "1.0";
75
+ const encoding = props.encoding || "UTF-8";
76
+ return `<?xml version="${version}" encoding="${encoding}"?>`;
77
+ },
78
+ { rules: { is_empty_body: true } }
79
+ );
73
80
 
74
81
  /**
75
82
  * Registers the DOCTYPE declaration.
76
83
  * Usage: [doctype = root: "note", system: "note.dtd"]
77
84
  */
78
- XML.register(["DOCTYPE", "doctype"], ({ props }) => {
79
- const root = props.root || "root";
80
- const system = props.system;
81
- const pub = props.public || props.fpi;
85
+ XML.register(
86
+ ["DOCTYPE", "doctype"],
87
+ ({ props }) => {
88
+ const root = props.root || "root";
89
+ const system = props.system;
90
+ const pub = props.public || props.fpi;
82
91
 
83
- if (pub && system) {
84
- return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
85
- } else if (system) {
86
- return `<!DOCTYPE ${root} SYSTEM "${system}">`;
87
- }
88
- return `<!DOCTYPE ${root}>`;
89
- }, { rules: { is_empty_body: true } });
92
+ if (pub && system) {
93
+ return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
94
+ } else if (system) {
95
+ return `<!DOCTYPE ${root} SYSTEM "${system}">`;
96
+ }
97
+ return `<!DOCTYPE ${root}>`;
98
+ },
99
+ { rules: { is_empty_body: true } }
100
+ );
90
101
 
91
102
  /**
92
103
  * Registers the XML stylesheet processing instruction.
93
104
  * Usage: [xml-stylesheet = href: "style.xsl"]
94
105
  */
95
- XML.register("xml-stylesheet", ({ props }) => {
96
- const type = props.type || "text/xsl";
97
- const href = props.href;
98
- if (!href) return "";
99
- return `<?xml-stylesheet type="${type}" href="${href}"?>`;
100
- }, { rules: { is_empty_body: true } });
106
+ XML.register(
107
+ "xml-stylesheet",
108
+ ({ props }) => {
109
+ const type = props.type || "text/xsl";
110
+ const href = props.href;
111
+ if (!href) return "";
112
+ return `<?xml-stylesheet type="${type}" href="${href}"?>`;
113
+ },
114
+ { rules: { is_empty_body: true } }
115
+ );
101
116
 
102
117
  /**
103
118
  * Registers CDATA sections.
@@ -105,8 +120,8 @@ XML.register("xml-stylesheet", ({ props }) => {
105
120
  * Self-closing: [cdata = "raw content" !] or [cdata = text: "raw content" !]
106
121
  */
107
122
  XML.register("cdata", ({ props, content, isSelfClosing }) => {
108
- const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
109
- return `<![CDATA[${text}]]>`;
123
+ const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
124
+ return `<![CDATA[${text}]]>`;
110
125
  });
111
126
 
112
127
  registerSharedOutputs(XML);
@@ -1,8 +1,17 @@
1
1
  /**
2
2
  * Registers universal utility blocks shared across all SomMark mappers.
3
- * These blocks are considered "Format Agnostic."
4
3
  *
5
4
  * @param {Mapper} mapper - The mapper instance to register tags on.
6
5
  */
7
6
  export function registerSharedOutputs(mapper) {
7
+ mapper.register(
8
+ ["raw", "Raw"],
9
+ ({ content }) => {
10
+ return content;
11
+ },
12
+ {
13
+ escape: false, rules: {
14
+ required_directives: ["raw"]
15
+ } }
16
+ );
8
17
  }
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "sommark",
3
- "version": "5.0.4",
3
+ "version": "5.1.0",
4
4
  "description": "SomMark is a template language that compiles to multiple output formats — HTML, JSON, YAML, TOML, CSV, Markdown, XML, and more.",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",
8
8
  "index.browser.js",
9
9
  "index.shared.js",
10
+ "async-hooks.js",
11
+ "vite.js",
12
+ "rollup.js",
13
+ "esbuild.js",
10
14
  "core/",
11
15
  "mappers/",
12
16
  "helpers/",
@@ -25,7 +29,10 @@
25
29
  "type": "module",
26
30
  "exports": {
27
31
  ".": "./index.js",
28
- "./browser": "./index.browser.js"
32
+ "./browser": "./index.browser.js",
33
+ "./vite": "./vite.js",
34
+ "./rollup": "./rollup.js",
35
+ "./esbuild": "./esbuild.js"
29
36
  },
30
37
  "scripts": {
31
38
  "test": "vitest",