sommark 4.5.3 → 5.0.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.
@@ -28,12 +28,11 @@ const MDX = Mapper.define({
28
28
 
29
29
  return {
30
30
  render: (ctx) => {
31
- const { args, content, isSelfClosing } = ctx;
32
- const element = this.tag(tagName).jsxProps(args);
31
+ const { props, content, isSelfClosing } = ctx;
32
+ const element = this.tag(tagName).jsxProps(props);
33
33
  return (isSelfClosing || isVoid) ? element.selfClose() : element.body(content);
34
34
  },
35
35
  options: {
36
- type: isVoid ? "Block" : (isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline", "AtBlock"]),
37
36
  escape: !isCodeStyleOrScript,
38
37
  rules: { is_empty_body: isVoid }
39
38
  }
@@ -50,32 +49,11 @@ const MDX = Mapper.define({
50
49
  text(text, options) {
51
50
  let out = text;
52
51
  if (options?.escape !== false) {
53
- out = this.escapeHTML(out);
52
+ out = this.md.mdxEscaper(out);
54
53
  }
55
54
  return out;
56
55
  },
57
56
 
58
- /**
59
- * Formats inline content before rendering, respecting explicit escape flags.
60
- */
61
- inlineText(text, options) {
62
- let out = text;
63
- if (options?.escape !== false) {
64
- out = this.escapeHTML(out);
65
- }
66
- return out;
67
- },
68
-
69
- /**
70
- * Formats the literal content inside AtBlocks.
71
- */
72
- atBlockBody(text, options) {
73
- let out = text;
74
- if (options?.escape !== false) {
75
- out = this.escapeHTML(out);
76
- }
77
- return out;
78
- }
79
57
  });
80
58
 
81
59
  const { tag } = MDX;
@@ -84,47 +62,14 @@ MDX.inherit(MARKDOWN);
84
62
  MDX.md = MARKDOWN.md;
85
63
 
86
64
  ["h1", "h2", "h3", "h4", "h5", "h6"].forEach(h => {
87
- MDX.register(h, function ({ args, content }) {
88
- const format = this.safeArg({ args, key: "format", fallBack: "" });
65
+ MDX.register(h, function ({ props, content }) {
66
+ const format = this.safeArg({ props, key: "format", fallBack: "" });
89
67
  if (format === "md" || format === "markdown") {
90
68
  return this.md.heading(content, h.slice(1) || 1);
91
69
  }
92
- delete args.format;
93
- return tag(h).jsxProps(args).body(content);
70
+ delete props.format;
71
+ return tag(h).jsxProps(props).body(content);
94
72
  });
95
73
  });
96
74
 
97
- /**
98
- * mdx AtBlock - Renders raw MDX content (ESM imports, exports, or complex JSX).
99
- */
100
- MDX.register("mdx", ({ content }) => {
101
- return content;
102
- }, { escape: false, type: "AtBlock" });
103
-
104
- // Inline CSS tag (Moved from shared)
105
- MDX.register("css", ({ args, content }) => {
106
- // Compile style from named arguments (keys that are not numeric digits)
107
- const namedStyle = Object.keys(args)
108
- .filter(k => isNaN(parseInt(k)))
109
- .map(k => `${k}:${args[k]}`)
110
- .join(";");
111
-
112
- // Fetch positional style string (index 0) or "style" key if present
113
- let positionalStyle = MDX.safeArg({ args, index: 0, key: "style", fallBack: "" });
114
-
115
- // Filter out positional styles that are just duplicates of named arguments
116
- const hasDuplicateNamed = Object.keys(args)
117
- .filter(k => isNaN(parseInt(k)))
118
- .some(k => args[k] === positionalStyle);
119
-
120
- if (hasDuplicateNamed) {
121
- positionalStyle = "";
122
- }
123
-
124
- // Combine both together
125
- let style = [positionalStyle, namedStyle].filter(s => s.trim()).join(";");
126
-
127
- return MDX.tag("span").jsxProps({ style }).body(content);
128
- }, { type: "Inline" });
129
-
130
75
  export default MDX;
@@ -36,30 +36,13 @@ const TEXT = Mapper.define({
36
36
  return text;
37
37
  },
38
38
 
39
- /**
40
- * Returns inline text literally.
41
- */
42
- inlineText(text) {
43
- return text;
44
- },
45
-
46
- /**
47
- * Returns at-block body text literally.
48
- */
49
- atBlockBody(text) {
50
- return text;
51
- },
52
-
53
39
  /**
54
40
  * Fallback for all tags - extracts inner content.
55
41
  */
56
- getUnknownTag(node) {
57
- const isBlock = node.type === "Block" || node.type === "ForEach";
42
+ getUnknownTag() {
58
43
  return {
59
44
  render: ({ content }) => content,
60
- options: {
61
- type: isBlock ? "Block" : (node.type === "AtBlock" ? "AtBlock" : "Inline")
62
- }
45
+ options: {}
63
46
  };
64
47
  }
65
48
  });
@@ -0,0 +1,231 @@
1
+ import Mapper from "../mapper.js";
2
+ import { safeArg } from "../../helpers/utils.js";
3
+ import { registerSharedOutputs } from "../shared/index.js";
4
+ import { transpilerError } from "../../core/errors.js";
5
+
6
+ const isValidInt = (v) => v !== "" && !isNaN(Number(v)) && !v.includes(".");
7
+ const isValidFloat = (v) => v !== "" && !isNaN(Number(v)) && v.includes(".");
8
+ const isValidNumber = (v) => v !== "" && !isNaN(Number(v));
9
+
10
+ // Escape a string value for use inside TOML basic strings
11
+ const tomlEscapeString = (str) =>
12
+ String(str ?? "")
13
+ .replace(/\\/g, "\\\\")
14
+ .replace(/"/g, '\\"')
15
+ .replace(/\x08/g, "\\b")
16
+ .replace(/\f/g, "\\f")
17
+ .replace(/\n/g, "\\n")
18
+ .replace(/\r/g, "\\r")
19
+ .replace(/\t/g, "\\t");
20
+
21
+ // Quote a bare key segment if it contains characters outside [A-Za-z0-9_-]
22
+ const toBareKey = (k) => /^[A-Za-z0-9_-]+$/.test(k) ? k : `"${tomlEscapeString(k)}"`;
23
+
24
+ // Handle dotted keys like "database.host" → database.host
25
+ const tomlKey = (key) => String(key).split(".").map(toBareKey).join(".");
26
+
27
+ const ITEM_SEP = "\x1F";
28
+
29
+ const TOML = Mapper.define({
30
+ comment(text) {
31
+ return `# ${text}`;
32
+ },
33
+ text(text) {
34
+ return text.trim() === "" ? "" : text;
35
+ }
36
+ });
37
+
38
+ /**
39
+ * [str] / [string] — string key-value pair or array string value
40
+ *
41
+ * Key-value: [str = "title", "My App" !] → title = "My App"
42
+ * Body form: [str = "description"]Long text[end] → description = "Long text"
43
+ * In array: [str = "hello" !] → "hello"
44
+ */
45
+ TOML.register(["str", "string"], ({ props, textContent, inArray = false }) => {
46
+ const value = inArray
47
+ ? safeArg({ props, index: 0, key: "value", fallBack: textContent.trim() })
48
+ : safeArg({ props, index: 1, key: "value", fallBack: textContent.trim() });
49
+ const escaped = `"${tomlEscapeString(String(value))}"`;
50
+ if (inArray) return escaped + ITEM_SEP;
51
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
52
+ return `${tomlKey(key)} = ${escaped}\n`;
53
+ }, { handleAst: true });
54
+
55
+ /**
56
+ * [int] / [integer] — integer key-value pair or array integer value
57
+ *
58
+ * Key-value: [int = "port", "5432" !] → port = 5432
59
+ * In array: [int = "5432" !] → 5432
60
+ */
61
+ TOML.register(["int", "integer"], ({ props, textContent, inArray = false }) => {
62
+ const raw = String(safeArg({ props, index: inArray ? 0 : 1, key: "value", fallBack: textContent.trim() })).trim();
63
+ if (!isValidInt(raw))
64
+ transpilerError(`<$yellow:[int]$> expects a whole number but got <$yellow:'${raw}'$>.{N}Use <$cyan:[float]$> for decimal numbers or <$cyan:[number]$> for either.`);
65
+ if (inArray) return raw + ITEM_SEP;
66
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
67
+ return `${tomlKey(key)} = ${raw}\n`;
68
+ }, { handleAst: true });
69
+
70
+ TOML.register("float", ({ props, textContent, inArray = false }) => {
71
+ const raw = String(safeArg({ props, index: inArray ? 0 : 1, key: "value", fallBack: textContent.trim() })).trim();
72
+ if (!isValidFloat(raw))
73
+ transpilerError(`<$yellow:[float]$> expects a decimal number but got <$yellow:'${raw}'$>.{N}Use <$cyan:[int]$> for whole numbers or <$cyan:[number]$> for either.`);
74
+ if (inArray) return raw + ITEM_SEP;
75
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
76
+ return `${tomlKey(key)} = ${raw}\n`;
77
+ }, { handleAst: true });
78
+
79
+ TOML.register("number", ({ props, textContent, inArray = false }) => {
80
+ const raw = String(safeArg({ props, index: inArray ? 0 : 1, key: "value", fallBack: textContent.trim() })).trim();
81
+ if (!isValidNumber(raw))
82
+ transpilerError(`<$yellow:[number]$> expects a numeric value but got <$yellow:'${raw}'$>.`);
83
+ if (inArray) return raw + ITEM_SEP;
84
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
85
+ return `${tomlKey(key)} = ${raw}\n`;
86
+ }, { handleAst: true });
87
+
88
+ /**
89
+ * [bool] / [boolean] — boolean key-value pair or array bool value
90
+ *
91
+ * Key-value: [bool = "debug", "false" !] → debug = false
92
+ * In array: [bool = "true" !] → true
93
+ */
94
+ TOML.register(["bool", "boolean"], ({ props, textContent, inArray = false }) => {
95
+ const raw = String(
96
+ inArray
97
+ ? safeArg({ props, index: 0, key: "value", fallBack: textContent.trim() })
98
+ : safeArg({ props, index: 1, key: "value", fallBack: textContent.trim() })
99
+ ).trim().toLowerCase();
100
+ const value = (raw === "true" || raw === "1" || raw === "yes") ? "true" : "false";
101
+ if (inArray) return value + ITEM_SEP;
102
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
103
+ return `${tomlKey(key)} = ${value}\n`;
104
+ }, { handleAst: true });
105
+
106
+ /**
107
+ * [datetime] — datetime key-value pair
108
+ * TOML datetimes are bare (unquoted): 1979-05-27T07:32:00Z
109
+ *
110
+ * [datetime = "born", "1979-05-27T07:32:00Z" !] → born = 1979-05-27T07:32:00Z
111
+ */
112
+ TOML.register("datetime", ({ props, textContent, inArray = false }) => {
113
+ const raw = String(
114
+ inArray
115
+ ? safeArg({ props, index: 0, key: "value", fallBack: textContent.trim() })
116
+ : safeArg({ props, index: 1, key: "value", fallBack: textContent.trim() })
117
+ ).trim();
118
+ if (inArray) return raw + ITEM_SEP;
119
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
120
+ return `${tomlKey(key)} = ${raw}\n`;
121
+ }, { handleAst: true });
122
+
123
+ /**
124
+ * [table] / [section] — renders a TOML table header + children
125
+ *
126
+ * [table = "database"]
127
+ * [str = "host", "localhost" !]
128
+ * [int = "port", "5432" !]
129
+ * [end]
130
+ *
131
+ * →
132
+ *
133
+ * [database]
134
+ * host = "localhost"
135
+ * port = 5432
136
+ */
137
+ TOML.register(["table", "section"], async ({ props, ast, renderChild }) => {
138
+ const name = safeArg({ props, index: 0, key: "name", fallBack: "" });
139
+ const parts = [];
140
+ for (const child of ast.body) {
141
+ const out = await renderChild(child);
142
+ if (out != null && out.trim() !== "") parts.push(out);
143
+ }
144
+ return `[${tomlKey(name)}]\n${parts.join("")}\n`;
145
+ }, { handleAst: true });
146
+
147
+ /**
148
+ * [array-table] — renders a TOML array of tables entry
149
+ *
150
+ * [array-table = "servers"]
151
+ * [str = "name", "alpha" !]
152
+ * [str = "ip", "10.0.0.1" !]
153
+ * [end]
154
+ *
155
+ * →
156
+ *
157
+ * [[servers]]
158
+ * name = "alpha"
159
+ * ip = "10.0.0.1"
160
+ */
161
+ TOML.register("array-table", async ({ props, ast, renderChild }) => {
162
+ const name = safeArg({ props, index: 0, key: "name", fallBack: "" });
163
+ const parts = [];
164
+ for (const child of ast.body) {
165
+ const out = await renderChild(child);
166
+ if (out != null && out.trim() !== "") parts.push(out);
167
+ }
168
+ return `[[${tomlKey(name)}]]\n${parts.join("")}\n`;
169
+ }, { handleAst: true });
170
+
171
+ /**
172
+ * [array] — renders a TOML inline array
173
+ * Children are rendered with inArray: true so they output just their value.
174
+ *
175
+ * [array = "ports"]
176
+ * [int = "8001" !][int = "8002" !][int = "8003" !]
177
+ * [end]
178
+ *
179
+ * → ports = [8001, 8002, 8003]
180
+ *
181
+ * Works with [for-each] for dynamic arrays:
182
+ * [array = "tags"]
183
+ * [for-each = ${ tags }$, as: "item"][str = ${ item }$ !][end]
184
+ * [end]
185
+ */
186
+ /**
187
+ * Unknown tag — tag name becomes the TOML key, first positional arg is the value.
188
+ * Type is inferred: number → raw integer/float, true/false → boolean, everything else → quoted string.
189
+ *
190
+ * [name = "Adam" !] → name = "Adam"
191
+ * [port = 5432 !] → port = 5432
192
+ * [debug = false !] → debug = false
193
+ * [bio]Long text[end] → bio = "Long text"
194
+ */
195
+ TOML.getUnknownTag = function (node) {
196
+ const key = node.id;
197
+ return {
198
+ render({ props, textContent, inArray = false }) {
199
+ const raw = String(
200
+ safeArg({ props, index: 0, key: "value", fallBack: textContent.trim() })
201
+ ).trim();
202
+
203
+ let val;
204
+ if (raw === "true" || raw === "false") {
205
+ val = raw;
206
+ } else if (raw !== "" && !isNaN(Number(raw))) {
207
+ val = raw;
208
+ } else {
209
+ val = `"${tomlEscapeString(raw)}"`;
210
+ }
211
+
212
+ if (inArray) return val + ITEM_SEP;
213
+ return `${tomlKey(key)} = ${val}\n`;
214
+ },
215
+ options: { handleAst: true }
216
+ };
217
+ };
218
+
219
+ TOML.register("array", async ({ props, ast, renderChild }) => {
220
+ const key = safeArg({ props, index: 0, key: "key", fallBack: "" });
221
+ let combined = "";
222
+ for (const child of ast.body) {
223
+ combined += await renderChild(child, { inArray: true });
224
+ }
225
+ const vals = combined.split(ITEM_SEP).filter(v => v.trim() !== "");
226
+ return `${tomlKey(key)} = [${vals.join(", ")}]\n`;
227
+ }, { handleAst: true });
228
+
229
+ registerSharedOutputs(TOML);
230
+
231
+ export default TOML;
@@ -6,19 +6,19 @@ import { registerSharedOutputs } from "../shared/index.js";
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
- * @param {Object} args - Key-value pairs to be rendered as XML attributes.
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
- const renderXmlTag = function (id, args, content, isSelfClosing) {
13
+ const renderXmlTag = function (id, props, content, isSelfClosing) {
14
14
  // XML is case-sensitive, so we use the exact id provided
15
15
  const element = this.tag(id);
16
16
 
17
17
  // Filter out positional indices (numeric keys) for XML attributes
18
18
  const namedArgs = {};
19
- Object.keys(args).forEach(key => {
19
+ Object.keys(props).forEach(key => {
20
20
  if (isNaN(parseInt(key))) {
21
- namedArgs[key] = args[key];
21
+ namedArgs[key] = props[key];
22
22
  }
23
23
  });
24
24
 
@@ -55,10 +55,8 @@ const XML = Mapper.define({
55
55
  getUnknownTag(node) {
56
56
  const id = node.id;
57
57
  return {
58
- render: ({ args, content, isSelfClosing }) => renderXmlTag.call(this, id, args, content, isSelfClosing),
59
- options: {
60
- type: "any"
61
- }
58
+ render: ({ props, content, isSelfClosing }) => renderXmlTag.call(this, id, props, content, isSelfClosing),
59
+ options: {}
62
60
  };
63
61
  }
64
62
  });
@@ -67,20 +65,20 @@ const XML = Mapper.define({
67
65
  * Registers the XML declaration as a self-closing block.
68
66
  * Usage: [xml = version: "1.0", encoding: "UTF-8"]
69
67
  */
70
- XML.register("xml", ({ args }) => {
71
- const version = args.version || "1.0";
72
- const encoding = args.encoding || "UTF-8";
68
+ XML.register("xml", ({ props }) => {
69
+ const version = props.version || "1.0";
70
+ const encoding = props.encoding || "UTF-8";
73
71
  return `<?xml version="${version}" encoding="${encoding}"?>`;
74
- }, { type: "Block", rules: { is_empty_body: true } });
72
+ }, { rules: { is_empty_body: true } });
75
73
 
76
74
  /**
77
75
  * Registers the DOCTYPE declaration.
78
76
  * Usage: [doctype = root: "note", system: "note.dtd"]
79
77
  */
80
- XML.register(["DOCTYPE", "doctype"], ({ args }) => {
81
- const root = args.root || "root";
82
- const system = args.system;
83
- const pub = args.public || args.fpi;
78
+ XML.register(["DOCTYPE", "doctype"], ({ props }) => {
79
+ const root = props.root || "root";
80
+ const system = props.system;
81
+ const pub = props.public || props.fpi;
84
82
 
85
83
  if (pub && system) {
86
84
  return `<!DOCTYPE ${root} PUBLIC "${pub}" "${system}">`;
@@ -88,26 +86,28 @@ XML.register(["DOCTYPE", "doctype"], ({ args }) => {
88
86
  return `<!DOCTYPE ${root} SYSTEM "${system}">`;
89
87
  }
90
88
  return `<!DOCTYPE ${root}>`;
91
- }, { type: "Block", rules: { is_empty_body: true } });
89
+ }, { rules: { is_empty_body: true } });
92
90
 
93
91
  /**
94
92
  * Registers the XML stylesheet processing instruction.
95
93
  * Usage: [xml-stylesheet = href: "style.xsl"]
96
94
  */
97
- XML.register("xml-stylesheet", ({ args }) => {
98
- const type = args.type || "text/xsl";
99
- const href = args.href;
95
+ XML.register("xml-stylesheet", ({ props }) => {
96
+ const type = props.type || "text/xsl";
97
+ const href = props.href;
100
98
  if (!href) return "";
101
99
  return `<?xml-stylesheet type="${type}" href="${href}"?>`;
102
- }, { type: "Block", rules: { is_self_closing: true } });
100
+ }, { rules: { is_empty_body: true } });
103
101
 
104
102
  /**
105
103
  * Registers CDATA sections.
106
- * Usage: @_cdata_@: ; raw content @_end_@
104
+ * Body form: [cdata]raw content[end]
105
+ * Self-closing: [cdata = "raw content" !] or [cdata = text: "raw content" !]
107
106
  */
108
- XML.register("cdata", ({ content }) => {
109
- return `<![CDATA[${content}]]>`;
110
- }, { type: "AtBlock" });
107
+ XML.register("cdata", ({ props, content, isSelfClosing }) => {
108
+ const text = isSelfClosing ? (props[0] ?? props.text ?? "") : content;
109
+ return `<![CDATA[${text}]]>`;
110
+ });
111
111
 
112
112
  registerSharedOutputs(XML);
113
113