@dogsbay/serialize-core 0.2.0-beta.115 → 0.2.0-beta.117
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/dist/child-links.d.ts +75 -0
- package/dist/child-links.d.ts.map +1 -0
- package/dist/child-links.js +264 -0
- package/dist/child-links.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/inline.d.ts +3 -0
- package/dist/inline.d.ts.map +1 -1
- package/dist/inline.js +32 -10
- package/dist/inline.js.map +1 -1
- package/dist/lead.d.ts +64 -0
- package/dist/lead.d.ts.map +1 -0
- package/dist/lead.js +196 -0
- package/dist/lead.js.map +1 -0
- package/dist/text.d.ts +100 -13
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +298 -2
- package/dist/text.js.map +1 -1
- package/package.json +3 -4
package/dist/lead.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inspect the leading nodes of a page tree.
|
|
3
|
+
*
|
|
4
|
+
* Defensive against empty trees and missing properties — returns
|
|
5
|
+
* `false` for both flags when the tree is empty.
|
|
6
|
+
*/
|
|
7
|
+
export function detectLeadingNodes(tree) {
|
|
8
|
+
if (!tree || tree.length === 0) {
|
|
9
|
+
return { hasH1: false, hasLede: false };
|
|
10
|
+
}
|
|
11
|
+
// H1 detection looks past two things that are not content:
|
|
12
|
+
//
|
|
13
|
+
// - leading raw-HTML CHROME (banners, toolbars — the comparison view
|
|
14
|
+
// injects both). Only tree[0] was inspected before, so a page that
|
|
15
|
+
// opened with a banner "had no H1" and the layout added a second
|
|
16
|
+
// one (external review);
|
|
17
|
+
// - a transparent `group` wrapper, which by definition adds no
|
|
18
|
+
// semantics — it exists only to carry an attribute for a run of
|
|
19
|
+
// blocks (the comparison marks a whole added/removed page with one).
|
|
20
|
+
// A wrapper that hid the page's H1 would resurrect the same bug.
|
|
21
|
+
//
|
|
22
|
+
// LEDE detection deliberately still keys off tree[0]: a paragraph that
|
|
23
|
+
// merely follows an HTML block is not the page's lede, and treating it
|
|
24
|
+
// as one silently suppresses the frontmatter description on any
|
|
25
|
+
// imported page opening with a <figure>/hero block (code review,
|
|
26
|
+
// 2026-07-13). Only an H1 introduces a lede.
|
|
27
|
+
let list = tree;
|
|
28
|
+
for (;;) {
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < list.length && list[i]?.type === "html")
|
|
31
|
+
i++;
|
|
32
|
+
const first = list[i];
|
|
33
|
+
if (first?.type === "group" && first.children?.length) {
|
|
34
|
+
list = first.children;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (isH1(first)) {
|
|
38
|
+
return { hasH1: true, hasLede: isParagraph(list[i + 1]) };
|
|
39
|
+
}
|
|
40
|
+
return { hasH1: false, hasLede: isParagraph(tree[0]) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function isH1(node) {
|
|
44
|
+
if (!node || node.type !== "heading")
|
|
45
|
+
return false;
|
|
46
|
+
// Some importers (dogsbay-md, mkdocs) attach depth as a top-level
|
|
47
|
+
// field; others (Starlight via mdx-jsx) use props.level. Both
|
|
48
|
+
// conventions are valid — check both.
|
|
49
|
+
const depth = node.depth;
|
|
50
|
+
if (typeof depth === "number")
|
|
51
|
+
return depth === 1;
|
|
52
|
+
const level = node.props?.level;
|
|
53
|
+
if (typeof level === "number")
|
|
54
|
+
return level === 1;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function isParagraph(node) {
|
|
58
|
+
if (!node)
|
|
59
|
+
return false;
|
|
60
|
+
return node.type === "paragraph";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Derive a `<meta name="description">` from a page body when the
|
|
64
|
+
* frontmatter doesn't supply one: the first prose paragraph,
|
|
65
|
+
* whitespace-collapsed and truncated at a word boundary.
|
|
66
|
+
*
|
|
67
|
+
* Deliberately Minja-unaware. This only runs at build time, on the
|
|
68
|
+
* preprocessed (resolved) tree — `{{ var }}` and `{% include %}` are
|
|
69
|
+
* already gone, so the first paragraph is real prose. (A still-undefined
|
|
70
|
+
* `{{ var }}` survives `preserve` mode, but that's a content bug equally
|
|
71
|
+
* visible in the body and already flagged by the `unresolved-directives`
|
|
72
|
+
* audit — not the description's job to scrub.) The `--no-preprocess` /
|
|
73
|
+
* `dogsbay convert` paths emit unresolved output by design, where a
|
|
74
|
+
* slightly-off description is the least of it.
|
|
75
|
+
*
|
|
76
|
+
* Scans past a leading H1 / heading / code to the first prose paragraph;
|
|
77
|
+
* returns `undefined` when the page has no prose. See
|
|
78
|
+
* plans/auto-meta-description.md.
|
|
79
|
+
*/
|
|
80
|
+
export function deriveDescription(tree, maxLen = 155) {
|
|
81
|
+
if (!tree || tree.length === 0)
|
|
82
|
+
return undefined;
|
|
83
|
+
// Skip a leading H1 (the page title) so the lede is the node after it.
|
|
84
|
+
let i = isH1(tree[0]) ? 1 : 0;
|
|
85
|
+
for (; i < tree.length; i++) {
|
|
86
|
+
const node = tree[i];
|
|
87
|
+
if (node.type !== "paragraph")
|
|
88
|
+
continue; // skip headings/code/etc.
|
|
89
|
+
const text = paragraphText(node).replace(/\s+/g, " ").trim();
|
|
90
|
+
if (!text)
|
|
91
|
+
continue;
|
|
92
|
+
return truncateAtWord(text, maxLen);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Flatten a paragraph node's text. Tolerant of the three shapes
|
|
98
|
+
* format-astro already handles: flat `node.inline`, wrapped
|
|
99
|
+
* `node.children[{ inline }]`, and pre-rendered `node.html`.
|
|
100
|
+
*/
|
|
101
|
+
function paragraphText(node) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
const visit = (n) => {
|
|
104
|
+
if (!n || typeof n !== "object")
|
|
105
|
+
return;
|
|
106
|
+
const o = n;
|
|
107
|
+
if (typeof o.text === "string")
|
|
108
|
+
parts.push(o.text);
|
|
109
|
+
if (typeof o.code === "string")
|
|
110
|
+
parts.push(o.code);
|
|
111
|
+
if (Array.isArray(o.inline))
|
|
112
|
+
o.inline.forEach(visit);
|
|
113
|
+
if (Array.isArray(o.children))
|
|
114
|
+
o.children.forEach(visit);
|
|
115
|
+
};
|
|
116
|
+
if (Array.isArray(node.inline))
|
|
117
|
+
node.inline.forEach(visit);
|
|
118
|
+
else if (Array.isArray(node.children))
|
|
119
|
+
node.children.forEach(visit);
|
|
120
|
+
else if (typeof node.html === "string") {
|
|
121
|
+
parts.push(node.html.replace(/<[^>]+>/g, ""));
|
|
122
|
+
}
|
|
123
|
+
return parts.join("");
|
|
124
|
+
}
|
|
125
|
+
function truncateAtWord(s, max) {
|
|
126
|
+
if (s.length <= max)
|
|
127
|
+
return s;
|
|
128
|
+
const cut = s.slice(0, max);
|
|
129
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
130
|
+
const base = lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut;
|
|
131
|
+
return base.replace(/[\s.,;:!?-]+$/, "") + "…";
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Extract plain text of the first paragraph in a page tree. Cross-shape
|
|
135
|
+
* tolerant: handles `node.inline` (dogsbay-md parser) and
|
|
136
|
+
* `node.children[{type: "prose", inline}]` (Starlight importer).
|
|
137
|
+
*/
|
|
138
|
+
export function extractFirstParagraph(tree) {
|
|
139
|
+
for (const node of tree) {
|
|
140
|
+
if (node.type !== "paragraph")
|
|
141
|
+
continue;
|
|
142
|
+
const inline = node.inline ?? findProseInline(node.children);
|
|
143
|
+
if (inline)
|
|
144
|
+
return inlineToPlainText(inline).trim();
|
|
145
|
+
}
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
function findProseInline(children) {
|
|
149
|
+
if (!children)
|
|
150
|
+
return undefined;
|
|
151
|
+
for (const child of children) {
|
|
152
|
+
if (child.type === "prose" && child.inline)
|
|
153
|
+
return child.inline;
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
function inlineToPlainText(inline) {
|
|
158
|
+
let out = "";
|
|
159
|
+
for (const node of inline) {
|
|
160
|
+
switch (node.type) {
|
|
161
|
+
case "text":
|
|
162
|
+
out += node.text;
|
|
163
|
+
break;
|
|
164
|
+
case "code":
|
|
165
|
+
out += node.text;
|
|
166
|
+
break;
|
|
167
|
+
case "link":
|
|
168
|
+
case "highlight":
|
|
169
|
+
out += inlineToPlainText(node.children);
|
|
170
|
+
break;
|
|
171
|
+
case "kbd":
|
|
172
|
+
out += node.keys.join("+");
|
|
173
|
+
break;
|
|
174
|
+
case "badge":
|
|
175
|
+
out += node.text;
|
|
176
|
+
break;
|
|
177
|
+
case "math":
|
|
178
|
+
out += node.latex;
|
|
179
|
+
break;
|
|
180
|
+
case "image":
|
|
181
|
+
if (node.alt)
|
|
182
|
+
out += node.alt;
|
|
183
|
+
break;
|
|
184
|
+
case "break":
|
|
185
|
+
out += " ";
|
|
186
|
+
break;
|
|
187
|
+
case "html-inline":
|
|
188
|
+
case "footnote-ref":
|
|
189
|
+
case "icon":
|
|
190
|
+
// No useful plain text; skip.
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=lead.js.map
|
package/dist/lead.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lead.js","sourceRoot":"","sources":["../src/lead.ts"],"names":[],"mappings":"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"}
|
package/dist/text.d.ts
CHANGED
|
@@ -35,21 +35,108 @@ export declare function normalizeTrailingWhitespace(content: string): string;
|
|
|
35
35
|
* use for HTML they cannot represent. Callers should record the degradation.
|
|
36
36
|
*/
|
|
37
37
|
export declare function stripHtml(html: string): string;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* MDX-ish targets but produces source no human wants to maintain — and the
|
|
44
|
-
* point of a migration is clean, editable markdown.
|
|
45
|
-
*
|
|
46
|
-
* Deliberately conservative: only unambiguous inline tags, non-greedy, and
|
|
47
|
-
* block-level markup is left completely alone (a regex cannot restructure
|
|
48
|
-
* tables or lists safely). Anything not listed here survives as raw HTML.
|
|
49
|
-
*/
|
|
50
|
-
export declare function inlineHtmlToMarkdown(html: string): string;
|
|
38
|
+
export declare function inlineHtmlToMarkdown(html: string, options?: {
|
|
39
|
+
/** The caller strips the remaining tags (`stripHtml`), so ALL text
|
|
40
|
+
* becomes Markdown and is escaped, block HTML or not. */
|
|
41
|
+
flatten?: boolean;
|
|
42
|
+
}): string;
|
|
51
43
|
/** Serialize a value as YAML frontmatter scalar, quoting when required. */
|
|
52
44
|
export declare function yamlScalar(value: string): string;
|
|
53
45
|
/** Build a YAML frontmatter block from ordered entries. Empty → "". */
|
|
54
46
|
export declare function frontmatterBlock(entries: Array<[string, string | number | boolean | undefined]>): string;
|
|
47
|
+
/**
|
|
48
|
+
* Text → CommonMark (or MDX) that reads back as the same text.
|
|
49
|
+
*
|
|
50
|
+
* A TreeNode `text` is LITERAL: an importer that met `<script>`, `\*`
|
|
51
|
+
* or `\[x\](…)` in its source stored `<script>`, `*`, `[x](…)`. Written into
|
|
52
|
+
* Markdown unescaped, those become a live element, emphasis and a link —
|
|
53
|
+
* the Docusaurus `.md` export put a spec's `<script>` and a
|
|
54
|
+
* `[x](javascript:…)` link into the site that way.
|
|
55
|
+
*
|
|
56
|
+
* - `<`, `{` and `}` become ENTITIES (`<`, `{`, `}`), which read
|
|
57
|
+
* the same in CommonMark and MDX and which no later MDX pass re-reads. A
|
|
58
|
+
* backslash form did not survive Docusaurus' `sanitizeForMdx`: the text
|
|
59
|
+
* `\{` became `\\{`, a literal backslash and a live expression.
|
|
60
|
+
* - `&` before an entity name becomes `&`.
|
|
61
|
+
* - Backslash, `` ` ``, `*`, `[`, `]`, `~~` and a run of `_` at a word edge
|
|
62
|
+
* are backslash-escaped. Both brackets: an unbalanced `\]` let
|
|
63
|
+
* `[@dependabot[bot]](url)` link the wrong text. A task marker (`[ ]`,
|
|
64
|
+
* `[x]`) starting the text is kept: GFM and Obsidian draw it as a
|
|
65
|
+
* checkbox, and the parser keeps it as text.
|
|
66
|
+
* - A URL written as text keeps its `_` and `\` (GFM autolinks it, and an
|
|
67
|
+
* escape would end up in the href); its brackets and backticks are still
|
|
68
|
+
* escaped.
|
|
69
|
+
*
|
|
70
|
+
* `mdx: true` also escapes a `<` before any non-space character (`<3` is a
|
|
71
|
+
* syntax error in MDX), not only a tag-shaped one, and braces. Without it
|
|
72
|
+
* braces stay, as CommonMark reads them as text.
|
|
73
|
+
*/
|
|
74
|
+
export declare function escapeMarkdownText(text: string, options?: {
|
|
75
|
+
mdx?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* How `<`, `{` and `}` are escaped. `"entity"` (default) reads the same
|
|
78
|
+
* everywhere and survives a later MDX pass (Docusaurus). `"backslash"`
|
|
79
|
+
* (`\<`, `\{`) is easier to read in a file people edit (an Obsidian
|
|
80
|
+
* vault) and is what a target that does not decode entities needs
|
|
81
|
+
* (OpenKnowledge).
|
|
82
|
+
*/
|
|
83
|
+
style?: "entity" | "backslash";
|
|
84
|
+
}): string;
|
|
85
|
+
/**
|
|
86
|
+
* May this URL be written as a link or image destination? Not a
|
|
87
|
+
* `javascript:`, `vbscript:` or `data:` URL (a `data:image/` source is
|
|
88
|
+
* allowed for images), however it is cased or padded with control
|
|
89
|
+
* characters and whitespace — browsers strip those before reading the
|
|
90
|
+
* scheme. markdown-it refuses these when it PARSES Markdown; a link node an
|
|
91
|
+
* importer built directly never passed that check.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isSafeHref(href: string, kind?: "link" | "image"): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Markdown from a string we did not write (an OpenAPI description, which
|
|
96
|
+
* the spec defines as CommonMark) made inert where it could run: raw HTML
|
|
97
|
+
* (`<` before a tag name becomes `<`, which still reads `<`) and link or
|
|
98
|
+
* definition destinations with a `javascript:`/`vbscript:`/`data:` scheme
|
|
99
|
+
* (their `](` is escaped, so the text stays and the link goes). The rest of
|
|
100
|
+
* the Markdown is kept.
|
|
101
|
+
*/
|
|
102
|
+
export declare function neutralizeMarkdown(md: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Apply `fn` to the parts of `md` outside inline code spans, leaving the
|
|
105
|
+
* spans as written. Spans are found as CommonMark finds them — a run of N
|
|
106
|
+
* backticks closed by the next run of exactly N, a backslash-escaped
|
|
107
|
+
* backtick opening nothing — in one pass over the runs (a backtracking
|
|
108
|
+
* RegExp took 7.6 s on 50,000 backticks).
|
|
109
|
+
*/
|
|
110
|
+
export declare function mapOutsideCodeSpans(md: string, fn: (prose: string) => string): string;
|
|
111
|
+
/**
|
|
112
|
+
* A URL as a Markdown link or image destination that the reader resolves
|
|
113
|
+
* back to the same URL, and that nothing in it can end: `&` becomes `&`
|
|
114
|
+
* (so `javascript:` stays those characters rather than decoding to a
|
|
115
|
+
* scheme), a backslash is escaped, and space, `(`, `)`, `<`, `>` and control
|
|
116
|
+
* characters are percent-encoded (a `)` ended the destination and the rest
|
|
117
|
+
* became live Markdown).
|
|
118
|
+
*/
|
|
119
|
+
export declare function markdownDestination(url: string): string;
|
|
120
|
+
/** A link or image title: one line, its quotes and backslashes escaped. */
|
|
121
|
+
export declare function markdownTitle(title: string | undefined): string;
|
|
122
|
+
/**
|
|
123
|
+
* LaTeX for `$…$` / `$$…$$` math read by arithmatex (MkDocs) or Obsidian:
|
|
124
|
+
* an unescaped `$` would close the span early, so it becomes `\$`.
|
|
125
|
+
*/
|
|
126
|
+
export declare function markdownLatex(latex: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* Math for remark-math / micromark (Docusaurus, MDX targets), which reads
|
|
129
|
+
* `$` like code: a run of N dollars is closed by a run of N, and `\$` does
|
|
130
|
+
* NOT escape. The delimiter is sized past any run in the LaTeX instead.
|
|
131
|
+
*/
|
|
132
|
+
export declare function remarkMath(latex: string, display: boolean): string;
|
|
133
|
+
/**
|
|
134
|
+
* A footnote label every reader accepts, written the same way for the
|
|
135
|
+
* reference and its definition. Injective: other characters are
|
|
136
|
+
* hex-encoded (`note.1` → `note_x2e_1`), not deleted, so `*` and `†` do
|
|
137
|
+
* not collide with footnote 1.
|
|
138
|
+
*/
|
|
139
|
+
export declare function footnoteLabel(label: string): string;
|
|
140
|
+
/** An id for `{#id}` or `id="…"`: characters that cannot end either. */
|
|
141
|
+
export declare function safeAnchorId(id: string): string;
|
|
55
142
|
//# sourceMappingURL=text.d.ts.map
|
package/dist/text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,GAAG,GAAG,GAAS,GAAG,MAAM,CAOjF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkC9C;
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,GAAE,GAAG,GAAG,GAAS,GAAG,MAAM,CAOjF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED,+EAA+E;AAC/E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAMnE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkC9C;AAoBD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP;8DAC0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,GACL,MAAM,CAkDR;AA6BD,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA8BhD;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,GAC9D,MAAM,CAQR;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IACP,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;CAC3B,GACL,MAAM,CA+BR;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,GAAG,OAAgB,GAAG,OAAO,CAKjF;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAyBrD;AA8BD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CAwCrF;AAQD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMvD;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE/D;AAYD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE/C"}
|
package/dist/text.js
CHANGED
|
@@ -114,7 +114,12 @@ export function stripHtml(html) {
|
|
|
114
114
|
* block-level markup is left completely alone (a regex cannot restructure
|
|
115
115
|
* tables or lists safely). Anything not listed here survives as raw HTML.
|
|
116
116
|
*/
|
|
117
|
-
|
|
117
|
+
/** Block-level HTML: its text is not inline Markdown. */
|
|
118
|
+
/** HTML that STARTS an HTML block (CommonMark start condition 6): its text
|
|
119
|
+
* is not read as Markdown. A block tag later in the line is inline HTML,
|
|
120
|
+
* and the text around it is Markdown. */
|
|
121
|
+
const HTML_BLOCK = /^\s*<\/?(?:table|thead|tbody|tr|td|th|pre|div|ul|ol|li|dl|dt|dd|blockquote|h[1-6]|section|details|summary|figure)\b/i;
|
|
122
|
+
export function inlineHtmlToMarkdown(html, options = {}) {
|
|
118
123
|
// Code spans are converted FIRST and then masked, so the emphasis/link rules
|
|
119
124
|
// below cannot rewrite markup that is meant to be shown literally
|
|
120
125
|
// (`<code><b>x</b></code>` must stay `<b>x</b>`, not become `**x**`).
|
|
@@ -123,6 +128,24 @@ export function inlineHtmlToMarkdown(html) {
|
|
|
123
128
|
masked.push(codeSpanText(inner));
|
|
124
129
|
return `\u0000CODE${masked.length - 1}\u0000`;
|
|
125
130
|
});
|
|
131
|
+
// Text between tags is literal: `[x](javascript:…)` or `*x*` written there
|
|
132
|
+
// must not become a link or emphasis in the Markdown. Only when the
|
|
133
|
+
// result stays inline: inside an HTML BLOCK (a table, a <pre>, a <div>)
|
|
134
|
+
// CommonMark processes no escapes, so they would show as backslashes, and
|
|
135
|
+
// no Markdown is read there either. (Text is HTML here: `<` already
|
|
136
|
+
// means `<`, so `&` and `<` are left alone.)
|
|
137
|
+
if (options.flatten || !HTML_BLOCK.test(out)) {
|
|
138
|
+
out = out.replace(/(^|>)([^<>]+)(?=<|$)/g, (_m, lead, text) => lead +
|
|
139
|
+
text
|
|
140
|
+
.replace(/\\/g, "\\\\")
|
|
141
|
+
.replace(/[*[\]]/g, "\\$&")
|
|
142
|
+
// A backtick as an entity: an escaped `\`` is still taken for a
|
|
143
|
+
// code-span delimiter by the MDX passes that mask code, and the
|
|
144
|
+
// braces between two of them were left live.
|
|
145
|
+
.replace(/`/g, "`")
|
|
146
|
+
.replace(/~~/g, "\\~\\~")
|
|
147
|
+
.replace(/_+/g, (run, at, all) => /[A-Za-z0-9]/.test(all[at - 1] ?? "") && /[A-Za-z0-9]/.test(all[at + run.length] ?? "") ? run : run.replace(/_/g, "\\_")));
|
|
148
|
+
}
|
|
126
149
|
out = out
|
|
127
150
|
.replace(/<(?:strong|b)>([\s\S]*?)<\/(?:strong|b)>/gi, "**$1**")
|
|
128
151
|
.replace(/<(?:em|i)>([\s\S]*?)<\/(?:em|i)>/gi, "*$1*")
|
|
@@ -131,10 +154,25 @@ export function inlineHtmlToMarkdown(html) {
|
|
|
131
154
|
.replace(
|
|
132
155
|
// `[^>]*` would stop at a `>` inside another attribute value
|
|
133
156
|
// (`<a href="x" title="a > b">`), so skip quoted runs explicitly.
|
|
134
|
-
/<a\s+href="([^"]*)"(?:[^>"']|"[^"]*"|'[^']*')*>([\s\S]*?)<\/a>/gi,
|
|
157
|
+
/<a\s+href="([^"]*)"(?:[^>"']|"[^"]*"|'[^']*')*>([\s\S]*?)<\/a>/gi,
|
|
158
|
+
// An unsafe scheme keeps the text and loses the link.
|
|
159
|
+
(_m, href, text) => {
|
|
160
|
+
const url = decodeAttr(href);
|
|
161
|
+
return isSafeHref(url) ? `[${text}](${markdownDestination(url)})` : text;
|
|
162
|
+
})
|
|
135
163
|
.replace(/<br\s*\/?>/gi, " \n");
|
|
136
164
|
return out.replace(/\u0000CODE(\d+)\u0000/g, (_m, i) => masked[Number(i)]);
|
|
137
165
|
}
|
|
166
|
+
/** An HTML attribute value as the browser reads it (entities decoded). */
|
|
167
|
+
function decodeAttr(v) {
|
|
168
|
+
return v
|
|
169
|
+
.replace(/&#x([0-9a-f]+);?/gi, (_m, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
170
|
+
.replace(/&#(\d+);?/g, (_m, d) => String.fromCodePoint(Number(d)))
|
|
171
|
+
.replace(/:/gi, ":")
|
|
172
|
+
.replace(/&tab;/gi, "\t")
|
|
173
|
+
.replace(/&newline;/gi, "\n")
|
|
174
|
+
.replace(/&/gi, "&");
|
|
175
|
+
}
|
|
138
176
|
/** CommonMark code span (see serialize-core `codeSpan`; duplicated here to keep
|
|
139
177
|
* text.ts free of an inline.ts import cycle). */
|
|
140
178
|
function codeSpanText(text) {
|
|
@@ -188,4 +226,262 @@ export function frontmatterBlock(entries) {
|
|
|
188
226
|
return "";
|
|
189
227
|
return `---\n${lines.join("\n")}\n---`;
|
|
190
228
|
}
|
|
229
|
+
/** A URL written as text: GFM autolinks it, so a backslash or `_` escape
|
|
230
|
+
* inside it would land in the href. */
|
|
231
|
+
const URL_TEXT = /\b(?:https?:\/\/|www\.)[^\s<>]+/g;
|
|
232
|
+
/**
|
|
233
|
+
* Text → CommonMark (or MDX) that reads back as the same text.
|
|
234
|
+
*
|
|
235
|
+
* A TreeNode `text` is LITERAL: an importer that met `<script>`, `\*`
|
|
236
|
+
* or `\[x\](…)` in its source stored `<script>`, `*`, `[x](…)`. Written into
|
|
237
|
+
* Markdown unescaped, those become a live element, emphasis and a link —
|
|
238
|
+
* the Docusaurus `.md` export put a spec's `<script>` and a
|
|
239
|
+
* `[x](javascript:…)` link into the site that way.
|
|
240
|
+
*
|
|
241
|
+
* - `<`, `{` and `}` become ENTITIES (`<`, `{`, `}`), which read
|
|
242
|
+
* the same in CommonMark and MDX and which no later MDX pass re-reads. A
|
|
243
|
+
* backslash form did not survive Docusaurus' `sanitizeForMdx`: the text
|
|
244
|
+
* `\{` became `\\{`, a literal backslash and a live expression.
|
|
245
|
+
* - `&` before an entity name becomes `&`.
|
|
246
|
+
* - Backslash, `` ` ``, `*`, `[`, `]`, `~~` and a run of `_` at a word edge
|
|
247
|
+
* are backslash-escaped. Both brackets: an unbalanced `\]` let
|
|
248
|
+
* `[@dependabot[bot]](url)` link the wrong text. A task marker (`[ ]`,
|
|
249
|
+
* `[x]`) starting the text is kept: GFM and Obsidian draw it as a
|
|
250
|
+
* checkbox, and the parser keeps it as text.
|
|
251
|
+
* - A URL written as text keeps its `_` and `\` (GFM autolinks it, and an
|
|
252
|
+
* escape would end up in the href); its brackets and backticks are still
|
|
253
|
+
* escaped.
|
|
254
|
+
*
|
|
255
|
+
* `mdx: true` also escapes a `<` before any non-space character (`<3` is a
|
|
256
|
+
* syntax error in MDX), not only a tag-shaped one, and braces. Without it
|
|
257
|
+
* braces stay, as CommonMark reads them as text.
|
|
258
|
+
*/
|
|
259
|
+
export function escapeMarkdownText(text, options = {}) {
|
|
260
|
+
const bs = options.style === "backslash";
|
|
261
|
+
const task = /^\s*\[[ xX]\](?=\s|$)/.exec(text);
|
|
262
|
+
const head = task ? task[0] : "";
|
|
263
|
+
const body = text.slice(head.length);
|
|
264
|
+
const common = (s) => s
|
|
265
|
+
.replace(/&(?=#?[A-Za-z0-9]+;)/g, "&")
|
|
266
|
+
.replace(options.mdx ? /<(?=\S|$)/g : /<(?=[A-Za-z/!?])/g, bs ? "\\<" : "<")
|
|
267
|
+
.replace(options.mdx ? /[{}]/g : /(?!)/g, (b) => (bs ? `\\${b}` : b === "{" ? "{" : "}"))
|
|
268
|
+
.replace(/[[\]]/g, "\\$&")
|
|
269
|
+
.replace(/`/g, bs ? "\\`" : "`")
|
|
270
|
+
// A `$` in text is a dollar, not math: remark-math and MDX passes
|
|
271
|
+
// that mask `$…$` must not see a span (its braces stayed live).
|
|
272
|
+
.replace(/\$/g, bs ? "\\$" : "$");
|
|
273
|
+
const prose = (s) => common(s.replace(/\\/g, "\\\\"))
|
|
274
|
+
.replace(/\*/g, "\\*")
|
|
275
|
+
.replace(/~~/g, "\\~\\~")
|
|
276
|
+
.replace(/_+/g, (run, at, all) => {
|
|
277
|
+
const before = all[at - 1] ?? "";
|
|
278
|
+
const after = all[at + run.length] ?? "";
|
|
279
|
+
return /[A-Za-z0-9]/.test(before) && /[A-Za-z0-9]/.test(after) ? run : run.replace(/_/g, "\\_");
|
|
280
|
+
});
|
|
281
|
+
let out = "";
|
|
282
|
+
let last = 0;
|
|
283
|
+
for (const m of body.matchAll(URL_TEXT)) {
|
|
284
|
+
out += prose(body.slice(last, m.index)) + common(m[0]);
|
|
285
|
+
last = m.index + m[0].length;
|
|
286
|
+
}
|
|
287
|
+
return head + out + prose(body.slice(last));
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* May this URL be written as a link or image destination? Not a
|
|
291
|
+
* `javascript:`, `vbscript:` or `data:` URL (a `data:image/` source is
|
|
292
|
+
* allowed for images), however it is cased or padded with control
|
|
293
|
+
* characters and whitespace — browsers strip those before reading the
|
|
294
|
+
* scheme. markdown-it refuses these when it PARSES Markdown; a link node an
|
|
295
|
+
* importer built directly never passed that check.
|
|
296
|
+
*/
|
|
297
|
+
export function isSafeHref(href, kind = "link") {
|
|
298
|
+
const scheme = href.replace(/[\u0000-\u0020\u007f]+/g, "").toLowerCase();
|
|
299
|
+
if (/^(?:javascript|vbscript):/.test(scheme))
|
|
300
|
+
return false;
|
|
301
|
+
if (/^data:/.test(scheme))
|
|
302
|
+
return kind === "image" && /^data:image\/(?:gif|png|jpeg|webp);/.test(scheme);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Markdown from a string we did not write (an OpenAPI description, which
|
|
307
|
+
* the spec defines as CommonMark) made inert where it could run: raw HTML
|
|
308
|
+
* (`<` before a tag name becomes `<`, which still reads `<`) and link or
|
|
309
|
+
* definition destinations with a `javascript:`/`vbscript:`/`data:` scheme
|
|
310
|
+
* (their `](` is escaped, so the text stays and the link goes). The rest of
|
|
311
|
+
* the Markdown is kept.
|
|
312
|
+
*/
|
|
313
|
+
export function neutralizeMarkdown(md) {
|
|
314
|
+
// Code is shown, not read: a `<token>` in a code span or fence stays.
|
|
315
|
+
const masked = [];
|
|
316
|
+
const mask = (m) => `\u0000M${masked.push(m) - 1}\u0000`;
|
|
317
|
+
let out = maskFencedCode(md, mask);
|
|
318
|
+
out = mapOutsideCodeSpans(out, (prose) => prose
|
|
319
|
+
// A safe angle-bracket destination is rewritten plain (percent-
|
|
320
|
+
// encoded), so every `<` left can be escaped without exception.
|
|
321
|
+
.replace(/\]\(\s*<([^<>\n]*)>/g, (m, url) => isSafeHref(markdownUrl(url)) ? `](${markdownDestination(markdownUrl(url))}` : `\\](${url}`)
|
|
322
|
+
// A destination whose scheme could run, however it is spelled:
|
|
323
|
+
// entities and backslash escapes decoded first, as the reader does.
|
|
324
|
+
// Escaping the `]` keeps the text and drops the link.
|
|
325
|
+
// (Not one the angle-bracket pass above already escaped: `\](`.)
|
|
326
|
+
.replace(/(?<!\\)\](\(\s*)([^\s)<>]*)/g, (m, open, dest) => isSafeHref(markdownUrl(dest)) ? m : `\\]${open}${dest}`)
|
|
327
|
+
.replace(/^(\s{0,3}\[[^\]\n]*)\](:\s*<?)(\S*)/gm, (m, label, sep, dest) => isSafeHref(markdownUrl(dest)) ? m : `${label}\\]${sep}${dest}`)
|
|
328
|
+
.replace(/<(?=[A-Za-z/!?])/g, "<"));
|
|
329
|
+
return out.replace(/\u0000M(\d+)\u0000/g, (_m, n) => masked[Number(n)]);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Replace each fenced code block (opening line to its closing fence, or to
|
|
333
|
+
* the end) with `mask(block)`. Line by line: a RegExp backtracked
|
|
334
|
+
* quadratically on one long line of backticks.
|
|
335
|
+
*/
|
|
336
|
+
function maskFencedCode(md, mask) {
|
|
337
|
+
const lines = md.split("\n");
|
|
338
|
+
const out = [];
|
|
339
|
+
for (let i = 0; i < lines.length; i++) {
|
|
340
|
+
const open = /^ {0,3}(`{3,}|~{3,})/.exec(lines[i]);
|
|
341
|
+
// A backtick fence's info string may not contain a backtick.
|
|
342
|
+
if (!open || (open[1][0] === "`" && lines[i].slice(open[0].length).includes("`"))) {
|
|
343
|
+
out.push(lines[i]);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const fence = open[1];
|
|
347
|
+
let j = i + 1;
|
|
348
|
+
while (j < lines.length) {
|
|
349
|
+
const close = /^ {0,3}(`+|~+)[ \t]*$/.exec(lines[j]);
|
|
350
|
+
if (close && close[1][0] === fence[0] && close[1].length >= fence.length)
|
|
351
|
+
break;
|
|
352
|
+
j++;
|
|
353
|
+
}
|
|
354
|
+
out.push(mask(lines.slice(i, Math.min(j, lines.length - 1) + 1).join("\n")));
|
|
355
|
+
i = j;
|
|
356
|
+
}
|
|
357
|
+
return out.join("\n");
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Apply `fn` to the parts of `md` outside inline code spans, leaving the
|
|
361
|
+
* spans as written. Spans are found as CommonMark finds them — a run of N
|
|
362
|
+
* backticks closed by the next run of exactly N, a backslash-escaped
|
|
363
|
+
* backtick opening nothing — in one pass over the runs (a backtracking
|
|
364
|
+
* RegExp took 7.6 s on 50,000 backticks).
|
|
365
|
+
*/
|
|
366
|
+
export function mapOutsideCodeSpans(md, fn) {
|
|
367
|
+
const runs = [];
|
|
368
|
+
for (let i = 0; i < md.length;) {
|
|
369
|
+
if (md[i] !== "`") {
|
|
370
|
+
i++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
let j = i;
|
|
374
|
+
while (md[j] === "`")
|
|
375
|
+
j++;
|
|
376
|
+
let bs = 0;
|
|
377
|
+
for (let k = i - 1; k >= 0 && md[k] === "\\"; k--)
|
|
378
|
+
bs++;
|
|
379
|
+
// An escaped first backtick is literal; the rest of the run still counts.
|
|
380
|
+
if (bs % 2) {
|
|
381
|
+
if (j - i > 1)
|
|
382
|
+
runs.push({ at: i + 1, len: j - i - 1 });
|
|
383
|
+
}
|
|
384
|
+
else
|
|
385
|
+
runs.push({ at: i, len: j - i });
|
|
386
|
+
i = j;
|
|
387
|
+
}
|
|
388
|
+
const byLen = new Map();
|
|
389
|
+
runs.forEach((r, n) => {
|
|
390
|
+
const list = byLen.get(r.len);
|
|
391
|
+
if (list)
|
|
392
|
+
list.push(n);
|
|
393
|
+
else
|
|
394
|
+
byLen.set(r.len, [n]);
|
|
395
|
+
});
|
|
396
|
+
const next = new Map(); // len → index into byLen list
|
|
397
|
+
let out = "";
|
|
398
|
+
let from = 0;
|
|
399
|
+
for (let n = 0; n < runs.length; n++) {
|
|
400
|
+
const r = runs[n];
|
|
401
|
+
if (r.at < from)
|
|
402
|
+
continue;
|
|
403
|
+
const list = byLen.get(r.len);
|
|
404
|
+
let k = next.get(r.len) ?? 0;
|
|
405
|
+
while (k < list.length && list[k] <= n)
|
|
406
|
+
k++;
|
|
407
|
+
next.set(r.len, k);
|
|
408
|
+
if (k >= list.length)
|
|
409
|
+
continue; // no closer: literal backticks
|
|
410
|
+
const close = runs[list[k]];
|
|
411
|
+
out += fn(md.slice(from, r.at)) + md.slice(r.at, close.at + close.len);
|
|
412
|
+
from = close.at + close.len;
|
|
413
|
+
n = list[k];
|
|
414
|
+
}
|
|
415
|
+
return out + fn(md.slice(from));
|
|
416
|
+
}
|
|
417
|
+
/** A Markdown destination as the reader resolves it: backslash escapes
|
|
418
|
+
* and character references decoded. */
|
|
419
|
+
function markdownUrl(dest) {
|
|
420
|
+
return decodeAttr(dest.replace(/\\([!-/:-@[-`{-~])/g, "$1"));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A URL as a Markdown link or image destination that the reader resolves
|
|
424
|
+
* back to the same URL, and that nothing in it can end: `&` becomes `&`
|
|
425
|
+
* (so `javascript:` stays those characters rather than decoding to a
|
|
426
|
+
* scheme), a backslash is escaped, and space, `(`, `)`, `<`, `>` and control
|
|
427
|
+
* characters are percent-encoded (a `)` ended the destination and the rest
|
|
428
|
+
* became live Markdown).
|
|
429
|
+
*/
|
|
430
|
+
export function markdownDestination(url) {
|
|
431
|
+
return url
|
|
432
|
+
// Only an `&` the reader would decode (`:`, `&`): `Q&A.md` stays.
|
|
433
|
+
.replace(/&(?=#?[A-Za-z0-9]+;)/g, "&")
|
|
434
|
+
.replace(/\\/g, "\\\\")
|
|
435
|
+
.replace(/[\u0000-\u0020\u007f()<>]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
|
|
436
|
+
}
|
|
437
|
+
/** A link or image title: one line, its quotes and backslashes escaped. */
|
|
438
|
+
export function markdownTitle(title) {
|
|
439
|
+
return title ? ` "${title.replace(/\s*[\r\n]+\s*/g, " ").replace(/["\\]/g, "\\$&")}"` : "";
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A tag-shaped `<` in LaTeX becomes `< ` (with a space): inert as HTML,
|
|
443
|
+
* and the same to KaTeX/MathJax in math mode. Where no math plugin is
|
|
444
|
+
* configured the span is plain Markdown, and a `<script>` in the LaTeX was
|
|
445
|
+
* live. (`\lt` broke `\text{a<b}`.)
|
|
446
|
+
*/
|
|
447
|
+
function inertLatex(latex) {
|
|
448
|
+
return latex.replace(/<(?=[A-Za-z/!?])/g, "< ");
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* LaTeX for `$…$` / `$$…$$` math read by arithmatex (MkDocs) or Obsidian:
|
|
452
|
+
* an unescaped `$` would close the span early, so it becomes `\$`.
|
|
453
|
+
*/
|
|
454
|
+
export function markdownLatex(latex) {
|
|
455
|
+
return inertLatex(latex).replace(/(\\*)\$/g, (m, bs) => (bs.length % 2 ? m : `${bs}\\$`));
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Math for remark-math / micromark (Docusaurus, MDX targets), which reads
|
|
459
|
+
* `$` like code: a run of N dollars is closed by a run of N, and `\$` does
|
|
460
|
+
* NOT escape. The delimiter is sized past any run in the LaTeX instead.
|
|
461
|
+
*/
|
|
462
|
+
export function remarkMath(latex, display) {
|
|
463
|
+
const tex = inertLatex(latex);
|
|
464
|
+
const longest = Math.max(0, ...[...tex.matchAll(/\$+/g)].map((m) => m[0].length));
|
|
465
|
+
if (display) {
|
|
466
|
+
const fence = "$".repeat(Math.max(2, longest + 1));
|
|
467
|
+
return `${fence}\n${tex}\n${fence}`;
|
|
468
|
+
}
|
|
469
|
+
const fence = "$".repeat(longest + 1);
|
|
470
|
+
const pad = /^\$|\$$/.test(tex) ? " " : "";
|
|
471
|
+
return `${fence}${pad}${tex}${pad}${fence}`;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* A footnote label every reader accepts, written the same way for the
|
|
475
|
+
* reference and its definition. Injective: other characters are
|
|
476
|
+
* hex-encoded (`note.1` → `note_x2e_1`), not deleted, so `*` and `†` do
|
|
477
|
+
* not collide with footnote 1.
|
|
478
|
+
*/
|
|
479
|
+
export function footnoteLabel(label) {
|
|
480
|
+
const out = String(label).replace(/[^A-Za-z0-9-]|_/g, (c) => `_x${c.codePointAt(0).toString(16)}_`);
|
|
481
|
+
return out || "1";
|
|
482
|
+
}
|
|
483
|
+
/** An id for `{#id}` or `id="…"`: characters that cannot end either. */
|
|
484
|
+
export function safeAnchorId(id) {
|
|
485
|
+
return id.replace(/[^\w.:-]/g, "-");
|
|
486
|
+
}
|
|
191
487
|
//# sourceMappingURL=text.js.map
|