@godxjp/markdown 31.26.1 → 31.27.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 +63 -0
- package/dist/codec/index.d.ts +57 -0
- package/dist/codec/index.js +576 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +224 -8
- package/package.json +13 -1
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** — `` 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,57 @@
|
|
|
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}
|
|
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
|
+
/** Document → canonical Markdown. Pure: no DOM. */
|
|
47
|
+
declare function serialize(doc: Doc | DocNode): string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Identifies the serializer's output. Bumped whenever `serialize` would write a stored body
|
|
51
|
+
* differently, so a host can tell which normalization a saved `normalized_body` came from.
|
|
52
|
+
*/
|
|
53
|
+
declare const CODEC_ID = "kit-md@1";
|
|
54
|
+
/** `serialize(parse(markdown))` — the canonical form of a body, exactly what the editor saves. */
|
|
55
|
+
declare function normalize(markdown: string): string;
|
|
56
|
+
|
|
57
|
+
export { CALLOUT_KINDS, CODEC_ID, COLUMNS_MAX, COLUMNS_MIN, type CalloutKind, type Doc, type DocNode, type Mark, normalize, parse, serialize };
|
|
@@ -0,0 +1,576 @@
|
|
|
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 { directiveFromMarkdown } from "mdast-util-directive";
|
|
77
|
+
import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
|
|
78
|
+
function micromarkExtensions() {
|
|
79
|
+
const [container] = directive().flow[58];
|
|
80
|
+
const containerOnly = { flow: { 58: container } };
|
|
81
|
+
return [gfm(), cjkFriendlyExtension(), containerOnly];
|
|
82
|
+
}
|
|
83
|
+
function fromMarkdownExtensions() {
|
|
84
|
+
return [gfmFromMarkdown(), directiveFromMarkdown()];
|
|
85
|
+
}
|
|
86
|
+
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;
|
|
87
|
+
function displayWidth(value) {
|
|
88
|
+
let width = 0;
|
|
89
|
+
for (const char of value) width += WIDE.test(char) ? 2 : 1;
|
|
90
|
+
return width;
|
|
91
|
+
}
|
|
92
|
+
function toMarkdownGfm() {
|
|
93
|
+
return gfmToMarkdown({ stringLength: displayWidth });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/codec/options.ts
|
|
97
|
+
function containerDirective(node, _, state, info) {
|
|
98
|
+
const tracker = state.createTracker(info);
|
|
99
|
+
const [first, ...rest] = node.children;
|
|
100
|
+
const hasLabel = first?.type === "paragraph" && first.data?.directiveLabel;
|
|
101
|
+
const label = hasLabel ? `[${state.containerPhrasing(first, { ...tracker.current(), before: "[", after: "]" })}]` : "";
|
|
102
|
+
const attributes = Object.entries(node.attributes ?? {}).map(
|
|
103
|
+
([key, value]) => value === "" || value == null ? key : `${key}=${/^[\w.-]+$/.test(String(value)) ? value : JSON.stringify(value)}`
|
|
104
|
+
).join(" ");
|
|
105
|
+
const opening = `${node.name}${label}${attributes ? `{${attributes}}` : ""}`;
|
|
106
|
+
const body = state.containerFlow(
|
|
107
|
+
{ type: "root", children: hasLabel ? rest : node.children },
|
|
108
|
+
tracker.current()
|
|
109
|
+
);
|
|
110
|
+
const longest = Math.max(2, ...[...body.matchAll(/^[ \t>]*(:{3,})/gm)].map((m) => m[1].length));
|
|
111
|
+
const fence = ":".repeat(longest + 1);
|
|
112
|
+
return body ? `${fence}${opening}
|
|
113
|
+
${body}
|
|
114
|
+
${fence}` : `${fence}${opening}
|
|
115
|
+
${fence}`;
|
|
116
|
+
}
|
|
117
|
+
function toMarkdownOptions() {
|
|
118
|
+
return {
|
|
119
|
+
bullet: "-",
|
|
120
|
+
bulletOther: "*",
|
|
121
|
+
emphasis: "*",
|
|
122
|
+
strong: "*",
|
|
123
|
+
fence: "`",
|
|
124
|
+
fences: true,
|
|
125
|
+
rule: "-",
|
|
126
|
+
listItemIndent: "one",
|
|
127
|
+
incrementListMarker: true,
|
|
128
|
+
setext: false,
|
|
129
|
+
closeAtx: false,
|
|
130
|
+
resourceLink: false,
|
|
131
|
+
extensions: [
|
|
132
|
+
toMarkdownGfm(),
|
|
133
|
+
{ handlers: { containerDirective } }
|
|
134
|
+
]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/codec/parse.ts
|
|
139
|
+
var WIKILINK = /!\[\[([^[\]\n]+)\]\]|\[\[([^[\]\n|#]+)(?:#([^[\]\n|]+))?(?:\|([^[\]\n]+))?\]\]/g;
|
|
140
|
+
function parseMdast(markdown) {
|
|
141
|
+
return fromMarkdown(markdown, {
|
|
142
|
+
extensions: micromarkExtensions(),
|
|
143
|
+
mdastExtensions: fromMarkdownExtensions()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function raw(node, inline2) {
|
|
147
|
+
const source = node.type === "html" ? node.value : toMarkdown(node, toMarkdownOptions()).replace(/\n+$/, "");
|
|
148
|
+
return { type: inline2 ? "rawInline" : "rawBlock", attrs: { source } };
|
|
149
|
+
}
|
|
150
|
+
function withMark(marks, mark) {
|
|
151
|
+
return [...marks, mark];
|
|
152
|
+
}
|
|
153
|
+
function text(value, marks) {
|
|
154
|
+
const out = [];
|
|
155
|
+
let last = 0;
|
|
156
|
+
for (const match of value.matchAll(WIKILINK)) {
|
|
157
|
+
const at = match.index;
|
|
158
|
+
if (at > last) out.push(leafText(value.slice(last, at), marks));
|
|
159
|
+
const [, embed, target, heading, label] = match;
|
|
160
|
+
out.push(
|
|
161
|
+
withMarks(
|
|
162
|
+
embed !== void 0 ? { type: "embedInline", attrs: { target: embed } } : {
|
|
163
|
+
type: "wikilink",
|
|
164
|
+
attrs: {
|
|
165
|
+
target: target.trim(),
|
|
166
|
+
heading: heading?.trim() ?? null,
|
|
167
|
+
label: label ?? null
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
marks
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
last = at + match[0].length;
|
|
174
|
+
}
|
|
175
|
+
if (last < value.length) out.push(leafText(value.slice(last), marks));
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
function withMarks(node, marks) {
|
|
179
|
+
return marks.length ? { ...node, marks } : node;
|
|
180
|
+
}
|
|
181
|
+
function leafText(value, marks) {
|
|
182
|
+
return marks.length ? { type: "text", text: value, marks } : { type: "text", text: value };
|
|
183
|
+
}
|
|
184
|
+
function inline(nodes, marks = []) {
|
|
185
|
+
return nodes.flatMap((node) => {
|
|
186
|
+
switch (node.type) {
|
|
187
|
+
case "text":
|
|
188
|
+
return text(node.value, marks);
|
|
189
|
+
case "emphasis":
|
|
190
|
+
return inline(node.children, withMark(marks, { type: "italic" }));
|
|
191
|
+
case "strong":
|
|
192
|
+
return inline(node.children, withMark(marks, { type: "bold" }));
|
|
193
|
+
case "delete":
|
|
194
|
+
return inline(node.children, withMark(marks, { type: "strike" }));
|
|
195
|
+
case "inlineCode":
|
|
196
|
+
return [leafText(node.value, withMark(marks, { type: "code" }))];
|
|
197
|
+
case "link":
|
|
198
|
+
return inline(
|
|
199
|
+
node.children,
|
|
200
|
+
withMark(marks, { type: "link", attrs: { href: node.url, title: node.title ?? null } })
|
|
201
|
+
);
|
|
202
|
+
case "image":
|
|
203
|
+
return [
|
|
204
|
+
withMarks(
|
|
205
|
+
{
|
|
206
|
+
type: "image",
|
|
207
|
+
attrs: { src: node.url, alt: node.alt ?? "", title: node.title ?? null }
|
|
208
|
+
},
|
|
209
|
+
marks
|
|
210
|
+
)
|
|
211
|
+
];
|
|
212
|
+
case "break":
|
|
213
|
+
return [withMarks({ type: "hardBreak" }, marks)];
|
|
214
|
+
default:
|
|
215
|
+
return [withMarks(raw(node, true), marks)];
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
function paragraph(node) {
|
|
220
|
+
const content2 = inline(node.children);
|
|
221
|
+
if (content2.length === 1 && content2[0].type === "embedInline" && !content2[0].marks) {
|
|
222
|
+
return [{ type: "embed", attrs: { ...content2[0].attrs } }];
|
|
223
|
+
}
|
|
224
|
+
return [content2.length ? { type: "paragraph", content: content2 } : { type: "paragraph" }];
|
|
225
|
+
}
|
|
226
|
+
function blocks(nodes, depth) {
|
|
227
|
+
return nodes.flatMap((node) => block(node, depth));
|
|
228
|
+
}
|
|
229
|
+
function callout(node, depth) {
|
|
230
|
+
const parts = readCallout(node);
|
|
231
|
+
if (!parts) return null;
|
|
232
|
+
return {
|
|
233
|
+
type: "callout",
|
|
234
|
+
attrs: { kind: parts.kind, title: parts.title },
|
|
235
|
+
content: blocks(parts.body, depth)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var MARKER = /^\[![A-Za-z]+\][+-]?/;
|
|
239
|
+
function keepMarker(children) {
|
|
240
|
+
const [first, ...rest] = children;
|
|
241
|
+
if (first?.type !== "paragraph") return children;
|
|
242
|
+
const [head, ...tail] = first.children;
|
|
243
|
+
const marker = head?.type === "text" ? MARKER.exec(head.value)?.[0] : void 0;
|
|
244
|
+
if (!marker || head?.type !== "text") return children;
|
|
245
|
+
const remainder = head.value.slice(marker.length);
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
...first,
|
|
249
|
+
children: [
|
|
250
|
+
{ type: "html", value: marker },
|
|
251
|
+
...remainder ? [{ ...head, value: remainder }] : [],
|
|
252
|
+
...tail
|
|
253
|
+
]
|
|
254
|
+
},
|
|
255
|
+
...rest
|
|
256
|
+
];
|
|
257
|
+
}
|
|
258
|
+
function directiveBlock(node, depth) {
|
|
259
|
+
const toggle = readToggle(node);
|
|
260
|
+
if (toggle) {
|
|
261
|
+
const summary = inline(toggle.summary);
|
|
262
|
+
return {
|
|
263
|
+
type: "details",
|
|
264
|
+
content: [
|
|
265
|
+
summary.length ? { type: "detailsSummary", content: summary } : { type: "detailsSummary" },
|
|
266
|
+
{ type: "detailsContent", content: nonEmpty(blocks(toggle.body, depth)) }
|
|
267
|
+
]
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const columns = readColumns(node, depth.columns);
|
|
271
|
+
if (columns) {
|
|
272
|
+
return {
|
|
273
|
+
type: "columns",
|
|
274
|
+
content: columns.map((column) => ({
|
|
275
|
+
type: "column",
|
|
276
|
+
attrs: { width: column.width },
|
|
277
|
+
content: nonEmpty(blocks(column.body, { columns: true }))
|
|
278
|
+
}))
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return raw(node, false);
|
|
282
|
+
}
|
|
283
|
+
function nonEmpty(content2) {
|
|
284
|
+
return content2.length ? content2 : [{ type: "paragraph" }];
|
|
285
|
+
}
|
|
286
|
+
function block(node, depth) {
|
|
287
|
+
switch (node.type) {
|
|
288
|
+
case "paragraph":
|
|
289
|
+
return paragraph(node);
|
|
290
|
+
case "heading": {
|
|
291
|
+
const content2 = inline(node.children);
|
|
292
|
+
return [
|
|
293
|
+
{ type: "heading", attrs: { level: node.depth }, ...content2.length ? { content: content2 } : {} }
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
case "thematicBreak":
|
|
297
|
+
return [{ type: "horizontalRule" }];
|
|
298
|
+
case "code":
|
|
299
|
+
if (node.meta) return [raw(node, false)];
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
type: "codeBlock",
|
|
303
|
+
attrs: { language: node.lang ?? null },
|
|
304
|
+
...node.value ? { content: [{ type: "text", text: node.value }] } : {}
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
case "blockquote":
|
|
308
|
+
return [
|
|
309
|
+
callout(node, depth) ?? {
|
|
310
|
+
type: "blockquote",
|
|
311
|
+
content: nonEmpty(blocks(keepMarker(node.children), depth))
|
|
312
|
+
}
|
|
313
|
+
];
|
|
314
|
+
case "list": {
|
|
315
|
+
const tasks = node.children.map((item) => typeof item.checked === "boolean");
|
|
316
|
+
if (tasks.some(Boolean) && !tasks.every(Boolean)) return [raw(node, false)];
|
|
317
|
+
const isTask = tasks.length > 0 && tasks.every(Boolean);
|
|
318
|
+
const tight = !node.spread && node.children.every((item) => !item.spread);
|
|
319
|
+
const items = node.children.map((item) => ({
|
|
320
|
+
type: isTask ? "taskItem" : "listItem",
|
|
321
|
+
...isTask ? { attrs: { checked: item.checked === true } } : {},
|
|
322
|
+
content: nonEmpty(blocks(item.children, depth))
|
|
323
|
+
}));
|
|
324
|
+
if (isTask) return [{ type: "taskList", attrs: { tight }, content: items }];
|
|
325
|
+
return node.ordered ? [{ type: "orderedList", attrs: { start: node.start ?? 1, tight }, content: items }] : [{ type: "bulletList", attrs: { tight }, content: items }];
|
|
326
|
+
}
|
|
327
|
+
case "table": {
|
|
328
|
+
const width = node.children[0]?.children.length ?? 0;
|
|
329
|
+
return [
|
|
330
|
+
{
|
|
331
|
+
type: "table",
|
|
332
|
+
content: node.children.map((row, r) => ({
|
|
333
|
+
type: "tableRow",
|
|
334
|
+
content: row.children.slice(0, width).map((cell, c) => {
|
|
335
|
+
const content2 = inline(cell.children);
|
|
336
|
+
return {
|
|
337
|
+
type: r === 0 ? "tableHeader" : "tableCell",
|
|
338
|
+
attrs: { align: node.align?.[c] ?? null },
|
|
339
|
+
content: [content2.length ? { type: "paragraph", content: content2 } : { type: "paragraph" }]
|
|
340
|
+
};
|
|
341
|
+
})
|
|
342
|
+
}))
|
|
343
|
+
}
|
|
344
|
+
];
|
|
345
|
+
}
|
|
346
|
+
case "containerDirective":
|
|
347
|
+
return [directiveBlock(node, depth)];
|
|
348
|
+
default:
|
|
349
|
+
return [raw(node, false)];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function parse(markdown) {
|
|
353
|
+
const content2 = blocks(parseMdast(markdown.replace(/\r\n?/g, "\n")).children, { columns: false });
|
|
354
|
+
return { type: "doc", content: content2.length ? content2 : [{ type: "paragraph" }] };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/codec/serialize.ts
|
|
358
|
+
import { toMarkdown as toMarkdown2 } from "mdast-util-to-markdown";
|
|
359
|
+
var MARK_RANK = ["link", "bold", "italic", "strike"];
|
|
360
|
+
var hasMark = (node, type) => node.marks?.some((m) => m.type === type) ?? false;
|
|
361
|
+
var linkOf = (node) => node.marks?.find((m) => m.type === "link");
|
|
362
|
+
var sameLink = (a, b) => linkOf(a)?.attrs.href === linkOf(b)?.attrs.href && linkOf(a)?.attrs.title === linkOf(b)?.attrs.title;
|
|
363
|
+
function wikilinkSource(node, inTable) {
|
|
364
|
+
const a = node.attrs;
|
|
365
|
+
const pipe = inTable ? "\\|" : "|";
|
|
366
|
+
return `[[${a.target}${a.heading ? `#${a.heading}` : ""}${a.label ? `${pipe}${a.label}` : ""}]]`;
|
|
367
|
+
}
|
|
368
|
+
function leaf(node, inTable) {
|
|
369
|
+
switch (node.type) {
|
|
370
|
+
case "text":
|
|
371
|
+
return hasMark(node, "code") ? [{ type: "inlineCode", value: node.text ?? "" }] : [{ type: "text", value: node.text ?? "" }];
|
|
372
|
+
case "hardBreak":
|
|
373
|
+
return [{ type: "break" }];
|
|
374
|
+
case "image": {
|
|
375
|
+
const a = node.attrs;
|
|
376
|
+
return [{ type: "image", url: a.src, alt: a.alt ?? "", title: a.title ?? null }];
|
|
377
|
+
}
|
|
378
|
+
case "wikilink":
|
|
379
|
+
return [{ type: "html", value: wikilinkSource(node, inTable) }];
|
|
380
|
+
case "embedInline":
|
|
381
|
+
return [{ type: "html", value: `![[${node.attrs.target}]]` }];
|
|
382
|
+
case "rawInline":
|
|
383
|
+
return [{ type: "html", value: String(node.attrs.source) }];
|
|
384
|
+
default:
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function phrasing(nodes, inTable = false, rank = 0) {
|
|
389
|
+
if (rank === MARK_RANK.length) return nodes.flatMap((n) => leaf(n, inTable));
|
|
390
|
+
const mark = MARK_RANK[rank];
|
|
391
|
+
const out = [];
|
|
392
|
+
let i = 0;
|
|
393
|
+
while (i < nodes.length) {
|
|
394
|
+
const start = nodes[i];
|
|
395
|
+
const marked = hasMark(start, mark);
|
|
396
|
+
let j = i + 1;
|
|
397
|
+
while (j < nodes.length && hasMark(nodes[j], mark) === marked && (!marked || mark !== "link" || sameLink(start, nodes[j]))) {
|
|
398
|
+
j++;
|
|
399
|
+
}
|
|
400
|
+
const inner = phrasing(nodes.slice(i, j), inTable, rank + 1);
|
|
401
|
+
if (!marked) out.push(...inner);
|
|
402
|
+
else if (mark === "link") {
|
|
403
|
+
const { href, title } = linkOf(start).attrs;
|
|
404
|
+
out.push({ type: "link", url: href, title, children: inner });
|
|
405
|
+
} else if (mark === "bold") out.push({ type: "strong", children: inner });
|
|
406
|
+
else if (mark === "italic") out.push({ type: "emphasis", children: inner });
|
|
407
|
+
else out.push({ type: "delete", children: inner });
|
|
408
|
+
i = j;
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
var content = (node) => node.content ?? [];
|
|
413
|
+
function paragraphOf(node) {
|
|
414
|
+
return { type: "paragraph", children: phrasing(content(node)) };
|
|
415
|
+
}
|
|
416
|
+
function flow(nodes) {
|
|
417
|
+
return nodes.flatMap(block2);
|
|
418
|
+
}
|
|
419
|
+
function block2(node) {
|
|
420
|
+
const attrs = node.attrs ?? {};
|
|
421
|
+
switch (node.type) {
|
|
422
|
+
case "paragraph":
|
|
423
|
+
return [paragraphOf(node)];
|
|
424
|
+
case "heading":
|
|
425
|
+
return [
|
|
426
|
+
{
|
|
427
|
+
type: "heading",
|
|
428
|
+
depth: Math.min(6, Math.max(1, Number(attrs.level) || 1)),
|
|
429
|
+
children: phrasing(content(node))
|
|
430
|
+
}
|
|
431
|
+
];
|
|
432
|
+
case "horizontalRule":
|
|
433
|
+
return [{ type: "thematicBreak" }];
|
|
434
|
+
case "codeBlock":
|
|
435
|
+
return [
|
|
436
|
+
{
|
|
437
|
+
type: "code",
|
|
438
|
+
lang: attrs.language ?? null,
|
|
439
|
+
meta: null,
|
|
440
|
+
value: content(node).map((t) => t.text ?? "").join("")
|
|
441
|
+
}
|
|
442
|
+
];
|
|
443
|
+
case "blockquote":
|
|
444
|
+
return [{ type: "blockquote", children: flow(content(node)) }];
|
|
445
|
+
case "callout": {
|
|
446
|
+
const marker = `[!${String(attrs.kind).toUpperCase()}]`;
|
|
447
|
+
const head = {
|
|
448
|
+
type: "paragraph",
|
|
449
|
+
children: [
|
|
450
|
+
{ type: "html", value: marker },
|
|
451
|
+
...attrs.title ? [{ type: "text", value: ` ${String(attrs.title)}` }] : []
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
return [{ type: "blockquote", children: [head, ...flow(content(node))] }];
|
|
455
|
+
}
|
|
456
|
+
case "bulletList":
|
|
457
|
+
case "orderedList":
|
|
458
|
+
case "taskList": {
|
|
459
|
+
const tight = attrs.tight !== false;
|
|
460
|
+
return [
|
|
461
|
+
{
|
|
462
|
+
type: "list",
|
|
463
|
+
ordered: node.type === "orderedList",
|
|
464
|
+
start: node.type === "orderedList" ? Number(attrs.start ?? 1) : null,
|
|
465
|
+
spread: !tight,
|
|
466
|
+
children: content(node).map((item) => ({
|
|
467
|
+
type: "listItem",
|
|
468
|
+
spread: !tight,
|
|
469
|
+
checked: node.type === "taskList" ? item.attrs?.checked === true : null,
|
|
470
|
+
children: flow(content(item))
|
|
471
|
+
}))
|
|
472
|
+
}
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
case "table": {
|
|
476
|
+
const rows = content(node);
|
|
477
|
+
const align = content(rows[0] ?? { type: "tableRow" }).map(
|
|
478
|
+
(cell) => cell.attrs?.align ?? null
|
|
479
|
+
);
|
|
480
|
+
return [
|
|
481
|
+
{
|
|
482
|
+
type: "table",
|
|
483
|
+
align,
|
|
484
|
+
children: rows.map((row) => ({
|
|
485
|
+
type: "tableRow",
|
|
486
|
+
children: content(row).map((cell) => ({
|
|
487
|
+
type: "tableCell",
|
|
488
|
+
children: phrasing(
|
|
489
|
+
content(cell).flatMap((p) => content(p)),
|
|
490
|
+
true
|
|
491
|
+
)
|
|
492
|
+
}))
|
|
493
|
+
}))
|
|
494
|
+
}
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
case "details": {
|
|
498
|
+
const [summary, body] = [
|
|
499
|
+
content(node).find((n) => n.type === "detailsSummary"),
|
|
500
|
+
content(node).find((n) => n.type === "detailsContent")
|
|
501
|
+
];
|
|
502
|
+
const label = summary && content(summary).length ? [
|
|
503
|
+
{
|
|
504
|
+
type: "paragraph",
|
|
505
|
+
data: { directiveLabel: true },
|
|
506
|
+
children: phrasing(content(summary))
|
|
507
|
+
}
|
|
508
|
+
] : [];
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
type: "containerDirective",
|
|
512
|
+
name: "toggle",
|
|
513
|
+
attributes: {},
|
|
514
|
+
children: [
|
|
515
|
+
...label,
|
|
516
|
+
...flow(content(body ?? { type: "detailsContent" }))
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
case "columns":
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
type: "containerDirective",
|
|
525
|
+
name: "columns",
|
|
526
|
+
attributes: {},
|
|
527
|
+
children: content(node).map((column) => ({
|
|
528
|
+
type: "containerDirective",
|
|
529
|
+
name: "column",
|
|
530
|
+
attributes: column.attrs?.width != null ? { width: String(column.attrs.width) } : {},
|
|
531
|
+
children: flow(content(column))
|
|
532
|
+
}))
|
|
533
|
+
}
|
|
534
|
+
];
|
|
535
|
+
case "embed":
|
|
536
|
+
return [{ type: "html", value: `![[${String(attrs.target)}]]` }];
|
|
537
|
+
case "rawBlock":
|
|
538
|
+
return [{ type: "html", value: String(attrs.source) }];
|
|
539
|
+
default:
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function decodeAttentionReferences(markdown) {
|
|
544
|
+
const references = [...markdown.matchAll(/&#x([0-9A-F]+);/g)];
|
|
545
|
+
if (references.length === 0) return markdown;
|
|
546
|
+
const target = JSON.stringify(parse(markdown));
|
|
547
|
+
let out = markdown;
|
|
548
|
+
for (const reference of references.reverse()) {
|
|
549
|
+
const at = reference.index;
|
|
550
|
+
const candidate = out.slice(0, at) + String.fromCodePoint(Number.parseInt(reference[1], 16)) + out.slice(at + reference[0].length);
|
|
551
|
+
if (JSON.stringify(parse(candidate)) === target) out = candidate;
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
function serialize(doc) {
|
|
556
|
+
const root = { type: "root", children: flow(doc.content ?? []) };
|
|
557
|
+
const markdown = toMarkdown2(root, toMarkdownOptions());
|
|
558
|
+
if (markdown.trim() === "") return "";
|
|
559
|
+
const tidy = markdown.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").replace(/\n+$/, "\n");
|
|
560
|
+
return decodeAttentionReferences(tidy);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/codec/index.ts
|
|
564
|
+
var CODEC_ID = "kit-md@1";
|
|
565
|
+
function normalize(markdown) {
|
|
566
|
+
return serialize(parse(markdown));
|
|
567
|
+
}
|
|
568
|
+
export {
|
|
569
|
+
CALLOUT_KINDS,
|
|
570
|
+
CODEC_ID,
|
|
571
|
+
COLUMNS_MAX,
|
|
572
|
+
COLUMNS_MIN,
|
|
573
|
+
normalize,
|
|
574
|
+
parse,
|
|
575
|
+
serialize
|
|
576
|
+
};
|
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 =
|
|
80
|
+
declare const RENDERER_VERSION = 4;
|
|
77
81
|
type MarkdownProps = {
|
|
78
82
|
/** The Markdown source. */
|
|
79
83
|
children: string;
|
|
@@ -124,6 +128,11 @@ 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>>;
|
|
127
136
|
};
|
|
128
137
|
/**
|
|
129
138
|
* THE ONE RENDERER (gh#1108). GFM — tables, task lists, fenced code, autolinks, footnotes,
|
|
@@ -135,7 +144,7 @@ type MarkdownProps = {
|
|
|
135
144
|
* Renders bare elements; the host wraps it (`<Prose>` from `@godxjp/ui/data-display`), so this
|
|
136
145
|
* package carries no UI-kit dependency.
|
|
137
146
|
*/
|
|
138
|
-
declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, }: MarkdownProps): React.JSX.Element;
|
|
147
|
+
declare function Markdown({ children, remarkPlugins, rehypePlugins, schema, components, resolveUrl, headingId, mermaid, allowedElements, disallowedElements, allowElement, unwrapDisallowed, shortCellLength, calloutTitles, }: MarkdownProps): React.JSX.Element;
|
|
139
148
|
|
|
140
149
|
type MermaidSvgCheck = {
|
|
141
150
|
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
|
|
90
|
-
const name =
|
|
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 =
|
|
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(
|
|
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,172 @@ 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 { directiveFromMarkdown } from "mdast-util-directive";
|
|
343
|
+
import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
|
|
344
|
+
function micromarkExtensions() {
|
|
345
|
+
const [container] = directive().flow[58];
|
|
346
|
+
const containerOnly = { flow: { 58: container } };
|
|
347
|
+
return [gfm(), cjkFriendlyExtension(), containerOnly];
|
|
348
|
+
}
|
|
349
|
+
function fromMarkdownExtensions() {
|
|
350
|
+
return [gfmFromMarkdown(), directiveFromMarkdown()];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/directives.ts
|
|
354
|
+
var DEFAULT_CALLOUT_TITLES = {
|
|
355
|
+
note: "Note",
|
|
356
|
+
tip: "Tip",
|
|
357
|
+
important: "Important",
|
|
358
|
+
warning: "Warning",
|
|
359
|
+
caution: "Caution"
|
|
360
|
+
};
|
|
361
|
+
var element = (hName, hProperties = {}) => ({
|
|
362
|
+
hName,
|
|
363
|
+
hProperties
|
|
364
|
+
});
|
|
365
|
+
function remarkKitSyntax(options = {}) {
|
|
366
|
+
const titles = { ...DEFAULT_CALLOUT_TITLES, ...options.calloutTitles };
|
|
367
|
+
return function() {
|
|
368
|
+
const data = this.data();
|
|
369
|
+
(data.micromarkExtensions ??= []).push(...micromarkExtensions().slice(1));
|
|
370
|
+
(data.fromMarkdownExtensions ??= []).push(fromMarkdownExtensions()[1]);
|
|
371
|
+
const walk = (node, inColumn) => {
|
|
372
|
+
if (node.type === "blockquote") callout(node, titles);
|
|
373
|
+
else if (node.type === "containerDirective") directive2(node, inColumn);
|
|
374
|
+
if (!("children" in node)) return;
|
|
375
|
+
const isColumn = node.type === "containerDirective" && node.name === "column";
|
|
376
|
+
for (const child of node.children) walk(child, inColumn || isColumn);
|
|
377
|
+
};
|
|
378
|
+
return (tree) => walk(tree, false);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function callout(node, titles) {
|
|
382
|
+
const parts = readCallout(node);
|
|
383
|
+
if (!parts) return;
|
|
384
|
+
const title = {
|
|
385
|
+
type: "paragraph",
|
|
386
|
+
data: element("p", { className: ["ui-prose-callout-title"] }),
|
|
387
|
+
children: [{ type: "text", value: parts.title ?? titles[parts.kind] }]
|
|
388
|
+
};
|
|
389
|
+
node.data = element("div", {
|
|
390
|
+
className: ["ui-prose-callout"],
|
|
391
|
+
dataKind: parts.kind,
|
|
392
|
+
role: "note"
|
|
393
|
+
});
|
|
394
|
+
node.children = [title, ...parts.body];
|
|
395
|
+
}
|
|
396
|
+
function directive2(node, nested) {
|
|
397
|
+
const toggle = readToggle(node);
|
|
398
|
+
if (toggle) {
|
|
399
|
+
const summary = {
|
|
400
|
+
type: "paragraph",
|
|
401
|
+
data: element("summary", { className: ["ui-prose-toggle-summary"] }),
|
|
402
|
+
children: toggle.summary
|
|
403
|
+
};
|
|
404
|
+
node.data = element("details", { className: ["ui-prose-toggle"] });
|
|
405
|
+
node.children = [summary, ...toggle.body];
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const columns = readColumns(node, nested);
|
|
409
|
+
if (columns) {
|
|
410
|
+
const shares = columnShares(columns);
|
|
411
|
+
node.data = element("div", { className: ["ui-prose-columns"] });
|
|
412
|
+
node.children = columns.map((column, i) => ({
|
|
413
|
+
type: "containerDirective",
|
|
414
|
+
name: "column",
|
|
415
|
+
attributes: {},
|
|
416
|
+
data: element("div", {
|
|
417
|
+
className: ["ui-prose-column"],
|
|
418
|
+
style: `--prose-column-grow:${shares[i]}`
|
|
419
|
+
}),
|
|
420
|
+
children: column.body
|
|
421
|
+
}));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!node.data?.hName) node.data = element("div", { className: ["ui-prose-directive"] });
|
|
425
|
+
}
|
|
426
|
+
|
|
244
427
|
// src/markdown.tsx
|
|
245
428
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
246
429
|
var MARKDOWN_FORMAT = "md";
|
|
247
|
-
var RENDERER_VERSION =
|
|
430
|
+
var RENDERER_VERSION = 4;
|
|
248
431
|
var URL_ATTRIBUTES = {
|
|
249
432
|
a: "href",
|
|
250
433
|
img: "src",
|
|
@@ -312,9 +495,11 @@ function Markdown({
|
|
|
312
495
|
disallowedElements,
|
|
313
496
|
allowElement,
|
|
314
497
|
unwrapDisallowed,
|
|
315
|
-
shortCellLength = 24
|
|
498
|
+
shortCellLength = 24,
|
|
499
|
+
calloutTitles
|
|
316
500
|
}) {
|
|
317
501
|
const sanitizeSchema = React3.useMemo(() => extendSchema(schema), [schema]);
|
|
502
|
+
const kitSyntax = React3.useMemo(() => remarkKitSyntax({ calloutTitles }), [calloutTitles]);
|
|
318
503
|
const merged = React3.useMemo(() => {
|
|
319
504
|
const HostTable = components?.table;
|
|
320
505
|
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,7 +518,7 @@ function Markdown({
|
|
|
333
518
|
return /* @__PURE__ */ jsx3(
|
|
334
519
|
ReactMarkdown,
|
|
335
520
|
{
|
|
336
|
-
remarkPlugins: [remarkGfm, ...remarkPlugins],
|
|
521
|
+
remarkPlugins: [remarkGfm, kitSyntax, ...remarkPlugins],
|
|
337
522
|
rehypePlugins: [
|
|
338
523
|
[rehypeResolveUrls, { resolve: resolveUrl }],
|
|
339
524
|
[rehypeHeadingIds, { resolve: headingId }],
|
|
@@ -440,6 +625,37 @@ var MARKDOWN_FIXTURES = [
|
|
|
440
625
|
markdown: " )",
|
|
441
626
|
html: '<p><img alt="x"/> <img alt="y"/></p>',
|
|
442
627
|
forbidden: ["data:", "javascript:"]
|
|
628
|
+
},
|
|
629
|
+
// Grammar v1 (gh#1156, renderer v4): the constructs the codec and @godxjp/block-editor model.
|
|
630
|
+
{
|
|
631
|
+
name: "CJK-friendly emphasis next to \u3001\u3002\u300C\u300D\uFF08\uFF09",
|
|
632
|
+
markdown: "\u306E**\u300C\u5F37\u8ABF\u300D**\u3067\u3059\u3002\uFF08*\u6CE8*\uFF09\u300C[\u30EA\u30F3\u30AF](https://example.com/)\u300D\u3001**\u592A\u5B57**\u3002",
|
|
633
|
+
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>'
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "callout with a title, and one without",
|
|
637
|
+
markdown: "> [!WARNING] Careful\n> Body\n\n> [!tip]\n> No title",
|
|
638
|
+
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>'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "a fold marker or unknown kind stays a quote",
|
|
642
|
+
markdown: "> [!NOTE]- folded\n\n> [!INFO] unknown",
|
|
643
|
+
html: "<blockquote>\n<p>[!NOTE]- folded</p>\n</blockquote>\n<blockquote>\n<p>[!INFO] unknown</p>\n</blockquote>"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "toggle with an inline summary",
|
|
647
|
+
markdown: ":::toggle[Read *more*]\nBody\n:::",
|
|
648
|
+
html: '<details class="ui-prose-toggle"><summary class="ui-prose-toggle-summary">Read <em>more</em></summary><p>Body</p></details>'
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: "columns with a width and an equal share",
|
|
652
|
+
markdown: "::::columns\n:::column{width=40}\nLeft\n:::\n\n:::column\nRight\n:::\n::::",
|
|
653
|
+
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>'
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "wikilinks, embeds and times stay text",
|
|
657
|
+
markdown: "See [[Page#Intro|intro]] at 10:30.\n\n![[Diagram]]",
|
|
658
|
+
html: "<p>See [[Page#Intro|intro]] at 10:30.</p>\n<p>![[Diagram]]</p>"
|
|
443
659
|
}
|
|
444
660
|
];
|
|
445
661
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/markdown",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.27.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,14 @@
|
|
|
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-gfm": "^3.1.0",
|
|
40
|
+
"mdast-util-to-markdown": "^2.2.0",
|
|
41
|
+
"micromark-extension-cjk-friendly": "^2.0.2",
|
|
42
|
+
"micromark-extension-directive": "^4.0.0",
|
|
43
|
+
"micromark-extension-gfm": "^3.0.0",
|
|
44
|
+
"micromark-util-types": "^2.0.2",
|
|
33
45
|
"react-markdown": "^10.1.0",
|
|
34
46
|
"rehype-sanitize": "^6.0.0",
|
|
35
47
|
"remark-gfm": "^4.0.1",
|