@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 +63 -0
- package/dist/codec/index.d.ts +71 -0
- package/dist/codec/index.js +611 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +251 -8
- package/package.json +15 -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,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 =
|
|
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
|
|
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,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 =
|
|
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: " )",
|
|
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.
|
|
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",
|