sommark 5.0.5 → 5.2.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/async-hooks.js +19 -0
- package/core/evaluator.js +212 -22
- package/core/helpers/config-loader.js +29 -9
- package/core/helpers/lib.js +1 -1
- package/core/helpers/preprocessor.js +19 -0
- package/core/modules.js +59 -21
- package/core/parser.js +6 -3
- package/core/pathe-bundle.js +1 -0
- package/core/transpiler.js +130 -15
- package/core/validator.js +17 -4
- package/dist/sommark.browser.js +669 -214
- package/dist/sommark.browser.lite.js +458 -191
- package/dist/sommark.parser.js +6 -3
- package/esbuild.js +64 -0
- package/index.browser.js +4 -1
- package/index.js +102 -1
- package/index.shared.js +13 -2
- package/mappers/languages/markdown.js +99 -81
- package/mappers/languages/xml.js +76 -61
- package/mappers/shared/index.js +10 -1
- package/package.json +11 -3
- package/rollup.js +79 -0
- package/vite.js +20 -0
package/dist/sommark.parser.js
CHANGED
|
@@ -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
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
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,10 @@
|
|
|
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";
|
|
3
|
+
import { resolve } from "pathe";
|
|
2
4
|
export * from "./index.shared.js";
|
|
3
5
|
|
|
6
|
+
setDefaultAsyncLocalStorage(AsyncLocalStorage);
|
|
7
|
+
|
|
4
8
|
// Node-specific filesystem import
|
|
5
9
|
let nodeFs = null;
|
|
6
10
|
if (typeof process !== "undefined" && process.versions?.node) {
|
|
@@ -9,6 +13,13 @@ if (typeof process !== "undefined" && process.versions?.node) {
|
|
|
9
13
|
// Add async interface so modules.js can use await fs.exists / await fs.readFile
|
|
10
14
|
nodeFs.exists = (p) => nodeFs.promises.access(p).then(() => true).catch(() => false);
|
|
11
15
|
nodeFs.readFile = (p, enc) => nodeFs.promises.readFile(p, enc);
|
|
16
|
+
nodeFs.stat = (p) => nodeFs.promises.stat(p);
|
|
17
|
+
nodeFs.__isNodeFs = true;
|
|
18
|
+
nodeFs.glob = async (pattern, opts) => {
|
|
19
|
+
const results = [];
|
|
20
|
+
for await (const f of nodeFs.promises.glob(pattern, opts)) results.push(f);
|
|
21
|
+
return results;
|
|
22
|
+
};
|
|
12
23
|
} catch (e) {}
|
|
13
24
|
}
|
|
14
25
|
setDefaultFs(nodeFs);
|
|
@@ -32,4 +43,94 @@ if (typeof process !== "undefined" && process.versions?.node) {
|
|
|
32
43
|
}
|
|
33
44
|
setDefaultFindAndLoadConfig(findAndLoadConfigFn);
|
|
34
45
|
|
|
46
|
+
// Load .env file from cwd and merge with process.env (process.env wins)
|
|
47
|
+
{
|
|
48
|
+
let mergedEnv = { ...process.env };
|
|
49
|
+
if (nodeFs) {
|
|
50
|
+
try {
|
|
51
|
+
const dotenvPath = nodeFs.promises
|
|
52
|
+
? await nodeFs.promises.readFile(process.cwd() + "/.env", "utf8")
|
|
53
|
+
: null;
|
|
54
|
+
if (dotenvPath) {
|
|
55
|
+
for (const line of dotenvPath.split("\n")) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
58
|
+
const eq = trimmed.indexOf("=");
|
|
59
|
+
if (eq === -1) continue;
|
|
60
|
+
const key = trimmed.slice(0, eq).trim();
|
|
61
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
62
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
63
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
64
|
+
val = val.slice(1, -1);
|
|
65
|
+
}
|
|
66
|
+
if (!(key in mergedEnv)) mergedEnv[key] = val;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch { /* .env file is optional */ }
|
|
70
|
+
}
|
|
71
|
+
setDefaultEnv(mergedEnv);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const fileHandler = {
|
|
75
|
+
async read(filePath) {
|
|
76
|
+
if (!nodeFs) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"[SomMark] fileHandler is not available in browser mode.\n" +
|
|
79
|
+
"File access is a server-side concept."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
const abs = resolve(cwd, filePath);
|
|
84
|
+
if (!abs.startsWith(cwd)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[SomMark] fileHandler.read: path traversal outside project root is not allowed.\n` +
|
|
87
|
+
`Attempted path: ${abs}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return nodeFs.readFile(abs, "utf-8");
|
|
91
|
+
},
|
|
92
|
+
async exists(filePath) {
|
|
93
|
+
if (!nodeFs) return false;
|
|
94
|
+
const cwd = process.cwd();
|
|
95
|
+
const abs = resolve(cwd, filePath);
|
|
96
|
+
if (!abs.startsWith(cwd)) return false;
|
|
97
|
+
return nodeFs.exists(abs);
|
|
98
|
+
},
|
|
99
|
+
async glob(pattern) {
|
|
100
|
+
if (!nodeFs) throw new Error("[SomMark] fileHandler.glob is not available in browser mode.\nFile access is a server-side concept.");
|
|
101
|
+
if (!nodeFs.glob) throw new Error("[SomMark] fileHandler.glob requires Node.js 22 or later.");
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
return nodeFs.glob(pattern, { cwd });
|
|
104
|
+
},
|
|
105
|
+
async stat(filePath) {
|
|
106
|
+
if (!nodeFs) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"[SomMark] fileHandler.stat is not available in browser mode.\n" +
|
|
109
|
+
"File access is a server-side concept."
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const cwd = process.cwd();
|
|
113
|
+
const abs = resolve(cwd, filePath);
|
|
114
|
+
if (!abs.startsWith(cwd)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[SomMark] fileHandler.stat: path traversal outside project root is not allowed.\n` +
|
|
117
|
+
`Attempted path: ${abs}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const s = await nodeFs.stat(abs);
|
|
122
|
+
return {
|
|
123
|
+
size: s.size,
|
|
124
|
+
mtime: s.mtimeMs,
|
|
125
|
+
ctime: s.ctimeMs,
|
|
126
|
+
atime: s.atimeMs,
|
|
127
|
+
isFile: s.isFile(),
|
|
128
|
+
isDirectory: s.isDirectory(),
|
|
129
|
+
};
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
35
136
|
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
|
}
|
|
@@ -74,7 +82,7 @@ class SomMark {
|
|
|
74
82
|
* @param {string} [options.baseDir=null] - The base directory for resolving relative paths.
|
|
75
83
|
*/
|
|
76
84
|
constructor(options = {}) {
|
|
77
|
-
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = true, outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, dualOutput = false, moduleIdentityToken = null } = options;
|
|
85
|
+
const { src, ast = null, format, mapperFile = null, filename = "anonymous", removeComments = true, placeholders = {}, customProps = [], fallbackTarget = true, outputValidator = null, importAliases = {}, importStack = [], baseDir = null, moduleCache = null, showSpinner = true, security = {}, dualOutput = false, webOutputs = false, moduleIdentityToken = null } = options;
|
|
78
86
|
this.rawSettings = options;
|
|
79
87
|
this.src = src;
|
|
80
88
|
this.ast = ast;
|
|
@@ -85,6 +93,7 @@ class SomMark {
|
|
|
85
93
|
this.placeholders = placeholders;
|
|
86
94
|
this.customProps = customProps;
|
|
87
95
|
this.dualOutput = dualOutput;
|
|
96
|
+
this.webOutputs = webOutputs;
|
|
88
97
|
this.cwd = options.baseDir || (options.files ? "/" : defaultCwd);
|
|
89
98
|
this.fs = options.fs
|
|
90
99
|
|| (options.files ? new VirtualFS(options.files) : null)
|
|
@@ -113,7 +122,8 @@ class SomMark {
|
|
|
113
122
|
allowFetch: security?.allowFetch !== false,
|
|
114
123
|
allowHttp: security?.allowHttp === true,
|
|
115
124
|
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
|
|
125
|
+
allowedExtensions: Array.isArray(security?.allowedExtensions) ? security.allowedExtensions.map(e => e.toLowerCase()) : null,
|
|
126
|
+
env: Array.isArray(security?.env) ? security.env : []
|
|
117
127
|
};
|
|
118
128
|
this.warnings = [];
|
|
119
129
|
this._prepared = false;
|
|
@@ -300,6 +310,7 @@ class SomMark {
|
|
|
300
310
|
security: this.security,
|
|
301
311
|
settings: this.rawSettings,
|
|
302
312
|
dualOutput: this.dualOutput,
|
|
313
|
+
webOutputs: this.webOutputs,
|
|
303
314
|
instance: this
|
|
304
315
|
});
|
|
305
316
|
|
|
@@ -29,28 +29,18 @@ const MARKDOWN = Mapper.define({
|
|
|
29
29
|
},
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Provides a fallback for unknown tags by
|
|
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
|
|
36
|
-
|
|
36
|
+
const id = node.id;
|
|
37
37
|
return {
|
|
38
|
-
|
|
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
|
-
|
|
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(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
149
|
-
const src
|
|
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
|
|
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(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
208
|
+
const extractRows = async sectionNode => {
|
|
211
209
|
const sectionRows = [];
|
|
212
|
-
for (const child of
|
|
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 =
|
|
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(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
292
|
-
},
|
|
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(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
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(
|
|
315
|
-
|
|
328
|
+
MARKDOWN.register(
|
|
329
|
+
"todo",
|
|
330
|
+
({ props, content, isSelfClosing }) => {
|
|
331
|
+
let status, task;
|
|
316
332
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
326
|
-
},
|
|
341
|
+
return md.todo(status, task);
|
|
342
|
+
},
|
|
343
|
+
{ trimAndWrapBlocks: false }
|
|
344
|
+
);
|
|
327
345
|
export default MARKDOWN;
|