@godxjp/markdown 31.26.2 → 31.28.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 CHANGED
@@ -53,6 +53,69 @@ Images: wrap the body in `<Prose imagePreview>` and every body image opens the k
53
53
  through the others — no `img` override needed. Images in table cells keep min(natural width, 20rem),
54
54
  so a narrow table scrolls instead of squashing them.
55
55
 
56
+ ## Grammar v1 and the codec (`@godxjp/markdown/codec`)
57
+
58
+ The renderer (`RENDERER_VERSION` 4), the codec and `@godxjp/block-editor` read one syntax, from one
59
+ set of parsers, so a body means the same thing in the editor, on a server and on the page.
60
+
61
+ ### Syntax
62
+
63
+ - **GFM** — tables, task lists, strikethrough, autolinks, footnotes.
64
+ - **CJK-friendly emphasis** — `の**「強調」**です` is strong. CommonMark's flanking rules leave
65
+ literal asterisks next to 、。「」(); the codec, the renderer and the editor all use the
66
+ CJK-friendly extension.
67
+ - **Callout** — a GitHub alert: `> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`
68
+ (kind case-insensitive on input, written uppercase), optional plain-text title on the same line
69
+ (`> [!WARNING] Title`); the rest of the quote is the body. A fold marker (`[!NOTE]-` / `+`) or an
70
+ unknown kind is NOT a callout: it stays an ordinary blockquote, marker verbatim.
71
+ - **Toggle** — `:::toggle[Summary *inline*]` … `:::`. Body is any blocks; toggles nest, and sit
72
+ inside callouts and columns. Open state is not stored.
73
+ - **Columns** — 2–4 columns, each with an optional `width` (integer percent 1–100; columns without
74
+ one share the remainder equally). Columns never nest.
75
+
76
+ ```md
77
+ ::::columns
78
+ :::column{width=40}
79
+ Left
80
+ :::
81
+
82
+ :::column
83
+ Right
84
+ :::
85
+ ::::
86
+ ```
87
+
88
+ Only container directives exist: the fence of a directive is one colon longer than the deepest
89
+ directive inside it, so the innermost fence is always `:::`. There are no text (`:name`) or leaf
90
+ (`::name`) directives — `10:30` is prose.
91
+
92
+ - **Wikilinks and embeds, verbatim** — `[[target]]`, `[[target#heading]]`, `[[target|label]]`,
93
+ `![[embed]]` (a block when alone on its line; the inner text is kept verbatim — `![[B#見出し]]`, `![[img.png|300]]`). Marks around them survive (`**[[x]]**`). Inside a table cell write `[[x\|label]]`.
94
+ - **`asset:` images** — `![alt](asset:id)` round-trips as written (the renderer resolves it through
95
+ `resolveUrl`).
96
+ - **Raw blocks** — anything the document does not model (raw HTML, footnote definitions, link
97
+ reference definitions, an unknown or malformed directive, a fence with a meta string) is kept as
98
+ Markdown that parses back to the same thing. Mermaid is an ordinary ` ```mermaid ` fence.
99
+
100
+ ### Codec
101
+
102
+ ```ts
103
+ import { CODEC_ID, normalize, parse, serialize } from "@godxjp/markdown/codec";
104
+
105
+ CODEC_ID; // "kit-md@1" — bumped whenever serialize would write a body differently
106
+ const doc = parse(markdown); // plain JSON, ProseMirror/Tiptap shape
107
+ const md = serialize(doc); // canonical Markdown
108
+ normalize(markdown) === serialize(parse(markdown)); // what the editor saves
109
+ normalize(normalize(x)) === normalize(x); // idempotent, for every fixture
110
+ ```
111
+
112
+ Pure: no React, no DOM, no editor — it runs on a server and in a Cloudflare Worker.
113
+
114
+ **Canonical form** (what `serialize` writes): ATX headings; `-` bullets; `1.` ordered lists;
115
+ `*em*`; `**strong**`; ` ``` ` fences (an indented code block becomes fenced); `---` rules;
116
+ `- [ ]` / `- [x]` todos; GFM tables with padded cells and an alignment row; a callout's body after a
117
+ `>` blank line; one blank line between blocks; LF; a trailing newline; an empty document is `""`.
118
+
56
119
  ## Stored versions
57
120
 
58
121
  Record `MARKDOWN_FORMAT` (`"md"`) and `RENDERER_VERSION` on every saved version.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The codec's document: plain JSON in the ProseMirror/Tiptap shape (`type` / `attrs` / `content` /
3
+ * `text` / `marks`), so `@godxjp/block-editor` loads it with `setContent` and hands back what
4
+ * `getJSON()` returns. No DOM, no editor instance — this module runs in a Worker (gh#1156).
5
+ *
6
+ * Node names follow Tiptap's StarterKit / Table / Details extensions where one exists:
7
+ * doc · paragraph · heading{level} · blockquote · codeBlock{language} · horizontalRule ·
8
+ * bulletList{tight} · orderedList{start,tight} · listItem · taskList{tight} · taskItem{checked} ·
9
+ * table · tableRow · tableHeader{align} · tableCell{align} · image{src,alt,title} · hardBreak
10
+ * and the kit's own:
11
+ * callout{kind,title} · details · detailsSummary · detailsContent · columns · column{width} ·
12
+ * embed{target} (block `![[x]]`) · wikilink{target,heading,label} · embedInline{target} ·
13
+ * rawBlock{source} · rawInline{source} · frontmatter{source} (YAML, first block only)
14
+ * Marks: bold · italic · strike · code · link{href,title}.
15
+ */
16
+ type Mark = {
17
+ type: "bold" | "italic" | "strike" | "code";
18
+ } | {
19
+ type: "link";
20
+ attrs: {
21
+ href: string;
22
+ title: string | null;
23
+ };
24
+ };
25
+ type DocNode = {
26
+ type: string;
27
+ attrs?: Record<string, unknown>;
28
+ content?: DocNode[];
29
+ text?: string;
30
+ marks?: Mark[];
31
+ };
32
+ type Doc = DocNode & {
33
+ type: "doc";
34
+ content: DocNode[];
35
+ };
36
+ /** GitHub alert kinds, the only callouts v1 knows. Serialized uppercase. */
37
+ declare const CALLOUT_KINDS: readonly ["note", "tip", "important", "warning", "caution"];
38
+ type CalloutKind = (typeof CALLOUT_KINDS)[number];
39
+ /** Columns per `:::columns` block (grammar v1). */
40
+ declare const COLUMNS_MIN = 2;
41
+ declare const COLUMNS_MAX = 4;
42
+
43
+ /** Markdown → document (plain JSON, ProseMirror/Tiptap shape). Pure: no DOM. */
44
+ declare function parse(markdown: string): Doc;
45
+
46
+ type OutlineEntry = {
47
+ depth: number;
48
+ text: string;
49
+ id: string;
50
+ };
51
+ /**
52
+ * The body's headings with the ids `<Markdown>` gives them — GitHub slugs, de-duplicated in
53
+ * document order, the renderer's default rule — for a table of contents (the kit's `Anchor`). Pure.
54
+ * A host that passes its own `headingId` resolver to `<Markdown>` must use that same resolver here.
55
+ */
56
+ declare function outline(markdown: string): OutlineEntry[];
57
+ /** The body's YAML front matter, verbatim (without the `---` fences), or null. Parse it with your YAML library. */
58
+ declare function frontmatterOf(markdown: string): string | null;
59
+
60
+ /** Document → canonical Markdown. Pure: no DOM. */
61
+ declare function serialize(doc: Doc | DocNode): string;
62
+
63
+ /**
64
+ * Identifies the serializer's output. Bumped whenever `serialize` would write a stored body
65
+ * differently, so a host can tell which normalization a saved `normalized_body` came from.
66
+ */
67
+ declare const CODEC_ID = "kit-md@1";
68
+ /** `serialize(parse(markdown))` — the canonical form of a body, exactly what the editor saves. */
69
+ declare function normalize(markdown: string): string;
70
+
71
+ export { CALLOUT_KINDS, CODEC_ID, COLUMNS_MAX, COLUMNS_MIN, type CalloutKind, type Doc, type DocNode, type Mark, type OutlineEntry, frontmatterOf, normalize, outline, parse, serialize };
@@ -0,0 +1,611 @@
1
+ // src/codec/parse.ts
2
+ import { fromMarkdown } from "mdast-util-from-markdown";
3
+ import { toMarkdown } from "mdast-util-to-markdown";
4
+
5
+ // src/codec/model.ts
6
+ var CALLOUT_KINDS = ["note", "tip", "important", "warning", "caution"];
7
+ var COLUMNS_MIN = 2;
8
+ var COLUMNS_MAX = 4;
9
+
10
+ // src/codec/readers.ts
11
+ var CALLOUT = /^\[!([A-Za-z]+)\](?:[ \t]+([^\n]*))?$/;
12
+ function readCallout(node) {
13
+ const [first, ...rest] = node.children;
14
+ if (first?.type !== "paragraph") return null;
15
+ const [head, ...tail] = first.children;
16
+ if (head?.type !== "text") return null;
17
+ const newline = head.value.indexOf("\n");
18
+ if (newline === -1 && tail.length > 0) return null;
19
+ const line = newline === -1 ? head.value : head.value.slice(0, newline);
20
+ const match = CALLOUT.exec(line);
21
+ const kind = match?.[1]?.toLowerCase();
22
+ if (!match || !kind || !CALLOUT_KINDS.includes(kind)) return null;
23
+ const remainder = [
24
+ ...newline === -1 ? [] : [{ ...head, value: head.value.slice(newline + 1) }],
25
+ ...tail
26
+ ].filter((n) => !(n.type === "text" && n.value === ""));
27
+ return {
28
+ kind,
29
+ title: match[2]?.trim() || null,
30
+ body: [
31
+ ...remainder.length ? [{ type: "paragraph", children: remainder }] : [],
32
+ ...rest
33
+ ]
34
+ };
35
+ }
36
+ function labelOf(node) {
37
+ const first = node.children[0];
38
+ return first?.type === "paragraph" && first.data?.directiveLabel ? first : null;
39
+ }
40
+ function readToggle(node) {
41
+ if (node.name !== "toggle" || Object.keys(node.attributes ?? {}).length > 0) return null;
42
+ const label = labelOf(node);
43
+ return {
44
+ summary: label?.children ?? [],
45
+ body: label ? node.children.slice(1) : node.children
46
+ };
47
+ }
48
+ function readColumns(node, nested) {
49
+ if (node.name !== "columns" || nested || labelOf(node) || Object.keys(node.attributes ?? {}).length) {
50
+ return null;
51
+ }
52
+ const columns = node.children;
53
+ if (columns.length < COLUMNS_MIN || columns.length > COLUMNS_MAX) return null;
54
+ const parts = [];
55
+ for (const column of columns) {
56
+ if (column.type !== "containerDirective" || column.name !== "column" || labelOf(column))
57
+ return null;
58
+ const attributes = Object.entries(column.attributes ?? {});
59
+ if (attributes.length > 1) return null;
60
+ let width = null;
61
+ if (attributes.length === 1) {
62
+ const [key, value] = attributes[0];
63
+ if (key !== "width" || typeof value !== "string" || !/^(?:[1-9]\d?|100)$/.test(value))
64
+ return null;
65
+ width = Number(value);
66
+ }
67
+ parts.push({ width, body: column.children });
68
+ }
69
+ return parts;
70
+ }
71
+
72
+ // src/codec/syntax.ts
73
+ import { directive } from "micromark-extension-directive";
74
+ import { gfm } from "micromark-extension-gfm";
75
+ import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
76
+ import { frontmatter } from "micromark-extension-frontmatter";
77
+ import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
78
+ import { directiveFromMarkdown } from "mdast-util-directive";
79
+ import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
80
+ function micromarkExtensions() {
81
+ const [container] = directive().flow[58];
82
+ const containerOnly = { flow: { 58: container } };
83
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
84
+ }
85
+ function fromMarkdownExtensions() {
86
+ return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
87
+ }
88
+ var WIDE = /[\u1100-\u115F\u2E80-\u303E\u3041-\u33FF\u3400-\u4DBF\u4E00-\u9FFF\uA000-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1F64F}\u{1F900}-\u{1F9FF}\u{20000}-\u{3FFFD}]/u;
89
+ function displayWidth(value) {
90
+ let width = 0;
91
+ for (const char of value) width += WIDE.test(char) ? 2 : 1;
92
+ return width;
93
+ }
94
+ function toMarkdownGfm() {
95
+ return gfmToMarkdown({ stringLength: displayWidth });
96
+ }
97
+ function toMarkdownFrontmatter() {
98
+ return frontmatterToMarkdown(["yaml"]);
99
+ }
100
+
101
+ // src/codec/options.ts
102
+ function containerDirective(node, _, state, info) {
103
+ const tracker = state.createTracker(info);
104
+ const [first, ...rest] = node.children;
105
+ const hasLabel = first?.type === "paragraph" && first.data?.directiveLabel;
106
+ const label = hasLabel ? `[${state.containerPhrasing(first, { ...tracker.current(), before: "[", after: "]" })}]` : "";
107
+ const attributes = Object.entries(node.attributes ?? {}).map(
108
+ ([key, value]) => value === "" || value == null ? key : `${key}=${/^[\w.-]+$/.test(String(value)) ? value : JSON.stringify(value)}`
109
+ ).join(" ");
110
+ const opening = `${node.name}${label}${attributes ? `{${attributes}}` : ""}`;
111
+ const body = state.containerFlow(
112
+ { type: "root", children: hasLabel ? rest : node.children },
113
+ tracker.current()
114
+ );
115
+ const longest = Math.max(2, ...[...body.matchAll(/^[ \t>]*(:{3,})/gm)].map((m) => m[1].length));
116
+ const fence = ":".repeat(longest + 1);
117
+ return body ? `${fence}${opening}
118
+ ${body}
119
+ ${fence}` : `${fence}${opening}
120
+ ${fence}`;
121
+ }
122
+ function toMarkdownOptions() {
123
+ return {
124
+ bullet: "-",
125
+ bulletOther: "*",
126
+ emphasis: "*",
127
+ strong: "*",
128
+ fence: "`",
129
+ fences: true,
130
+ rule: "-",
131
+ listItemIndent: "one",
132
+ incrementListMarker: true,
133
+ setext: false,
134
+ closeAtx: false,
135
+ resourceLink: false,
136
+ extensions: [
137
+ toMarkdownGfm(),
138
+ toMarkdownFrontmatter(),
139
+ { handlers: { containerDirective } }
140
+ ]
141
+ };
142
+ }
143
+
144
+ // src/codec/parse.ts
145
+ var WIKILINK = /!\[\[([^[\]\n]+)\]\]|\[\[([^[\]\n|#]+)(?:#([^[\]\n|]+))?(?:\|([^[\]\n]+))?\]\]/g;
146
+ function parseMdast(markdown) {
147
+ return fromMarkdown(markdown, {
148
+ extensions: micromarkExtensions(),
149
+ mdastExtensions: fromMarkdownExtensions()
150
+ });
151
+ }
152
+ function raw(node, inline2) {
153
+ const source = node.type === "html" ? node.value : toMarkdown(node, toMarkdownOptions()).replace(/\n+$/, "");
154
+ return { type: inline2 ? "rawInline" : "rawBlock", attrs: { source } };
155
+ }
156
+ function withMark(marks, mark) {
157
+ return [...marks, mark];
158
+ }
159
+ function text(value, marks) {
160
+ const out = [];
161
+ let last = 0;
162
+ for (const match of value.matchAll(WIKILINK)) {
163
+ const at = match.index;
164
+ if (at > last) out.push(leafText(value.slice(last, at), marks));
165
+ const [, embed, target, heading, label] = match;
166
+ out.push(
167
+ withMarks(
168
+ embed !== void 0 ? { type: "embedInline", attrs: { target: embed } } : {
169
+ type: "wikilink",
170
+ attrs: {
171
+ target: target.trim(),
172
+ heading: heading?.trim() ?? null,
173
+ label: label ?? null
174
+ }
175
+ },
176
+ marks
177
+ )
178
+ );
179
+ last = at + match[0].length;
180
+ }
181
+ if (last < value.length) out.push(leafText(value.slice(last), marks));
182
+ return out;
183
+ }
184
+ function withMarks(node, marks) {
185
+ return marks.length ? { ...node, marks } : node;
186
+ }
187
+ function leafText(value, marks) {
188
+ return marks.length ? { type: "text", text: value, marks } : { type: "text", text: value };
189
+ }
190
+ function inline(nodes, marks = []) {
191
+ return nodes.flatMap((node) => {
192
+ switch (node.type) {
193
+ case "text":
194
+ return text(node.value, marks);
195
+ case "emphasis":
196
+ return inline(node.children, withMark(marks, { type: "italic" }));
197
+ case "strong":
198
+ return inline(node.children, withMark(marks, { type: "bold" }));
199
+ case "delete":
200
+ return inline(node.children, withMark(marks, { type: "strike" }));
201
+ case "inlineCode":
202
+ return [leafText(node.value, withMark(marks, { type: "code" }))];
203
+ case "link":
204
+ return inline(
205
+ node.children,
206
+ withMark(marks, { type: "link", attrs: { href: node.url, title: node.title ?? null } })
207
+ );
208
+ case "image":
209
+ return [
210
+ withMarks(
211
+ {
212
+ type: "image",
213
+ attrs: { src: node.url, alt: node.alt ?? "", title: node.title ?? null }
214
+ },
215
+ marks
216
+ )
217
+ ];
218
+ case "break":
219
+ return [withMarks({ type: "hardBreak" }, marks)];
220
+ default:
221
+ return [withMarks(raw(node, true), marks)];
222
+ }
223
+ });
224
+ }
225
+ function paragraph(node) {
226
+ const content2 = inline(node.children);
227
+ if (content2.length === 1 && content2[0].type === "embedInline" && !content2[0].marks) {
228
+ return [{ type: "embed", attrs: { ...content2[0].attrs } }];
229
+ }
230
+ return [content2.length ? { type: "paragraph", content: content2 } : { type: "paragraph" }];
231
+ }
232
+ function blocks(nodes, depth) {
233
+ return nodes.flatMap((node) => block(node, depth));
234
+ }
235
+ function callout(node, depth) {
236
+ const parts = readCallout(node);
237
+ if (!parts) return null;
238
+ return {
239
+ type: "callout",
240
+ attrs: { kind: parts.kind, title: parts.title },
241
+ content: blocks(parts.body, depth)
242
+ };
243
+ }
244
+ var MARKER = /^\[![A-Za-z]+\][+-]?/;
245
+ function keepMarker(children) {
246
+ const [first, ...rest] = children;
247
+ if (first?.type !== "paragraph") return children;
248
+ const [head, ...tail] = first.children;
249
+ const marker = head?.type === "text" ? MARKER.exec(head.value)?.[0] : void 0;
250
+ if (!marker || head?.type !== "text") return children;
251
+ const remainder = head.value.slice(marker.length);
252
+ return [
253
+ {
254
+ ...first,
255
+ children: [
256
+ { type: "html", value: marker },
257
+ ...remainder ? [{ ...head, value: remainder }] : [],
258
+ ...tail
259
+ ]
260
+ },
261
+ ...rest
262
+ ];
263
+ }
264
+ function directiveBlock(node, depth) {
265
+ const toggle = readToggle(node);
266
+ if (toggle) {
267
+ const summary = inline(toggle.summary);
268
+ return {
269
+ type: "details",
270
+ content: [
271
+ summary.length ? { type: "detailsSummary", content: summary } : { type: "detailsSummary" },
272
+ { type: "detailsContent", content: nonEmpty(blocks(toggle.body, depth)) }
273
+ ]
274
+ };
275
+ }
276
+ const columns = readColumns(node, depth.columns);
277
+ if (columns) {
278
+ return {
279
+ type: "columns",
280
+ content: columns.map((column) => ({
281
+ type: "column",
282
+ attrs: { width: column.width },
283
+ content: nonEmpty(blocks(column.body, { columns: true }))
284
+ }))
285
+ };
286
+ }
287
+ return raw(node, false);
288
+ }
289
+ function nonEmpty(content2) {
290
+ return content2.length ? content2 : [{ type: "paragraph" }];
291
+ }
292
+ function block(node, depth) {
293
+ switch (node.type) {
294
+ case "paragraph":
295
+ return paragraph(node);
296
+ case "heading": {
297
+ const content2 = inline(node.children);
298
+ return [
299
+ { type: "heading", attrs: { level: node.depth }, ...content2.length ? { content: content2 } : {} }
300
+ ];
301
+ }
302
+ case "thematicBreak":
303
+ return [{ type: "horizontalRule" }];
304
+ case "code":
305
+ if (node.meta) return [raw(node, false)];
306
+ return [
307
+ {
308
+ type: "codeBlock",
309
+ attrs: { language: node.lang ?? null },
310
+ ...node.value ? { content: [{ type: "text", text: node.value }] } : {}
311
+ }
312
+ ];
313
+ case "blockquote":
314
+ return [
315
+ callout(node, depth) ?? {
316
+ type: "blockquote",
317
+ content: nonEmpty(blocks(keepMarker(node.children), depth))
318
+ }
319
+ ];
320
+ case "list": {
321
+ const tasks = node.children.map((item) => typeof item.checked === "boolean");
322
+ if (tasks.some(Boolean) && !tasks.every(Boolean)) return [raw(node, false)];
323
+ const isTask = tasks.length > 0 && tasks.every(Boolean);
324
+ const tight = !node.spread && node.children.every((item) => !item.spread);
325
+ const items = node.children.map((item) => ({
326
+ type: isTask ? "taskItem" : "listItem",
327
+ ...isTask ? { attrs: { checked: item.checked === true } } : {},
328
+ content: nonEmpty(blocks(item.children, depth))
329
+ }));
330
+ if (isTask) return [{ type: "taskList", attrs: { tight }, content: items }];
331
+ return node.ordered ? [{ type: "orderedList", attrs: { start: node.start ?? 1, tight }, content: items }] : [{ type: "bulletList", attrs: { tight }, content: items }];
332
+ }
333
+ case "table": {
334
+ const width = node.children[0]?.children.length ?? 0;
335
+ return [
336
+ {
337
+ type: "table",
338
+ content: node.children.map((row, r) => ({
339
+ type: "tableRow",
340
+ content: row.children.slice(0, width).map((cell, c) => {
341
+ const content2 = inline(cell.children);
342
+ return {
343
+ type: r === 0 ? "tableHeader" : "tableCell",
344
+ attrs: { align: node.align?.[c] ?? null },
345
+ content: [content2.length ? { type: "paragraph", content: content2 } : { type: "paragraph" }]
346
+ };
347
+ })
348
+ }))
349
+ }
350
+ ];
351
+ }
352
+ case "containerDirective":
353
+ return [directiveBlock(node, depth)];
354
+ case "yaml":
355
+ return [{ type: "frontmatter", attrs: { source: node.value } }];
356
+ default:
357
+ return [raw(node, false)];
358
+ }
359
+ }
360
+ function parse(markdown) {
361
+ const content2 = blocks(parseMdast(markdown.replace(/\r\n?/g, "\n")).children, { columns: false });
362
+ return { type: "doc", content: content2.length ? content2 : [{ type: "paragraph" }] };
363
+ }
364
+
365
+ // src/codec/serialize.ts
366
+ import { toMarkdown as toMarkdown2 } from "mdast-util-to-markdown";
367
+ var MARK_RANK = ["link", "bold", "italic", "strike"];
368
+ var hasMark = (node, type) => node.marks?.some((m) => m.type === type) ?? false;
369
+ var linkOf = (node) => node.marks?.find((m) => m.type === "link");
370
+ var sameLink = (a, b) => linkOf(a)?.attrs.href === linkOf(b)?.attrs.href && linkOf(a)?.attrs.title === linkOf(b)?.attrs.title;
371
+ function wikilinkSource(node, inTable) {
372
+ const a = node.attrs;
373
+ const pipe = inTable ? "\\|" : "|";
374
+ return `[[${a.target}${a.heading ? `#${a.heading}` : ""}${a.label ? `${pipe}${a.label}` : ""}]]`;
375
+ }
376
+ function leaf(node, inTable) {
377
+ switch (node.type) {
378
+ case "text":
379
+ return hasMark(node, "code") ? [{ type: "inlineCode", value: node.text ?? "" }] : [{ type: "text", value: node.text ?? "" }];
380
+ case "hardBreak":
381
+ return [{ type: "break" }];
382
+ case "image": {
383
+ const a = node.attrs;
384
+ return [{ type: "image", url: a.src, alt: a.alt ?? "", title: a.title ?? null }];
385
+ }
386
+ case "wikilink":
387
+ return [{ type: "html", value: wikilinkSource(node, inTable) }];
388
+ case "embedInline":
389
+ return [{ type: "html", value: `![[${node.attrs.target}]]` }];
390
+ case "rawInline":
391
+ return [{ type: "html", value: String(node.attrs.source) }];
392
+ default:
393
+ return [];
394
+ }
395
+ }
396
+ function phrasing(nodes, inTable = false, rank = 0) {
397
+ if (rank === MARK_RANK.length) return nodes.flatMap((n) => leaf(n, inTable));
398
+ const mark = MARK_RANK[rank];
399
+ const out = [];
400
+ let i = 0;
401
+ while (i < nodes.length) {
402
+ const start = nodes[i];
403
+ const marked = hasMark(start, mark);
404
+ let j = i + 1;
405
+ while (j < nodes.length && hasMark(nodes[j], mark) === marked && (!marked || mark !== "link" || sameLink(start, nodes[j]))) {
406
+ j++;
407
+ }
408
+ const inner = phrasing(nodes.slice(i, j), inTable, rank + 1);
409
+ if (!marked) out.push(...inner);
410
+ else if (mark === "link") {
411
+ const { href, title } = linkOf(start).attrs;
412
+ out.push({ type: "link", url: href, title, children: inner });
413
+ } else if (mark === "bold") out.push({ type: "strong", children: inner });
414
+ else if (mark === "italic") out.push({ type: "emphasis", children: inner });
415
+ else out.push({ type: "delete", children: inner });
416
+ i = j;
417
+ }
418
+ return out;
419
+ }
420
+ var content = (node) => node.content ?? [];
421
+ function paragraphOf(node) {
422
+ return { type: "paragraph", children: phrasing(content(node)) };
423
+ }
424
+ function flow(nodes) {
425
+ return nodes.flatMap(block2);
426
+ }
427
+ function block2(node) {
428
+ const attrs = node.attrs ?? {};
429
+ switch (node.type) {
430
+ case "paragraph":
431
+ return [paragraphOf(node)];
432
+ case "heading":
433
+ return [
434
+ {
435
+ type: "heading",
436
+ depth: Math.min(6, Math.max(1, Number(attrs.level) || 1)),
437
+ children: phrasing(content(node))
438
+ }
439
+ ];
440
+ case "horizontalRule":
441
+ return [{ type: "thematicBreak" }];
442
+ case "codeBlock":
443
+ return [
444
+ {
445
+ type: "code",
446
+ lang: attrs.language ?? null,
447
+ meta: null,
448
+ value: content(node).map((t) => t.text ?? "").join("")
449
+ }
450
+ ];
451
+ case "blockquote":
452
+ return [{ type: "blockquote", children: flow(content(node)) }];
453
+ case "callout": {
454
+ const marker = `[!${String(attrs.kind).toUpperCase()}]`;
455
+ const head = {
456
+ type: "paragraph",
457
+ children: [
458
+ { type: "html", value: marker },
459
+ ...attrs.title ? [{ type: "text", value: ` ${String(attrs.title)}` }] : []
460
+ ]
461
+ };
462
+ return [{ type: "blockquote", children: [head, ...flow(content(node))] }];
463
+ }
464
+ case "bulletList":
465
+ case "orderedList":
466
+ case "taskList": {
467
+ const tight = attrs.tight !== false;
468
+ return [
469
+ {
470
+ type: "list",
471
+ ordered: node.type === "orderedList",
472
+ start: node.type === "orderedList" ? Number(attrs.start ?? 1) : null,
473
+ spread: !tight,
474
+ children: content(node).map((item) => ({
475
+ type: "listItem",
476
+ spread: !tight,
477
+ checked: node.type === "taskList" ? item.attrs?.checked === true : null,
478
+ children: flow(content(item))
479
+ }))
480
+ }
481
+ ];
482
+ }
483
+ case "table": {
484
+ const rows = content(node);
485
+ const align = content(rows[0] ?? { type: "tableRow" }).map(
486
+ (cell) => cell.attrs?.align ?? null
487
+ );
488
+ return [
489
+ {
490
+ type: "table",
491
+ align,
492
+ children: rows.map((row) => ({
493
+ type: "tableRow",
494
+ children: content(row).map((cell) => ({
495
+ type: "tableCell",
496
+ children: phrasing(
497
+ content(cell).flatMap((p) => content(p)),
498
+ true
499
+ )
500
+ }))
501
+ }))
502
+ }
503
+ ];
504
+ }
505
+ case "details": {
506
+ const [summary, body] = [
507
+ content(node).find((n) => n.type === "detailsSummary"),
508
+ content(node).find((n) => n.type === "detailsContent")
509
+ ];
510
+ const label = summary && content(summary).length ? [
511
+ {
512
+ type: "paragraph",
513
+ data: { directiveLabel: true },
514
+ children: phrasing(content(summary))
515
+ }
516
+ ] : [];
517
+ return [
518
+ {
519
+ type: "containerDirective",
520
+ name: "toggle",
521
+ attributes: {},
522
+ children: [
523
+ ...label,
524
+ ...flow(content(body ?? { type: "detailsContent" }))
525
+ ]
526
+ }
527
+ ];
528
+ }
529
+ case "columns":
530
+ return [
531
+ {
532
+ type: "containerDirective",
533
+ name: "columns",
534
+ attributes: {},
535
+ children: content(node).map((column) => ({
536
+ type: "containerDirective",
537
+ name: "column",
538
+ attributes: column.attrs?.width != null ? { width: String(column.attrs.width) } : {},
539
+ children: flow(content(column))
540
+ }))
541
+ }
542
+ ];
543
+ case "embed":
544
+ return [{ type: "html", value: `![[${String(attrs.target)}]]` }];
545
+ case "rawBlock":
546
+ return [{ type: "html", value: String(attrs.source) }];
547
+ case "frontmatter":
548
+ return [{ type: "yaml", value: String(attrs.source) }];
549
+ default:
550
+ return [];
551
+ }
552
+ }
553
+ function decodeAttentionReferences(markdown) {
554
+ const references = [...markdown.matchAll(/&#x([0-9A-F]+);/g)];
555
+ if (references.length === 0) return markdown;
556
+ const target = JSON.stringify(parse(markdown));
557
+ let out = markdown;
558
+ for (const reference of references.reverse()) {
559
+ const at = reference.index;
560
+ const candidate = out.slice(0, at) + String.fromCodePoint(Number.parseInt(reference[1], 16)) + out.slice(at + reference[0].length);
561
+ if (JSON.stringify(parse(candidate)) === target) out = candidate;
562
+ }
563
+ return out;
564
+ }
565
+ function serialize(doc) {
566
+ const root = { type: "root", children: flow(doc.content ?? []) };
567
+ const markdown = toMarkdown2(root, toMarkdownOptions());
568
+ if (markdown.trim() === "") return "";
569
+ const tidy = markdown.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\n+$/, "\n");
570
+ return decodeAttentionReferences(tidy);
571
+ }
572
+
573
+ // src/codec/outline.ts
574
+ import GithubSlugger from "github-slugger";
575
+ import { visit } from "unist-util-visit";
576
+ function textOf(node) {
577
+ if (node.type === "text" || node.type === "inlineCode" || node.type === "html") return node.value;
578
+ if (node.type === "image" || node.type === "imageReference") return "";
579
+ if ("children" in node) return node.children.map(textOf).join("");
580
+ return "";
581
+ }
582
+ function outline(markdown) {
583
+ const slugger = new GithubSlugger();
584
+ const entries = [];
585
+ visit(parseMdast(markdown.replace(/\r\n?/g, "\n")), "heading", (node) => {
586
+ const text2 = textOf(node);
587
+ entries.push({ depth: node.depth, text: text2, id: slugger.slug(text2) });
588
+ });
589
+ return entries;
590
+ }
591
+ function frontmatterOf(markdown) {
592
+ const first = parseMdast(markdown.replace(/\r\n?/g, "\n")).children[0];
593
+ return first?.type === "yaml" ? first.value : null;
594
+ }
595
+
596
+ // src/codec/index.ts
597
+ var CODEC_ID = "kit-md@1";
598
+ function normalize(markdown) {
599
+ return serialize(parse(markdown));
600
+ }
601
+ export {
602
+ CALLOUT_KINDS,
603
+ CODEC_ID,
604
+ COLUMNS_MAX,
605
+ COLUMNS_MIN,
606
+ frontmatterOf,
607
+ normalize,
608
+ outline,
609
+ parse,
610
+ serialize
611
+ };
package/dist/index.d.ts CHANGED
@@ -65,6 +65,10 @@ declare function extendSchema(extension?: SchemaExtension): Options;
65
65
  */
66
66
  declare function safeUrl(url: string, key: string): string;
67
67
 
68
+ /** GitHub alert kinds, the only callouts v1 knows. Serialized uppercase. */
69
+ declare const CALLOUT_KINDS: readonly ["note", "tip", "important", "warning", "caution"];
70
+ type CalloutKind = (typeof CALLOUT_KINDS)[number];
71
+
68
72
  type PluggableList = NonNullable<Options$1["remarkPlugins"]>;
69
73
  /**
70
74
  * The stored body's format, and the renderer generation that produced its output. A consumer
@@ -73,7 +77,7 @@ type PluggableList = NonNullable<Options$1["remarkPlugins"]>;
73
77
  * differently — a schema change, a new default plugin — not on every release.
74
78
  */
75
79
  declare const MARKDOWN_FORMAT = "md";
76
- declare const RENDERER_VERSION = 3;
80
+ declare const RENDERER_VERSION = 4;
77
81
  type MarkdownProps = {
78
82
  /** The Markdown source. */
79
83
  children: string;
@@ -124,6 +128,24 @@ type MarkdownProps = {
124
128
  * Characters are user-perceived (grapheme clusters). Default 24; `false` stamps nothing.
125
129
  */
126
130
  shortCellLength?: number | false;
131
+ /**
132
+ * Visible callout titles per kind when a callout has none of its own (`> [!WARNING]`). English by
133
+ * default — this package carries no i18n; a host passes its localized strings.
134
+ */
135
+ calloutTitles?: Partial<Record<CalloutKind, string>>;
136
+ /**
137
+ * Where an EXTERNAL link opens. An external link is an absolute http(s) URL whose origin differs
138
+ * from `origin`. `"new-tab"` gives it `target="_blank"` and `rel="noopener noreferrer"`. Relative
139
+ * links, `#anchors`, wikilinks resolved to a path, and `mailto:` stay in the tab. The default,
140
+ * `"same-tab"`, changes nothing. Markdown has no syntax for `target`, so this is a renderer policy.
141
+ */
142
+ externalLinks?: "same-tab" | "new-tab";
143
+ /**
144
+ * The page origin that `externalLinks` compares against. Defaults to `location.origin`. Pass it
145
+ * when rendering on the server, or every absolute link counts as external there and hydration
146
+ * disagrees on same-origin ones.
147
+ */
148
+ origin?: string;
127
149
  };
128
150
  /**
129
151
  * THE ONE RENDERER (gh#1108). GFM — tables, task lists, fenced code, autolinks, footnotes,
@@ -135,7 +157,7 @@ type MarkdownProps = {
135
157
  * Renders bare elements; the host wraps it (`<Prose>` from `@godxjp/ui/data-display`), so this
136
158
  * package carries no UI-kit dependency.
137
159
  */
138
- declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, }: MarkdownProps): React.JSX.Element;
160
+ declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, externalLinks, origin, }: MarkdownProps): React.JSX.Element;
139
161
 
140
162
  type MermaidSvgCheck = {
141
163
  ok: true;
package/dist/index.js CHANGED
@@ -86,14 +86,14 @@ function checkMermaidSvg(svg) {
86
86
  const root = doc.documentElement;
87
87
  if (root.localName.toLowerCase() !== "svg") return { ok: false, reason: "root" };
88
88
  const elements = [root, ...Array.from(root.getElementsByTagName("*"))];
89
- for (const element of elements) {
90
- const name = element.localName.toLowerCase();
89
+ for (const element2 of elements) {
90
+ const name = element2.localName.toLowerCase();
91
91
  if (!ALLOWED_ELEMENTS.has(name)) return { ok: false, reason: `element ${name}` };
92
92
  if (name === "style") {
93
- const css = element.textContent ?? "";
93
+ const css = element2.textContent ?? "";
94
94
  if (CSS_HAZARD.test(css) || !urlsAreLocal(css)) return { ok: false, reason: "style" };
95
95
  }
96
- for (const attribute of Array.from(element.attributes)) {
96
+ for (const attribute of Array.from(element2.attributes)) {
97
97
  const attr = attribute.localName.toLowerCase();
98
98
  const value = attribute.value;
99
99
  if (attr.startsWith("on")) return { ok: false, reason: `handler ${attr}` };
@@ -203,7 +203,28 @@ var markdownSchema = {
203
203
  h3: [...defaultSchema.attributes?.h3 ?? [], "id"],
204
204
  h4: [...defaultSchema.attributes?.h4 ?? [], "id"],
205
205
  h5: [...defaultSchema.attributes?.h5 ?? [], "id"],
206
- h6: [...defaultSchema.attributes?.h6 ?? [], "id"]
206
+ h6: [...defaultSchema.attributes?.h6 ?? [], "id"],
207
+ // Grammar v1 (gh#1156): the renderer's own callout / toggle / column markup. Every value is
208
+ // pinned — a body cannot mint any other class, kind, role or inline style through these.
209
+ div: [
210
+ ...defaultSchema.attributes?.div ?? [],
211
+ [
212
+ "className",
213
+ "ui-prose-callout",
214
+ "ui-prose-columns",
215
+ "ui-prose-column",
216
+ "ui-prose-directive"
217
+ ],
218
+ ["dataKind", "note", "tip", "important", "warning", "caution"],
219
+ ["role", "note"],
220
+ ["style", /^--prose-column-grow:\d+(?:\.\d+)?$/]
221
+ ],
222
+ p: [...defaultSchema.attributes?.p ?? [], ["className", "ui-prose-callout-title"]],
223
+ details: [...defaultSchema.attributes?.details ?? [], ["className", "ui-prose-toggle"]],
224
+ summary: [
225
+ ...defaultSchema.attributes?.summary ?? [],
226
+ ["className", "ui-prose-toggle-summary"]
227
+ ]
207
228
  }
208
229
  };
209
230
  var ruleName = (rule) => Array.isArray(rule) ? rule[0] : rule;
@@ -241,10 +262,177 @@ function safeUrl(url, key) {
241
262
  return SAFE_HREF.test(probe) ? value : "";
242
263
  }
243
264
 
265
+ // src/codec/model.ts
266
+ var CALLOUT_KINDS = ["note", "tip", "important", "warning", "caution"];
267
+ var COLUMNS_MIN = 2;
268
+ var COLUMNS_MAX = 4;
269
+
270
+ // src/codec/readers.ts
271
+ var CALLOUT = /^\[!([A-Za-z]+)\](?:[ \t]+([^\n]*))?$/;
272
+ function readCallout(node) {
273
+ const [first, ...rest] = node.children;
274
+ if (first?.type !== "paragraph") return null;
275
+ const [head, ...tail] = first.children;
276
+ if (head?.type !== "text") return null;
277
+ const newline = head.value.indexOf("\n");
278
+ if (newline === -1 && tail.length > 0) return null;
279
+ const line = newline === -1 ? head.value : head.value.slice(0, newline);
280
+ const match = CALLOUT.exec(line);
281
+ const kind = match?.[1]?.toLowerCase();
282
+ if (!match || !kind || !CALLOUT_KINDS.includes(kind)) return null;
283
+ const remainder = [
284
+ ...newline === -1 ? [] : [{ ...head, value: head.value.slice(newline + 1) }],
285
+ ...tail
286
+ ].filter((n) => !(n.type === "text" && n.value === ""));
287
+ return {
288
+ kind,
289
+ title: match[2]?.trim() || null,
290
+ body: [
291
+ ...remainder.length ? [{ type: "paragraph", children: remainder }] : [],
292
+ ...rest
293
+ ]
294
+ };
295
+ }
296
+ function labelOf(node) {
297
+ const first = node.children[0];
298
+ return first?.type === "paragraph" && first.data?.directiveLabel ? first : null;
299
+ }
300
+ function readToggle(node) {
301
+ if (node.name !== "toggle" || Object.keys(node.attributes ?? {}).length > 0) return null;
302
+ const label = labelOf(node);
303
+ return {
304
+ summary: label?.children ?? [],
305
+ body: label ? node.children.slice(1) : node.children
306
+ };
307
+ }
308
+ function readColumns(node, nested) {
309
+ if (node.name !== "columns" || nested || labelOf(node) || Object.keys(node.attributes ?? {}).length) {
310
+ return null;
311
+ }
312
+ const columns = node.children;
313
+ if (columns.length < COLUMNS_MIN || columns.length > COLUMNS_MAX) return null;
314
+ const parts = [];
315
+ for (const column of columns) {
316
+ if (column.type !== "containerDirective" || column.name !== "column" || labelOf(column))
317
+ return null;
318
+ const attributes = Object.entries(column.attributes ?? {});
319
+ if (attributes.length > 1) return null;
320
+ let width = null;
321
+ if (attributes.length === 1) {
322
+ const [key, value] = attributes[0];
323
+ if (key !== "width" || typeof value !== "string" || !/^(?:[1-9]\d?|100)$/.test(value))
324
+ return null;
325
+ width = Number(value);
326
+ }
327
+ parts.push({ width, body: column.children });
328
+ }
329
+ return parts;
330
+ }
331
+ function columnShares(columns) {
332
+ const explicit = columns.reduce((sum, c) => sum + (c.width ?? 0), 0);
333
+ const open = columns.filter((c) => c.width == null).length;
334
+ const each = open ? Math.max(1, (100 - explicit) / open) : 0;
335
+ return columns.map((c) => c.width ?? Math.round(each * 100) / 100);
336
+ }
337
+
338
+ // src/codec/syntax.ts
339
+ import { directive } from "micromark-extension-directive";
340
+ import { gfm } from "micromark-extension-gfm";
341
+ import { cjkFriendlyExtension } from "micromark-extension-cjk-friendly";
342
+ import { frontmatter } from "micromark-extension-frontmatter";
343
+ import { frontmatterFromMarkdown, frontmatterToMarkdown } from "mdast-util-frontmatter";
344
+ import { directiveFromMarkdown } from "mdast-util-directive";
345
+ import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
346
+ function micromarkExtensions() {
347
+ const [container] = directive().flow[58];
348
+ const containerOnly = { flow: { 58: container } };
349
+ return [gfm(), cjkFriendlyExtension(), frontmatter(["yaml"]), containerOnly];
350
+ }
351
+ function fromMarkdownExtensions() {
352
+ return [gfmFromMarkdown(), directiveFromMarkdown(), frontmatterFromMarkdown(["yaml"])];
353
+ }
354
+
355
+ // src/directives.ts
356
+ var DEFAULT_CALLOUT_TITLES = {
357
+ note: "Note",
358
+ tip: "Tip",
359
+ important: "Important",
360
+ warning: "Warning",
361
+ caution: "Caution"
362
+ };
363
+ var element = (hName, hProperties = {}) => ({
364
+ hName,
365
+ hProperties
366
+ });
367
+ function remarkKitSyntax(options = {}) {
368
+ const titles = { ...DEFAULT_CALLOUT_TITLES, ...options.calloutTitles };
369
+ return function() {
370
+ const data = this.data();
371
+ (data.micromarkExtensions ??= []).push(...micromarkExtensions().slice(1));
372
+ (data.fromMarkdownExtensions ??= []).push(...fromMarkdownExtensions().slice(1));
373
+ const walk = (node, inColumn) => {
374
+ if (node.type === "blockquote") callout(node, titles);
375
+ else if (node.type === "containerDirective") directive2(node, inColumn);
376
+ if (!("children" in node)) return;
377
+ const isColumn = node.type === "containerDirective" && node.name === "column";
378
+ for (const child of node.children) walk(child, inColumn || isColumn);
379
+ };
380
+ return (tree) => {
381
+ tree.children = tree.children.filter((node) => node.type !== "yaml");
382
+ walk(tree, false);
383
+ };
384
+ };
385
+ }
386
+ function callout(node, titles) {
387
+ const parts = readCallout(node);
388
+ if (!parts) return;
389
+ const title = {
390
+ type: "paragraph",
391
+ data: element("p", { className: ["ui-prose-callout-title"] }),
392
+ children: [{ type: "text", value: parts.title ?? titles[parts.kind] }]
393
+ };
394
+ node.data = element("div", {
395
+ className: ["ui-prose-callout"],
396
+ dataKind: parts.kind,
397
+ role: "note"
398
+ });
399
+ node.children = [title, ...parts.body];
400
+ }
401
+ function directive2(node, nested) {
402
+ const toggle = readToggle(node);
403
+ if (toggle) {
404
+ const summary = {
405
+ type: "paragraph",
406
+ data: element("summary", { className: ["ui-prose-toggle-summary"] }),
407
+ children: toggle.summary
408
+ };
409
+ node.data = element("details", { className: ["ui-prose-toggle"] });
410
+ node.children = [summary, ...toggle.body];
411
+ return;
412
+ }
413
+ const columns = readColumns(node, nested);
414
+ if (columns) {
415
+ const shares = columnShares(columns);
416
+ node.data = element("div", { className: ["ui-prose-columns"] });
417
+ node.children = columns.map((column, i) => ({
418
+ type: "containerDirective",
419
+ name: "column",
420
+ attributes: {},
421
+ data: element("div", {
422
+ className: ["ui-prose-column"],
423
+ style: `--prose-column-grow:${shares[i]}`
424
+ }),
425
+ children: column.body
426
+ }));
427
+ return;
428
+ }
429
+ if (!node.data?.hName) node.data = element("div", { className: ["ui-prose-directive"] });
430
+ }
431
+
244
432
  // src/markdown.tsx
245
433
  import { jsx as jsx3 } from "react/jsx-runtime";
246
434
  var MARKDOWN_FORMAT = "md";
247
- var RENDERER_VERSION = 3;
435
+ var RENDERER_VERSION = 4;
248
436
  var URL_ATTRIBUTES = {
249
437
  a: "href",
250
438
  img: "src",
@@ -287,6 +475,25 @@ function rehypeShortCells(options) {
287
475
  });
288
476
  };
289
477
  }
478
+ function rehypeExternalLinks(options) {
479
+ return (tree) => {
480
+ if (options.policy !== "new-tab") return;
481
+ const origin = options.origin ?? globalThis.location?.origin;
482
+ visit2(tree, "element", (node) => {
483
+ if (node.tagName !== "a") return;
484
+ const href = node.properties?.href;
485
+ if (typeof href !== "string" || !/^https?:\/\//i.test(href)) return;
486
+ let target;
487
+ try {
488
+ target = new URL(href);
489
+ } catch {
490
+ return;
491
+ }
492
+ if (origin && target.origin === origin) return;
493
+ node.properties = { ...node.properties, target: "_blank", rel: ["noopener", "noreferrer"] };
494
+ });
495
+ };
496
+ }
290
497
  function omitNode(props) {
291
498
  const { node: _node, ...rest } = props;
292
499
  return rest;
@@ -312,9 +519,13 @@ function Markdown({
312
519
  disallowedElements,
313
520
  allowElement,
314
521
  unwrapDisallowed,
315
- shortCellLength = 24
522
+ shortCellLength = 24,
523
+ calloutTitles,
524
+ externalLinks = "same-tab",
525
+ origin
316
526
  }) {
317
527
  const sanitizeSchema = React3.useMemo(() => extendSchema(schema), [schema]);
528
+ const kitSyntax = React3.useMemo(() => remarkKitSyntax({ calloutTitles }), [calloutTitles]);
318
529
  const merged = React3.useMemo(() => {
319
530
  const HostTable = components?.table;
320
531
  const table = (props) => /* @__PURE__ */ jsx3(TableScroll, { children: HostTable ? typeof HostTable === "string" ? React3.createElement(HostTable, props) : /* @__PURE__ */ jsx3(HostTable, { ...props }) : /* @__PURE__ */ jsx3("table", { ...omitNode(props) }) });
@@ -333,12 +544,13 @@ function Markdown({
333
544
  return /* @__PURE__ */ jsx3(
334
545
  ReactMarkdown,
335
546
  {
336
- remarkPlugins: [remarkGfm, ...remarkPlugins],
547
+ remarkPlugins: [remarkGfm, kitSyntax, ...remarkPlugins],
337
548
  rehypePlugins: [
338
549
  [rehypeResolveUrls, { resolve: resolveUrl }],
339
550
  [rehypeHeadingIds, { resolve: headingId }],
340
551
  [rehypeSanitize, sanitizeSchema],
341
552
  [rehypeShortCells, { max: shortCellLength }],
553
+ [rehypeExternalLinks, { policy: externalLinks, origin }],
342
554
  ...rehypePlugins
343
555
  ],
344
556
  urlTransform: safeUrl,
@@ -440,6 +652,37 @@ var MARKDOWN_FIXTURES = [
440
652
  markdown: "![x](data:image/svg+xml;base64,PHN2Zz4=) ![y](javascript:alert(1))",
441
653
  html: '<p><img alt="x"/> <img alt="y"/></p>',
442
654
  forbidden: ["data:", "javascript:"]
655
+ },
656
+ // Grammar v1 (gh#1156, renderer v4): the constructs the codec and @godxjp/block-editor model.
657
+ {
658
+ name: "CJK-friendly emphasis next to \u3001\u3002\u300C\u300D\uFF08\uFF09",
659
+ markdown: "\u306E**\u300C\u5F37\u8ABF\u300D**\u3067\u3059\u3002\uFF08*\u6CE8*\uFF09\u300C[\u30EA\u30F3\u30AF](https://example.com/)\u300D\u3001**\u592A\u5B57**\u3002",
660
+ html: '<p>\u306E<strong>\u300C\u5F37\u8ABF\u300D</strong>\u3067\u3059\u3002\uFF08<em>\u6CE8</em>\uFF09\u300C<a href="https://example.com/">\u30EA\u30F3\u30AF</a>\u300D\u3001<strong>\u592A\u5B57</strong>\u3002</p>'
661
+ },
662
+ {
663
+ name: "callout with a title, and one without",
664
+ markdown: "> [!WARNING] Careful\n> Body\n\n> [!tip]\n> No title",
665
+ html: '<div class="ui-prose-callout" data-kind="warning" role="note">\n<p class="ui-prose-callout-title">Careful</p>\n<p>Body</p>\n</div>\n<div class="ui-prose-callout" data-kind="tip" role="note">\n<p class="ui-prose-callout-title">Tip</p>\n<p>No title</p>\n</div>'
666
+ },
667
+ {
668
+ name: "a fold marker or unknown kind stays a quote",
669
+ markdown: "> [!NOTE]- folded\n\n> [!INFO] unknown",
670
+ html: "<blockquote>\n<p>[!NOTE]- folded</p>\n</blockquote>\n<blockquote>\n<p>[!INFO] unknown</p>\n</blockquote>"
671
+ },
672
+ {
673
+ name: "toggle with an inline summary",
674
+ markdown: ":::toggle[Read *more*]\nBody\n:::",
675
+ html: '<details class="ui-prose-toggle"><summary class="ui-prose-toggle-summary">Read <em>more</em></summary><p>Body</p></details>'
676
+ },
677
+ {
678
+ name: "columns with a width and an equal share",
679
+ markdown: "::::columns\n:::column{width=40}\nLeft\n:::\n\n:::column\nRight\n:::\n::::",
680
+ html: '<div class="ui-prose-columns"><div class="ui-prose-column" style="--prose-column-grow:40"><p>Left</p></div><div class="ui-prose-column" style="--prose-column-grow:60"><p>Right</p></div></div>'
681
+ },
682
+ {
683
+ name: "wikilinks, embeds and times stay text",
684
+ markdown: "See [[Page#Intro|intro]] at 10:30.\n\n![[Diagram]]",
685
+ html: "<p>See [[Page#Intro|intro]] at 10:30.</p>\n<p>![[Diagram]]</p>"
443
686
  }
444
687
  ];
445
688
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/markdown",
3
- "version": "31.26.2",
3
+ "version": "31.28.0",
4
4
  "description": "The one Markdown renderer for GoDX apps: GFM + heading anchors, one sanitiser schema, a shared fixture corpus, and Mermaid drawn only after its SVG passes a fail-closed gate.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,10 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
+ "./codec": {
15
+ "types": "./dist/codec/index.d.ts",
16
+ "import": "./dist/codec/index.js"
17
+ },
14
18
  "./package.json": "./package.json"
15
19
  },
16
20
  "sideEffects": false,
@@ -30,6 +34,16 @@
30
34
  "license": "Apache-2.0",
31
35
  "dependencies": {
32
36
  "github-slugger": "^2.0.0",
37
+ "mdast-util-directive": "^3.1.1",
38
+ "mdast-util-from-markdown": "^2.1.0",
39
+ "mdast-util-frontmatter": "^2.0.1",
40
+ "mdast-util-gfm": "^3.1.0",
41
+ "mdast-util-to-markdown": "^2.2.0",
42
+ "micromark-extension-cjk-friendly": "^2.0.2",
43
+ "micromark-extension-directive": "^4.0.0",
44
+ "micromark-extension-frontmatter": "^2.0.0",
45
+ "micromark-extension-gfm": "^3.0.0",
46
+ "micromark-util-types": "^2.0.2",
33
47
  "react-markdown": "^10.1.0",
34
48
  "rehype-sanitize": "^6.0.0",
35
49
  "remark-gfm": "^4.0.1",