md4x 0.0.19 → 0.0.21

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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/build/md4x.wasm CHANGED
Binary file
Binary file
Binary file
@@ -0,0 +1,115 @@
1
+ const decoder = new TextDecoder();
2
+
3
+ function parseCodeMeta(bytes, extraFields) {
4
+ const nullIdx = bytes.indexOf(0);
5
+ if (nullIdx === -1) {
6
+ return { output: decoder.decode(bytes), codeBlocks: [] };
7
+ }
8
+ const outBytes = bytes.subarray(0, nullIdx);
9
+ const metaBytes = bytes.subarray(nullIdx + 1);
10
+ const output = decoder.decode(outBytes);
11
+ const meta = JSON.parse(decoder.decode(metaBytes));
12
+ const codeBlocks = meta.map((m) => {
13
+ const start = decoder.decode(outBytes.subarray(0, m.s)).length;
14
+ const end = decoder.decode(outBytes.subarray(0, m.e)).length;
15
+ const block = { start, end, lang: m.l || "" };
16
+ if (m.f) block.filename = m.f;
17
+ if (m.h) block.highlights = m.h;
18
+ if (extraFields) extraFields(block, m);
19
+ return block;
20
+ });
21
+ return { output, codeBlocks };
22
+ }
23
+
24
+ export function parseHtmlMeta(bytes) {
25
+ const { output, codeBlocks } = parseCodeMeta(bytes);
26
+ return { html: output, codeBlocks };
27
+ }
28
+
29
+ export function parseHtmlWithHighlighting(bytes, highlighter) {
30
+ const { html, codeBlocks } = parseHtmlMeta(bytes);
31
+ if (codeBlocks.length === 0) return html;
32
+ let out = "";
33
+ let pos = 0;
34
+ for (const block of codeBlocks) {
35
+ const code = unescapeHtml(html.slice(block.start, block.end));
36
+ const highlighted = highlighter(code, block);
37
+ if (highlighted === undefined) {
38
+ out += html.slice(pos, block.end);
39
+ } else {
40
+ // Calculate <pre><code...> prefix length to replace the full wrapper
41
+ const preLen = block.lang
42
+ ? '<pre><code class="language-'.length + block.lang.length + '">'.length
43
+ : "<pre><code>".length;
44
+ out += html.slice(pos, block.start - preLen);
45
+ out += highlighted;
46
+ pos = block.end + "</code></pre>\n".length;
47
+ continue;
48
+ }
49
+ pos = block.end;
50
+ }
51
+ out += html.slice(pos);
52
+ return out;
53
+ }
54
+
55
+ function unescapeHtml(str) {
56
+ if (!str.includes("&")) return str;
57
+ return str
58
+ .replaceAll("&amp;", "&")
59
+ .replaceAll("&lt;", "<")
60
+ .replaceAll("&gt;", ">")
61
+ .replaceAll("&quot;", '"');
62
+ }
63
+
64
+ const DIM = "\x1b[2m";
65
+ const DIM_OFF = "\x1b[22m";
66
+
67
+ export function parseAnsiMeta(bytes) {
68
+ const { output, codeBlocks } = parseCodeMeta(bytes, (block, m) => {
69
+ if (m.i) block.prefix = m.i;
70
+ });
71
+ return { ansi: output, codeBlocks };
72
+ }
73
+
74
+ export function parseAnsiWithHighlighting(bytes, highlighter) {
75
+ const { ansi, codeBlocks } = parseAnsiMeta(bytes);
76
+ if (codeBlocks.length === 0) return ansi;
77
+ let out = "";
78
+ let pos = 0;
79
+ for (const block of codeBlocks) {
80
+ const region = ansi.slice(block.start, block.end);
81
+ const prefix = block.prefix || " ";
82
+ // Strip DIM wrapper and extract raw code by removing prefix from each line
83
+ let inner = region;
84
+ if (inner.startsWith(DIM)) inner = inner.slice(DIM.length);
85
+ if (inner.endsWith(DIM_OFF)) inner = inner.slice(0, -DIM_OFF.length);
86
+ const code = inner
87
+ .split("\n")
88
+ .filter((l) => l.length > 0)
89
+ .map((l) => (l.startsWith(prefix) ? l.slice(prefix.length) : l))
90
+ .join("\n");
91
+ const highlighted = highlighter(code, block);
92
+ if (highlighted === undefined) {
93
+ out += ansi.slice(pos, block.end);
94
+ } else {
95
+ out += ansi.slice(pos, block.start);
96
+ // Wrap each line with the indent prefix
97
+ const lines = highlighted.split("\n");
98
+ for (let i = 0; i < lines.length; i++) {
99
+ if (lines[i].length > 0) {
100
+ out += prefix + lines[i];
101
+ }
102
+ if (i < lines.length - 1) {
103
+ out += "\n";
104
+ }
105
+ }
106
+ // Ensure trailing newline
107
+ if (!highlighted.endsWith("\n")) out += "\n";
108
+ pos = block.end;
109
+ continue;
110
+ }
111
+ pos = block.end;
112
+ }
113
+ out += ansi.slice(pos);
114
+ return out;
115
+ }
package/lib/cli.mjs CHANGED
@@ -24,6 +24,8 @@ const { values, positionals } = parseArgs({
24
24
  "html-title": { type: "string" },
25
25
  "html-css": { type: "string" },
26
26
  heal: { type: "boolean", default: false },
27
+ "show-urls": { type: "boolean", default: false },
28
+ "show-frontmatter": { type: "boolean", default: false },
27
29
  stat: { type: "boolean", short: "s", default: false },
28
30
  help: { type: "boolean", short: "h", default: false },
29
31
  version: { type: "boolean", short: "v", default: false },
@@ -47,6 +49,8 @@ ${_g("General options:")}
47
49
  ${_c("-o")}, ${_c("--output")}=${_d("FILE")} Output file ${_d("(default: stdout)")}
48
50
  ${_c("-t")}, ${_c("--format")}=${_d("FORMAT")} Output format: ${_c("html")}, ${_c("text")}, ${_c("ast")}, ${_c("ansi")}, ${_c("meta")}, ${_c("heal")} ${_d("(default: ansi for TTY, text otherwise)")}
49
51
  ${_c("--heal")} Heal incomplete markdown before rendering
52
+ ${_c("--show-urls")} Show link URLs after link text ${_d("(ANSI only)")}
53
+ ${_c("--show-frontmatter")} Show frontmatter content ${_d("(ANSI only)")}
50
54
  ${_c("-s")}, ${_c("--stat")} Measure parsing time
51
55
  ${_c("-h")}, ${_c("--help")} Display this help and exit
52
56
  ${_c("-v")}, ${_c("--version")} Display version and exit
@@ -165,7 +169,11 @@ switch (format) {
165
169
  output = renderToHtml(input, healOpt);
166
170
  break;
167
171
  case "ansi":
168
- output = renderToAnsi(input, healOpt);
172
+ output = renderToAnsi(input, {
173
+ ...healOpt,
174
+ showUrls: values["show-urls"],
175
+ showFrontmatter: values["show-frontmatter"],
176
+ });
169
177
  break;
170
178
  case "text":
171
179
  output = renderToText(input, healOpt);
package/lib/napi.d.mts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  ComarkTree,
3
3
  ComarkMeta,
4
4
  HtmlOptions,
5
+ AnsiOptions,
5
6
  RenderOptions,
6
7
  } from "./types.mjs";
7
8
 
@@ -14,13 +15,18 @@ export type {
14
15
  ComarkHeading,
15
16
  ComarkMeta,
16
17
  HtmlOptions,
18
+ AnsiOptions,
17
19
  RenderOptions,
18
20
  } from "./types.mjs";
19
21
 
22
+ export type * from "./types.mjs";
23
+
20
24
  export interface NAPIBinding {
21
25
  renderToHtml(input: string, flags?: number): string;
26
+ renderToHtmlMeta(input: string): Buffer;
22
27
  renderToAST(input: string, flags?: number): string;
23
28
  renderToAnsi(input: string, flags?: number): string;
29
+ renderToAnsiMeta(input: string): Buffer;
24
30
  renderToMeta(input: string, flags?: number): string;
25
31
  renderToText(input: string, flags?: number): string;
26
32
  heal(input: string): string;
@@ -40,10 +46,7 @@ export declare function parseAST(
40
46
  input: string,
41
47
  opts?: RenderOptions,
42
48
  ): ComarkTree;
43
- export declare function renderToAnsi(
44
- input: string,
45
- opts?: RenderOptions,
46
- ): string;
49
+ export declare function renderToAnsi(input: string, opts?: AnsiOptions): string;
47
50
  export declare function renderToMeta(
48
51
  input: string,
49
52
  opts?: RenderOptions,
package/lib/napi.mjs CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ parseHtmlWithHighlighting,
3
+ parseAnsiWithHighlighting,
4
+ } from "./_shared.mjs";
5
+
1
6
  // --- internal ---
2
7
 
3
8
  let binding;
@@ -40,7 +45,14 @@ const HEAL_FLAG = 0x0100;
40
45
  export function renderToHtml(input, opts) {
41
46
  let flags = opts?.full ? 0x0008 : 0;
42
47
  if (opts?.heal) flags |= HEAL_FLAG;
43
- return getBinding().renderToHtml(str(input), flags);
48
+ if (!opts?.highlighter) {
49
+ return getBinding().renderToHtml(str(input), flags);
50
+ }
51
+ const buf = getBinding().renderToHtmlMeta(str(input));
52
+ return parseHtmlWithHighlighting(
53
+ new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength),
54
+ opts.highlighter,
55
+ );
44
56
  }
45
57
 
46
58
  export function renderToAST(input, opts) {
@@ -53,8 +65,18 @@ export function parseAST(input, opts) {
53
65
  }
54
66
 
55
67
  export function renderToAnsi(input, opts) {
56
- const flags = opts?.heal ? HEAL_FLAG : 0;
57
- return getBinding().renderToAnsi(str(input), flags);
68
+ let flags = opts?.heal ? HEAL_FLAG : 0;
69
+ if (opts?.showUrls) flags |= 0x0010;
70
+ if (opts?.showFrontmatter) flags |= 0x0020;
71
+ if (!opts?.highlighter) {
72
+ return getBinding().renderToAnsi(str(input), flags);
73
+ }
74
+ const s = opts?.heal ? getBinding().heal(str(input)) : str(input);
75
+ const buf = getBinding().renderToAnsiMeta(s);
76
+ return parseAnsiWithHighlighting(
77
+ new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength),
78
+ opts.highlighter,
79
+ );
58
80
  }
59
81
 
60
82
  export function renderToMeta(input, opts) {
package/lib/types.d.mts CHANGED
@@ -34,7 +34,73 @@ export interface RenderOptions {
34
34
  heal?: boolean;
35
35
  }
36
36
 
37
+ export interface AnsiOptions extends RenderOptions {
38
+ /**
39
+ * Custom highlighter function for fenced code blocks. If provided, code blocks
40
+ * are passed through this callback which can return custom ANSI-highlighted output.
41
+ */
42
+ highlighter?: AnsiCodeBlockHighlighter;
43
+ /** Show link URLs after link text (e.g. `text (url)`). Default: false (links are clickable via OSC 8). */
44
+ showUrls?: boolean;
45
+ /** Show frontmatter content as dim text. Default: false (frontmatter is suppressed). */
46
+ showFrontmatter?: boolean;
47
+ }
48
+
37
49
  export interface HtmlOptions extends RenderOptions {
38
50
  /** Generate a full HTML document with `<!DOCTYPE html>`, `<head>`, and `<body>`. */
39
51
  full?: boolean;
52
+
53
+ /**
54
+ * Custom highlighter function for fenced code blocks. If provided, this will cause
55
+ */
56
+ highlighter: CodeBlockHighlighter;
40
57
  }
58
+
59
+ export interface CodeBlock {
60
+ /** Character offset in HTML string where code content starts (after `<code...>`) */
61
+ start: number;
62
+ /** Character offset in HTML string where code content ends (before `</code>`) */
63
+ end: number;
64
+ /** Language identifier (empty string if none) */
65
+ lang: string;
66
+ /** Filename from `[filename]` syntax */
67
+ filename?: string;
68
+ /** Highlighted line numbers from `{1-3,5}` syntax */
69
+ highlights?: number[];
70
+ }
71
+
72
+ export type CodeBlockHighlighter = (
73
+ /** Raw code content (HTML entities unescaped) */
74
+ code: string,
75
+ /** Code block metadata (lang, filename, highlights, offsets) */
76
+ block: CodeBlock,
77
+ ) => string | undefined;
78
+
79
+ export interface HtmlWithCodeBlocks {
80
+ /** The HTML string */
81
+ html: string;
82
+ /** Metadata for each fenced code block in document order */
83
+ codeBlocks: CodeBlock[];
84
+ }
85
+
86
+ export interface AnsiCodeBlock {
87
+ /** Character offset in ANSI string where code block starts (including DIM escape) */
88
+ start: number;
89
+ /** Character offset in ANSI string where code block ends (including DIM_OFF escape) */
90
+ end: number;
91
+ /** Language identifier (empty string if none) */
92
+ lang: string;
93
+ /** Filename from `[filename]` syntax */
94
+ filename?: string;
95
+ /** Highlighted line numbers from `{1-3,5}` syntax */
96
+ highlights?: number[];
97
+ /** Line indent prefix (includes ANSI escapes for colored bars in nested contexts) */
98
+ prefix?: string;
99
+ }
100
+
101
+ export type AnsiCodeBlockHighlighter = (
102
+ /** Raw code content (indentation stripped) */
103
+ code: string,
104
+ /** Code block metadata (lang, filename, highlights, offsets) */
105
+ block: AnsiCodeBlock,
106
+ ) => string | undefined;
@@ -1,3 +1,8 @@
1
+ import {
2
+ parseHtmlWithHighlighting,
3
+ parseAnsiWithHighlighting,
4
+ } from "../_shared.mjs";
5
+
1
6
  // --- internal ---
2
7
 
3
8
  let _instance;
@@ -55,13 +60,41 @@ function render(exports, fn, input, ...extra) {
55
60
  return result;
56
61
  }
57
62
 
63
+ /* Render with a meta function, returning raw bytes for highlighter processing. */
64
+ function renderMetaBytes(exports, metaFn, input) {
65
+ const { memory, md4x_alloc, md4x_free, md4x_result_ptr, md4x_result_size } =
66
+ exports;
67
+ const encoded = new TextEncoder().encode(str(input));
68
+ const ptr = md4x_alloc(encoded.length);
69
+ new Uint8Array(memory.buffer).set(encoded, ptr);
70
+ const ret = metaFn(ptr, encoded.length);
71
+ md4x_free(ptr);
72
+ if (ret !== 0) {
73
+ throw new Error("md4x: render failed");
74
+ }
75
+ const outPtr = md4x_result_ptr();
76
+ const outSize = md4x_result_size();
77
+ const bytes = new Uint8Array(memory.buffer, outPtr, outSize);
78
+ return { bytes, outPtr };
79
+ }
80
+
58
81
  const HEAL_FLAG = 0x0100;
59
82
 
60
83
  export function renderToHtml(input, opts) {
61
84
  let flags = opts?.full ? 0x0008 : 0;
62
85
  if (opts?.heal) flags |= HEAL_FLAG;
63
86
  const exports = _getExports();
64
- return render(exports, exports.md4x_to_html, input, flags);
87
+ if (!opts?.highlighter) {
88
+ return render(exports, exports.md4x_to_html, input, flags);
89
+ }
90
+ const { bytes, outPtr } = renderMetaBytes(
91
+ exports,
92
+ exports.md4x_to_html_meta,
93
+ input,
94
+ );
95
+ const result = parseHtmlWithHighlighting(bytes, opts.highlighter);
96
+ exports.md4x_free(outPtr);
97
+ return result;
65
98
  }
66
99
 
67
100
  export function renderToAST(input, opts) {
@@ -75,9 +108,22 @@ export function parseAST(input, opts) {
75
108
  }
76
109
 
77
110
  export function renderToAnsi(input, opts) {
78
- const flags = opts?.heal ? HEAL_FLAG : 0;
111
+ let flags = opts?.heal ? HEAL_FLAG : 0;
112
+ if (opts?.showUrls) flags |= 0x0010;
113
+ if (opts?.showFrontmatter) flags |= 0x0020;
79
114
  const exports = _getExports();
80
- return render(exports, exports.md4x_to_ansi, input, flags);
115
+ if (!opts?.highlighter) {
116
+ return render(exports, exports.md4x_to_ansi, input, flags);
117
+ }
118
+ const s = opts?.heal ? heal(str(input)) : str(input);
119
+ const { bytes, outPtr } = renderMetaBytes(
120
+ exports,
121
+ exports.md4x_to_ansi_meta,
122
+ s,
123
+ );
124
+ const result = parseAnsiWithHighlighting(bytes, opts.highlighter);
125
+ exports.md4x_free(outPtr);
126
+ return result;
81
127
  }
82
128
 
83
129
  export function renderToMeta(input, opts) {
@@ -2,6 +2,7 @@ import type {
2
2
  ComarkTree,
3
3
  ComarkMeta,
4
4
  HtmlOptions,
5
+ AnsiOptions,
5
6
  RenderOptions,
6
7
  } from "../types.mjs";
7
8
 
@@ -14,6 +15,7 @@ export type {
14
15
  ComarkHeading,
15
16
  ComarkMeta,
16
17
  HtmlOptions,
18
+ AnsiOptions,
17
19
  RenderOptions,
18
20
  } from "../types.mjs";
19
21
 
@@ -36,10 +38,7 @@ export declare function parseAST(
36
38
  input: string,
37
39
  opts?: RenderOptions,
38
40
  ): ComarkTree;
39
- export declare function renderToAnsi(
40
- input: string,
41
- opts?: RenderOptions,
42
- ): string;
41
+ export declare function renderToAnsi(input: string, opts?: AnsiOptions): string;
43
42
  export declare function renderToMeta(
44
43
  input: string,
45
44
  opts?: RenderOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md4x",
3
- "version": "0.0.19",
3
+ "version": "0.0.21",
4
4
  "repository": "unjs/md4x",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "./wasm": {
9
9
  "types": "./lib/wasm/index.d.mts",
10
10
  "unwasm": "./lib/wasm/unwasm.mjs",
11
- "default": "./lib/wasm/defult.mjs"
11
+ "default": "./lib/wasm/default.mjs"
12
12
  },
13
13
  "./napi": "./lib/napi.mjs",
14
14
  "./build/md4x.wasm": "./build/md4x.wasm",
@@ -16,7 +16,7 @@
16
16
  "node": "./lib/napi.mjs",
17
17
  "default": {
18
18
  "unwasm": "./lib/wasm/unwasm.mjs",
19
- "default": "./lib/wasm/defult.mjs"
19
+ "default": "./lib/wasm/default.mjs"
20
20
  }
21
21
  }
22
22
  },
File without changes