sommark 4.5.2 → 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.
- package/README.md +314 -178
- package/cli/cli.mjs +1 -1
- package/cli/commands/color.js +36 -14
- package/cli/commands/help.js +3 -0
- package/cli/commands/init.js +0 -2
- package/cli/constants.js +5 -2
- package/constants/html_props.js +66 -1
- package/constants/svg_elements.js +31 -0
- package/core/errors.js +5 -4
- package/core/evaluator.js +1 -2
- package/core/formats.js +7 -1
- package/core/helpers/config-loader.js +1 -3
- package/core/helpers/lib.js +1 -1
- package/core/labels.js +2 -15
- package/core/lexer.js +197 -313
- package/core/modules.js +13 -13
- package/core/parser.js +226 -535
- package/core/tokenTypes.js +6 -15
- package/core/transpiler.js +129 -110
- package/core/validator.js +6 -26
- package/dist/sommark.browser.js +1939 -2223
- package/dist/sommark.browser.lite.js +1937 -2220
- package/dist/sommark.lexer.js +392 -544
- package/dist/sommark.parser.js +604 -1200
- package/formatter/mark.js +34 -0
- package/formatter/tag.js +7 -33
- package/helpers/utils.js +15 -16
- package/index.js +9 -1
- package/index.shared.js +22 -12
- package/mappers/languages/csv.js +62 -0
- package/mappers/languages/html.js +21 -69
- package/mappers/languages/json.js +74 -156
- package/mappers/languages/jsonc.js +21 -63
- package/mappers/languages/markdown.js +159 -276
- package/mappers/languages/mdx.js +7 -62
- package/mappers/languages/text.js +2 -19
- package/mappers/languages/toml.js +231 -0
- package/mappers/languages/xml.js +25 -25
- package/mappers/languages/yaml.js +323 -0
- package/mappers/mapper.js +1 -22
- package/mappers/shared/index.js +3 -16
- package/package.json +5 -2
package/formatter/mark.js
CHANGED
|
@@ -188,8 +188,42 @@ class MarkdownBuilder {
|
|
|
188
188
|
return result;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Escapes Markdown trigger characters for MDX output using HTML entities instead of
|
|
193
|
+
* backslashes. Backslash escapes render literally inside JSX text children, so entities
|
|
194
|
+
* are the only reliable way to neutralise Markdown symbols in MDX.
|
|
195
|
+
*/
|
|
196
|
+
mdxEscaper(text) {
|
|
197
|
+
if (!text) return "";
|
|
198
|
+
|
|
199
|
+
// 1. HTML tags → entities (before & escaping to avoid double-encoding)
|
|
200
|
+
let result = text.replace(/<([a-zA-Z\/][^>]*?)>/g, "<$1>");
|
|
201
|
+
|
|
202
|
+
// 2. Ampersands and quotes
|
|
203
|
+
result = result
|
|
204
|
+
.replace(/&(?!lt;|gt;)/g, "&")
|
|
205
|
+
.replace(/"/g, """)
|
|
206
|
+
.replace(/'/g, "'");
|
|
191
207
|
|
|
208
|
+
// 3. Unordered list triggers: -, *, + at start of line followed by space
|
|
209
|
+
result = result.replace(/^([-*+])(\s+)/gm, (_, c, sp) => `&#${c.codePointAt(0)};${sp}`);
|
|
192
210
|
|
|
211
|
+
// 4. Ordered list triggers: 1. at start of line — encode the dot
|
|
212
|
+
result = result.replace(/^(\d+)\.(\s+)/gm, (_, n, sp) => `${n}.${sp}`);
|
|
213
|
+
|
|
214
|
+
// 5. Emphasis triggers: *text*, **text**, _text_, ~~text~~
|
|
215
|
+
result = result.replace(/(\*+|_+|~~)(\S[\s\S]*?\S)\1/g, (_, prefix, content) => {
|
|
216
|
+
const enc = prefix.split("").map(c => `&#${c.codePointAt(0)};`).join("");
|
|
217
|
+
return enc + content + enc;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 6. Horizontal rule triggers: ---, ***, ___ on their own line
|
|
221
|
+
result = result.replace(/^([*_-]{3,})\s*$/gm, (m) =>
|
|
222
|
+
m.replace(/[*_-]/g, c => `&#${c.codePointAt(0)};`)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
193
227
|
|
|
194
228
|
/**
|
|
195
229
|
* Formats data as a Markdown table.
|
package/formatter/tag.js
CHANGED
|
@@ -69,10 +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();
|
|
74
72
|
|
|
75
|
-
// 1. Initial
|
|
73
|
+
// 1. Initial style processing
|
|
76
74
|
if (!isCodeStyleOrScript && args.style) {
|
|
77
75
|
if (typeof args.style === "object") {
|
|
78
76
|
inline_style = Object.entries(args.style)
|
|
@@ -85,20 +83,11 @@ class TagBuilder {
|
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
// 2.
|
|
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
|
|
86
|
+
// 2. Attribute dispatching
|
|
96
87
|
const keys = Object.keys(args).filter(arg => isNaN(parseInt(arg)));
|
|
97
88
|
keys.forEach(key => {
|
|
98
|
-
if (!isNaN(parseInt(key))) return; // Skip numeric positional arguments
|
|
99
89
|
if (key === "style") return;
|
|
100
90
|
if (isCodeStyleOrScript && key === "scoped") return;
|
|
101
|
-
if (useClassFallback && key === "class") return;
|
|
102
91
|
|
|
103
92
|
const isDimensionAttributeSupported = ["img", "video", "svg", "canvas", "iframe", "object", "embed"].includes(id);
|
|
104
93
|
const isWidthOrHeight = key === "width" || key === "height";
|
|
@@ -108,38 +97,23 @@ class TagBuilder {
|
|
|
108
97
|
const isDataOrAria = kebabize(key).startsWith("data-") || kebabize(key).startsWith("aria-");
|
|
109
98
|
|
|
110
99
|
const k = isEvent ? key.toLowerCase() : (isNative || isCustom) ? key : kebabize(key);
|
|
100
|
+
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
111
101
|
|
|
112
102
|
if (isCodeStyleOrScript || options.fallbackTarget === false) {
|
|
113
|
-
// Specialized tags or fallback disabled: render standard attributes
|
|
114
|
-
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
103
|
+
// Specialized tags or fallback disabled: render as standard attributes
|
|
115
104
|
this.#attr.push(`${k}="${escapeHTML(String(val))}"`);
|
|
116
105
|
} else {
|
|
117
|
-
// Standard elements: process smart fallbacks
|
|
118
106
|
if (isEvent || ((isNative || isCustom) && (!isWidthOrHeight || isDimensionAttributeSupported)) || isDataOrAria) {
|
|
119
|
-
const val = typeof args[key] === "object" ? JSON.stringify(args[key]) : args[key];
|
|
120
107
|
this.#attr.push(`${k}="${escapeHTML(String(val))}"`);
|
|
121
108
|
} else {
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
}
|
|
109
|
+
// Unknown attribute: fall through to inline style
|
|
110
|
+
inline_style += `${k}:${val};`;
|
|
133
111
|
}
|
|
134
112
|
}
|
|
135
113
|
});
|
|
136
114
|
|
|
137
|
-
if (useClassFallback && classSet.size > 0) {
|
|
138
|
-
this.#attr.push(`class="${escapeHTML([...classSet].join(" "))}"`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
115
|
if (inline_style) {
|
|
142
|
-
//
|
|
116
|
+
// Wrap CSS variables in var()
|
|
143
117
|
const processedStyle = inline_style.replace(/(^|[^\w\-_$])(--[\w\-_$]+)(?![\w\-_$]|:)/g, "$1var($2)");
|
|
144
118
|
this.#attr.push(`style="${escapeHTML(processedStyle)}"`);
|
|
145
119
|
}
|
package/helpers/utils.js
CHANGED
|
@@ -90,9 +90,9 @@ export function matchedValue(outputs, targetId) {
|
|
|
90
90
|
* @param {any} [options.fallBack=null] - Value to return if resolution or validation fails.
|
|
91
91
|
* @returns {any} - The resolved argument value or the fallback.
|
|
92
92
|
*/
|
|
93
|
-
export function safeArg({
|
|
94
|
-
if (typeof
|
|
95
|
-
sommarkError([`{line}<$red:TypeError:$> <$yellow:
|
|
93
|
+
export function safeArg({ props, index, key, type = null, setType = null, fallBack = null }) {
|
|
94
|
+
if (typeof props !== 'object' || props === null) {
|
|
95
|
+
sommarkError([`{line}<$red:TypeError:$> <$yellow:props must be an object$>{line}`]);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (index === undefined && key === undefined) {
|
|
@@ -102,7 +102,6 @@ export function safeArg({ args, index, key, type = null, setType = null, fallBac
|
|
|
102
102
|
const validate = value => {
|
|
103
103
|
if (value === undefined) return false;
|
|
104
104
|
|
|
105
|
-
// Handle explicit type check functions (e.g., isObject, isArray)
|
|
106
105
|
if (typeof type === 'function') {
|
|
107
106
|
return type(value);
|
|
108
107
|
}
|
|
@@ -112,30 +111,30 @@ export function safeArg({ args, index, key, type = null, setType = null, fallBac
|
|
|
112
111
|
return typeof evaluated === type;
|
|
113
112
|
};
|
|
114
113
|
|
|
115
|
-
if (index !== undefined && validate(
|
|
116
|
-
return
|
|
114
|
+
if (index !== undefined && validate(props[index])) {
|
|
115
|
+
return props[index];
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
if (key !== undefined && validate(
|
|
120
|
-
return
|
|
118
|
+
if (key !== undefined && validate(props[key])) {
|
|
119
|
+
return props[key];
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
return fallBack;
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
/**
|
|
127
|
-
* Extracts
|
|
128
|
-
*
|
|
129
|
-
* @param {Object}
|
|
130
|
-
* @returns {Array<any>} - An ordered array of positional
|
|
126
|
+
* Extracts positional props from a block node's props object.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object} props - The block node's props object.
|
|
129
|
+
* @returns {Array<any>} - An ordered array of positional prop values.
|
|
131
130
|
*/
|
|
132
|
-
export function getPositionalArgs(
|
|
133
|
-
if (!
|
|
134
|
-
const keys = Object.keys(
|
|
131
|
+
export function getPositionalArgs(props) {
|
|
132
|
+
if (!props) return [];
|
|
133
|
+
const keys = Object.keys(props);
|
|
135
134
|
const result = keys
|
|
136
135
|
.filter(k => !isNaN(parseInt(k)))
|
|
137
136
|
.sort((a, b) => parseInt(a) - parseInt(b))
|
|
138
|
-
.map(k =>
|
|
137
|
+
.map(k => props[k]);
|
|
139
138
|
|
|
140
139
|
return result;
|
|
141
140
|
}
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import SomMark, { setDefaultFs, setDefaultCwd, setDefaultFindAndLoadConfig } from "./index.shared.js";
|
|
1
|
+
import SomMark, { setDefaultFs, setDefaultCwd, setDefaultFindAndLoadConfig, setDefaultResolvePath } from "./index.shared.js";
|
|
2
2
|
export * from "./index.shared.js";
|
|
3
3
|
|
|
4
4
|
// Node-specific filesystem import
|
|
@@ -14,6 +14,14 @@ if (typeof process !== "undefined" && process.versions?.node) {
|
|
|
14
14
|
setDefaultFs(nodeFs);
|
|
15
15
|
if (typeof process !== "undefined" && process.cwd) setDefaultCwd(process.cwd());
|
|
16
16
|
|
|
17
|
+
// Resolve filenames to absolute paths for clear error messages
|
|
18
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
19
|
+
try {
|
|
20
|
+
const nodePath = await import("node:path");
|
|
21
|
+
setDefaultResolvePath(nodePath.resolve.bind(nodePath));
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
// Node-specific config-loader import
|
|
18
26
|
let findAndLoadConfigFn = async () => ({});
|
|
19
27
|
if (typeof process !== "undefined" && process.versions?.node) {
|
package/index.shared.js
CHANGED
|
@@ -11,9 +11,12 @@ import MDX from "./mappers/languages/mdx.js";
|
|
|
11
11
|
import Json from "./mappers/languages/json.js";
|
|
12
12
|
import Jsonc from "./mappers/languages/jsonc.js";
|
|
13
13
|
import XML from "./mappers/languages/xml.js";
|
|
14
|
+
import CSV from "./mappers/languages/csv.js";
|
|
15
|
+
import TOML from "./mappers/languages/toml.js";
|
|
16
|
+
import YAML from "./mappers/languages/yaml.js";
|
|
14
17
|
import TEXT from "./mappers/languages/text.js";
|
|
15
18
|
import { runtimeError } from "./core/errors.js";
|
|
16
|
-
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, jsoncFormat, xmlFormat } from "./core/formats.js";
|
|
19
|
+
import FORMATS, { textFormat, htmlFormat, markdownFormat, mdxFormat, jsonFormat, jsoncFormat, xmlFormat, csvFormat, tomlFormat, yamlFormat } from "./core/formats.js";
|
|
17
20
|
import TOKEN_TYPES from "./core/tokenTypes.js";
|
|
18
21
|
import * as labels from "./core/labels.js";
|
|
19
22
|
import { resolveModules } from "./core/modules.js";
|
|
@@ -26,6 +29,7 @@ import { preprocessRuntimeLogic } from "./core/helpers/preprocessor.js";
|
|
|
26
29
|
let defaultFs = null;
|
|
27
30
|
let defaultCwd = "/";
|
|
28
31
|
let defaultFindAndLoadConfig = async () => ({});
|
|
32
|
+
let defaultResolvePath = (p) => p; // identity in browser; overridden to path.resolve in Node.js
|
|
29
33
|
|
|
30
34
|
const isURL = (s) => typeof s === "string" && /^https?:\/\//.test(s);
|
|
31
35
|
|
|
@@ -38,6 +42,12 @@ export function setDefaultFs(fs) {
|
|
|
38
42
|
Evaluator.setDefaultFs(fs);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
export function setDefaultResolvePath(fn) {
|
|
46
|
+
defaultResolvePath = fn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolveFilename = (f) => (f && f !== "anonymous") ? defaultResolvePath(f) : f;
|
|
50
|
+
|
|
41
51
|
export function setDefaultFindAndLoadConfig(fn) {
|
|
42
52
|
defaultFindAndLoadConfig = fn;
|
|
43
53
|
}
|
|
@@ -64,18 +74,16 @@ class SomMark {
|
|
|
64
74
|
* @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
|
|
65
75
|
*/
|
|
66
76
|
constructor(options = {}) {
|
|
67
|
-
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 = {},
|
|
77
|
+
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 = {}, dualOutput = false, moduleIdentityToken = null } = options;
|
|
68
78
|
this.rawSettings = options;
|
|
69
79
|
this.src = src;
|
|
70
80
|
this.ast = ast;
|
|
71
81
|
this.targetFormat = format;
|
|
72
82
|
this.mapperFile = mapperFile;
|
|
73
|
-
this.filename = filename;
|
|
83
|
+
this.filename = resolveFilename(filename);
|
|
74
84
|
this.removeComments = removeComments;
|
|
75
85
|
this.placeholders = placeholders;
|
|
76
86
|
this.customProps = customProps;
|
|
77
|
-
this.generateRuntimeOutput = generateRuntimeOutput;
|
|
78
|
-
this.hideRuntimeOutput = hideRuntimeOutput;
|
|
79
87
|
this.dualOutput = dualOutput;
|
|
80
88
|
this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
|
|
81
89
|
this.fs = options.fs
|
|
@@ -115,7 +123,7 @@ class SomMark {
|
|
|
115
123
|
|
|
116
124
|
this.Mapper = Mapper;
|
|
117
125
|
|
|
118
|
-
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [jsoncFormat]: Jsonc, [xmlFormat]: XML, [textFormat]: TEXT };
|
|
126
|
+
const mapperFiles = { [htmlFormat]: HTML, [markdownFormat]: MARKDOWN, [mdxFormat]: MDX, [jsonFormat]: Json, [jsoncFormat]: Jsonc, [xmlFormat]: XML, [csvFormat]: CSV, [tomlFormat]: TOML, [yamlFormat]: YAML, [textFormat]: TEXT };
|
|
119
127
|
|
|
120
128
|
if (!this.mapperFile && this.targetFormat) {
|
|
121
129
|
const DefaultMapper = mapperFiles[this.targetFormat];
|
|
@@ -290,8 +298,6 @@ class SomMark {
|
|
|
290
298
|
mapperFile: this.mapperFile,
|
|
291
299
|
security: this.security,
|
|
292
300
|
settings: this.rawSettings,
|
|
293
|
-
generateRuntimeOutput: this.generateRuntimeOutput,
|
|
294
|
-
hideRuntimeOutput: this.hideRuntimeOutput,
|
|
295
301
|
dualOutput: this.dualOutput,
|
|
296
302
|
instance: this
|
|
297
303
|
});
|
|
@@ -324,7 +330,7 @@ const lex = async (src, filename = "anonymous") => {
|
|
|
324
330
|
if (typeof src !== "string") {
|
|
325
331
|
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
326
332
|
}
|
|
327
|
-
return lexer(src, filename);
|
|
333
|
+
return lexer(src, resolveFilename(filename));
|
|
328
334
|
};
|
|
329
335
|
|
|
330
336
|
/**
|
|
@@ -391,7 +397,7 @@ const lexSync = (src, filename = "anonymous") => {
|
|
|
391
397
|
if (typeof src !== "string") {
|
|
392
398
|
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
393
399
|
}
|
|
394
|
-
return lexer(src, filename);
|
|
400
|
+
return lexer(src, resolveFilename(filename));
|
|
395
401
|
};
|
|
396
402
|
|
|
397
403
|
/**
|
|
@@ -408,8 +414,9 @@ const parseSync = (src, filename = "anonymous") => {
|
|
|
408
414
|
if (typeof src !== "string") {
|
|
409
415
|
runtimeError([`{line}<$red:Invalid Source Type:$> <$yellow:The 'src' argument must be a string, received ${typeof src}.$>{line}`]);
|
|
410
416
|
}
|
|
411
|
-
const
|
|
412
|
-
|
|
417
|
+
const resolved = resolveFilename(filename);
|
|
418
|
+
const tokens = lexer(src, resolved);
|
|
419
|
+
return parser(tokens, resolved);
|
|
413
420
|
};
|
|
414
421
|
|
|
415
422
|
async function findAndLoadConfig(targetPath) {
|
|
@@ -427,6 +434,9 @@ export {
|
|
|
427
434
|
Json,
|
|
428
435
|
Jsonc,
|
|
429
436
|
XML,
|
|
437
|
+
CSV,
|
|
438
|
+
TOML,
|
|
439
|
+
YAML,
|
|
430
440
|
Mapper,
|
|
431
441
|
FORMATS,
|
|
432
442
|
lex,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import Mapper from "../mapper.js";
|
|
2
|
+
import { registerSharedOutputs } from "../shared/index.js";
|
|
3
|
+
|
|
4
|
+
const csvEscape = (value) => {
|
|
5
|
+
const str = String(value ?? "").trim();
|
|
6
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
7
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
9
|
+
return str;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const rowFromProps = (props) =>
|
|
13
|
+
Object.keys(props)
|
|
14
|
+
.filter(k => !isNaN(parseInt(k)))
|
|
15
|
+
.sort((a, b) => parseInt(a) - parseInt(b))
|
|
16
|
+
.map(k => csvEscape(props[k]))
|
|
17
|
+
.join(",");
|
|
18
|
+
|
|
19
|
+
const renderRow = async ({ props, ast, isSelfClosing, renderChild }) => {
|
|
20
|
+
if (isSelfClosing) return rowFromProps(props) + "\n";
|
|
21
|
+
const cells = [];
|
|
22
|
+
for (const child of ast.body.filter(c => c.type === "Block")) {
|
|
23
|
+
const out = await renderChild(child);
|
|
24
|
+
if (out != null && out !== "") cells.push(out);
|
|
25
|
+
}
|
|
26
|
+
return cells.join(",") + "\n";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const CSV = Mapper.define({
|
|
30
|
+
comment(text) {
|
|
31
|
+
return `# ${text}`;
|
|
32
|
+
},
|
|
33
|
+
text(text) {
|
|
34
|
+
return text.trim() === "" ? "" : text;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* [header] — header row
|
|
40
|
+
* Self-closing: [header = "name", "age", "city" !]
|
|
41
|
+
* Body form: [header][col]name[end][col]age[end][end]
|
|
42
|
+
*/
|
|
43
|
+
CSV.register(["header", "thead"], renderRow, { handleAst: true });
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* [row] / [tr] — data row
|
|
47
|
+
* Self-closing: [row = "Alice", "30", "New York" !]
|
|
48
|
+
* Body form: [row][col]Alice[end][col]30[end][end]
|
|
49
|
+
*/
|
|
50
|
+
CSV.register(["row", "tr"], renderRow, { handleAst: true });
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* [col] / [cell] / [td] — single cell, used inside body-form rows
|
|
54
|
+
* [col]New York, NY[end]
|
|
55
|
+
*/
|
|
56
|
+
CSV.register(["col", "cell", "td"], ({ textContent }) => {
|
|
57
|
+
return csvEscape(textContent);
|
|
58
|
+
}, { handleAst: true, trimAndWrapBlocks: false });
|
|
59
|
+
|
|
60
|
+
registerSharedOutputs(CSV);
|
|
61
|
+
|
|
62
|
+
export default CSV;
|
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import Mapper from "../mapper.js";
|
|
2
2
|
import { VOID_ELEMENTS } from "../../constants/void_elements.js";
|
|
3
|
+
import { SVG_ELEMENTS } from "../../constants/svg_elements.js";
|
|
3
4
|
import { registerSharedOutputs } from "../shared/index.js";
|
|
4
|
-
import kebabize from "../../helpers/kebabize.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Helper to format an HTML tag with attributes and content.
|
|
8
8
|
*
|
|
9
9
|
* @param {string} id - The name of the HTML tag.
|
|
10
|
-
* @param {Object}
|
|
10
|
+
* @param {Object} props - The attributes for the tag.
|
|
11
11
|
* @param {string} content - The text or tags inside this tag.
|
|
12
12
|
* @returns {string} - The finished HTML string.
|
|
13
13
|
*/
|
|
14
|
-
const renderHtmlTag = function (id,
|
|
14
|
+
const renderHtmlTag = function (id, props, content, isSelfClosing) {
|
|
15
15
|
const element = this.tag(id);
|
|
16
|
+
const idLower = id.toLowerCase();
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
if (SVG_ELEMENTS.has(idLower)) {
|
|
19
|
+
element.attributes(props);
|
|
20
|
+
} else {
|
|
21
|
+
element.smartAttributes(props, this.customProps, this.options);
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
let finalContent = content;
|
|
20
|
-
if (
|
|
25
|
+
if (idLower === "script" && props.scoped === true) {
|
|
21
26
|
finalContent = `(function(){\n${content}\n})();`;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
if (VOID_ELEMENTS.has(
|
|
29
|
+
if (VOID_ELEMENTS.has(idLower) || isSelfClosing) {
|
|
25
30
|
return element.selfClose();
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -65,43 +70,21 @@ const HTML = Mapper.define({
|
|
|
65
70
|
return this.escapeHTML(text);
|
|
66
71
|
},
|
|
67
72
|
|
|
68
|
-
/**
|
|
69
|
-
* Formats text inside inline tags (like bold or links).
|
|
70
|
-
*/
|
|
71
|
-
inlineText(text, options) {
|
|
72
|
-
if (options?.escape !== false) {
|
|
73
|
-
return this.escapeHTML(text);
|
|
74
|
-
}
|
|
75
|
-
return text;
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Formats the content inside AtBlocks.
|
|
80
|
-
*/
|
|
81
|
-
atBlockBody(text, options) {
|
|
82
|
-
let out = String(text);
|
|
83
|
-
if (options?.escape !== false) {
|
|
84
|
-
out = this.escapeHTML(out);
|
|
85
|
-
}
|
|
86
|
-
return out;
|
|
87
|
-
},
|
|
88
|
-
|
|
89
73
|
/**
|
|
90
74
|
* Provides high-fidelity fallback for unknown ids by rendering them as HTML elements.
|
|
91
75
|
* @param {Object} node - The unknown AST node.
|
|
92
76
|
* @returns {Object} - A virtual id registration for fallback rendering.
|
|
93
77
|
*/
|
|
94
78
|
getUnknownTag(node) {
|
|
95
|
-
const
|
|
96
|
-
const isVoid = VOID_ELEMENTS.has(
|
|
97
|
-
const isCodeStyleOrScript = ["code", "style", "script"].includes(
|
|
79
|
+
const idLower = node.id.toLowerCase();
|
|
80
|
+
const isVoid = VOID_ELEMENTS.has(idLower);
|
|
81
|
+
const isCodeStyleOrScript = ["code", "style", "script"].includes(idLower);
|
|
98
82
|
|
|
99
83
|
return {
|
|
100
|
-
render: function ({
|
|
84
|
+
render: function ({ props, content, isSelfClosing }) { return renderHtmlTag.call(this, node.id, props, content, isSelfClosing); },
|
|
101
85
|
options: {
|
|
102
|
-
type: isCodeStyleOrScript ? ["Block", "AtBlock"] : ["Block", "Inline"],
|
|
103
86
|
escape: !isCodeStyleOrScript,
|
|
104
|
-
rules: {
|
|
87
|
+
rules: { is_empty_body: isVoid }
|
|
105
88
|
}
|
|
106
89
|
};
|
|
107
90
|
},
|
|
@@ -114,7 +97,7 @@ const HTML = Mapper.define({
|
|
|
114
97
|
// DOCTYPE tag
|
|
115
98
|
HTML.register(["DOCTYPE", "doctype"], () => {
|
|
116
99
|
return "<!DOCTYPE html>";
|
|
117
|
-
}, {
|
|
100
|
+
}, { rules: { is_empty_body: true } });
|
|
118
101
|
|
|
119
102
|
// head tag
|
|
120
103
|
HTML.register("head", function ({ content }) {
|
|
@@ -123,52 +106,21 @@ HTML.register("head", function ({ content }) {
|
|
|
123
106
|
varsStyle = `<style>:root { ${this.cssVariables} }</style>\n`;
|
|
124
107
|
}
|
|
125
108
|
return this.tag("head").body(`${varsStyle}${content}`);
|
|
126
|
-
}, {
|
|
109
|
+
}, { escape: false });
|
|
127
110
|
|
|
128
111
|
// Root tag for Metadata and CSS Variables (Collector)
|
|
129
112
|
HTML.register(
|
|
130
113
|
["Root", "root"],
|
|
131
|
-
function ({
|
|
114
|
+
function ({ props }) {
|
|
132
115
|
this.cssVariables = this.cssVariables || "";
|
|
133
|
-
Object.keys(
|
|
116
|
+
Object.keys(props).forEach(key => {
|
|
134
117
|
if (key.startsWith("--")) {
|
|
135
|
-
this.cssVariables += `${key}:${
|
|
118
|
+
this.cssVariables += `${key}:${props[key]};`;
|
|
136
119
|
}
|
|
137
120
|
});
|
|
138
121
|
return "";
|
|
139
122
|
},
|
|
140
|
-
{
|
|
141
|
-
type: "Block"
|
|
142
|
-
}
|
|
143
123
|
);
|
|
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
|
-
}
|
|
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
|
-
});
|
|
172
124
|
registerSharedOutputs(HTML);
|
|
173
125
|
|
|
174
126
|
export default HTML;
|