@simple-mark/model 0.1.0 → 0.3.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/index.d.ts +71 -2
- package/index.js +210 -1
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -1,11 +1,80 @@
|
|
|
1
1
|
import { MarkdownExtension } from "@lezer/markdown";
|
|
2
|
-
import { Tree } from "@lezer/common";
|
|
2
|
+
import { SyntaxNode, Tree } from "@lezer/common";
|
|
3
3
|
//#region packages/model/src/parser.d.ts
|
|
4
4
|
/** The Markdown flavor every SimpleMark renderer parses: CommonMark + GFM + $math$. */
|
|
5
5
|
export declare const markdownExtensions: MarkdownExtension[];
|
|
6
6
|
/** A Lezer parser for that flavor, for hosts without CodeMirror (e.g. a canvas renderer). */
|
|
7
7
|
export declare const markdownParser: import("@lezer/markdown").MarkdownParser;
|
|
8
8
|
//#endregion
|
|
9
|
+
//#region packages/model/src/links.d.ts
|
|
10
|
+
interface LinkAt {
|
|
11
|
+
/** An image (``) or anything else that links (links, autolinks, bare URLs). */
|
|
12
|
+
kind: 'link' | 'image';
|
|
13
|
+
/** The target as written; null when a reference (`[text][label]`) has no definition. */
|
|
14
|
+
target: string | null;
|
|
15
|
+
/** Where the link is in the text. */
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The link at `pos`: a link, image, autolink, bare URL, or the URL of a definition
|
|
21
|
+
* (`[label]: url`). `text` must use one character per line break, so offsets match the tree.
|
|
22
|
+
*/
|
|
23
|
+
export declare function linkAt(tree: Tree, text: string, pos: number): LinkAt | null;
|
|
24
|
+
/**
|
|
25
|
+
* What a link target is:
|
|
26
|
+
* - `web`: http(s), mailto, or a bare `www.` address (given back as a full URL)
|
|
27
|
+
* - `anchor`: `#heading` in the same document
|
|
28
|
+
* - `file`: a relative or absolute path (an image, another Markdown file, …)
|
|
29
|
+
* - `unsafe`: any other scheme (javascript:, data:, file:, …), which should not be opened
|
|
30
|
+
*/
|
|
31
|
+
export declare function linkKind(target: string): {
|
|
32
|
+
kind: 'web' | 'anchor' | 'file' | 'unsafe';
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The anchor of a heading, as GitHub makes it: lower case, punctuation removed, spaces as
|
|
37
|
+
* hyphens ("Next Steps!" → "next-steps", "다음 단계" → "다음-단계").
|
|
38
|
+
*/
|
|
39
|
+
export declare function headingSlug(heading: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* The headings of a document with their anchors; repeated anchors get -1, -2, … like GitHub.
|
|
42
|
+
* `from` is where each heading starts.
|
|
43
|
+
*/
|
|
44
|
+
export declare function headingAnchors(tree: Tree, text: string): {
|
|
45
|
+
anchor: string;
|
|
46
|
+
from: number;
|
|
47
|
+
}[];
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region packages/model/src/previews.d.ts
|
|
50
|
+
/** An image as written in the document. */
|
|
51
|
+
interface ImageSource {
|
|
52
|
+
alt: string;
|
|
53
|
+
/** The path as written, or null when a reference has no definition. */
|
|
54
|
+
src: string | null;
|
|
55
|
+
/** For messages: the path, or the reference label (`[logo]`) that has no definition. */
|
|
56
|
+
raw: string;
|
|
57
|
+
}
|
|
58
|
+
/** A preview drawn below the line that ends at `at` (an offset in the text). */
|
|
59
|
+
type PreviewBlock = {
|
|
60
|
+
at: number;
|
|
61
|
+
images: ImageSource[];
|
|
62
|
+
} | {
|
|
63
|
+
at: number;
|
|
64
|
+
tex: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The TeX of a math node (`InlineMath` or `BlockMath`), without delimiters or quote markers;
|
|
68
|
+
* null while it is not closed. `slice` reads the document.
|
|
69
|
+
*/
|
|
70
|
+
export declare function mathSource(node: SyntaxNode, slice: (from: number, to: number) => string): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* The previews of a document: the images of each line (one entry per line, in order) and
|
|
73
|
+
* closed, non-empty `$$` blocks. `text` must use one character per line break ("\n"), so
|
|
74
|
+
* offsets match the tree.
|
|
75
|
+
*/
|
|
76
|
+
export declare function previewBlocks(tree: Tree, text: string): PreviewBlock[];
|
|
77
|
+
//#endregion
|
|
9
78
|
//#region packages/model/src/styles.d.ts
|
|
10
79
|
/** Styles of whole lines, in precedence order (later wins when two set the same property). */
|
|
11
80
|
export declare const BLOCK_STYLES: readonly ['quote', 'codeblock', 'mathblock', 'table', 'tableHeader', 'tableRowAlt', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
|
|
@@ -113,4 +182,4 @@ export declare function resolveLineLook(blocks: Iterable<BlockStyle>, theme?: Th
|
|
|
113
182
|
/** The look of text with these block and inline styles (any order). */
|
|
114
183
|
export declare function resolveLook(blocks: Iterable<BlockStyle>, inline: Iterable<InlineStyle>, theme?: Theme): Look;
|
|
115
184
|
//#endregion
|
|
116
|
-
export type { BlockRange, BlockStyle, ColumnAlign, InlineStyle, LineLook, Look, MarkdownStyles, RenderedTable, RowCell, Span, Table, Theme };
|
|
185
|
+
export type { BlockRange, BlockStyle, ColumnAlign, ImageSource, InlineStyle, LineLook, LinkAt, Look, MarkdownStyles, PreviewBlock, RenderedTable, RowCell, Span, Table, Theme };
|
package/index.js
CHANGED
|
@@ -103,6 +103,215 @@ const markdownExtensions = [GFM, {
|
|
|
103
103
|
/** A Lezer parser for that flavor, for hosts without CodeMirror (e.g. a canvas renderer). */
|
|
104
104
|
const markdownParser = parser.configure(markdownExtensions);
|
|
105
105
|
//#endregion
|
|
106
|
+
//#region packages/model/src/previews.ts
|
|
107
|
+
/** A reference label (`[Logo]`) as definitions are looked up: case and spacing ignored. */
|
|
108
|
+
const normalizeLabel = (label) => label.slice(1, -1).trim().replace(/\s+/g, " ").toLowerCase();
|
|
109
|
+
/** A link destination as written, without the optional angle brackets (`<a b.png>`). */
|
|
110
|
+
const destination = (text, url) => text.slice(url.from, url.to).replace(/^<(.*)>$/, "$1");
|
|
111
|
+
/** Link reference definitions (`[label]: url`) of the document, by normalized label. */
|
|
112
|
+
function linkDefinitions(tree, text) {
|
|
113
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
114
|
+
for (let node = tree.topNode.firstChild; node; node = node.nextSibling) {
|
|
115
|
+
if (node.name !== "LinkReference") continue;
|
|
116
|
+
const label = node.getChild("LinkLabel");
|
|
117
|
+
const url = node.getChild("URL");
|
|
118
|
+
if (label && url) {
|
|
119
|
+
const key = normalizeLabel(text.slice(label.from, label.to));
|
|
120
|
+
if (!definitions.has(key)) definitions.set(key, destination(text, url));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return definitions;
|
|
124
|
+
}
|
|
125
|
+
function imageSource(node, text, definitions) {
|
|
126
|
+
const marks = node.getChildren("LinkMark");
|
|
127
|
+
const alt = marks.length >= 2 ? text.slice(marks[0].to, marks[1].from) : "";
|
|
128
|
+
const url = node.getChild("URL");
|
|
129
|
+
if (url) {
|
|
130
|
+
const src = destination(text, url);
|
|
131
|
+
return {
|
|
132
|
+
alt,
|
|
133
|
+
src,
|
|
134
|
+
raw: src
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const label = node.getChild("LinkLabel");
|
|
138
|
+
const labelText = label && label.to - label.from > 2 ? text.slice(label.from, label.to) : `[${alt}]`;
|
|
139
|
+
const src = definitions.get(normalizeLabel(labelText));
|
|
140
|
+
return src === void 0 ? {
|
|
141
|
+
alt,
|
|
142
|
+
src: null,
|
|
143
|
+
raw: labelText
|
|
144
|
+
} : {
|
|
145
|
+
alt,
|
|
146
|
+
src,
|
|
147
|
+
raw: src
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The TeX of a math node (`InlineMath` or `BlockMath`), without delimiters or quote markers;
|
|
152
|
+
* null while it is not closed. `slice` reads the document.
|
|
153
|
+
*/
|
|
154
|
+
function mathSource(node, slice) {
|
|
155
|
+
const marks = node.getChildren("MathMark");
|
|
156
|
+
if (marks.length < 2) return null;
|
|
157
|
+
if (node.name === "InlineMath") return slice(marks[0].to, marks[1].from);
|
|
158
|
+
return node.getChildren("MathText").map((part) => slice(part.from, part.to)).join("\n");
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The previews of a document: the images of each line (one entry per line, in order) and
|
|
162
|
+
* closed, non-empty `$$` blocks. `text` must use one character per line break ("\n"), so
|
|
163
|
+
* offsets match the tree.
|
|
164
|
+
*/
|
|
165
|
+
function previewBlocks(tree, text) {
|
|
166
|
+
const definitions = linkDefinitions(tree, text);
|
|
167
|
+
const lineEnd = (pos) => {
|
|
168
|
+
const end = text.indexOf("\n", pos);
|
|
169
|
+
return end === -1 ? text.length : end;
|
|
170
|
+
};
|
|
171
|
+
const imagesByLine = /* @__PURE__ */ new Map();
|
|
172
|
+
const blocks = [];
|
|
173
|
+
tree.iterate({ enter(node) {
|
|
174
|
+
if (node.name === "Image") {
|
|
175
|
+
const at = lineEnd(node.to);
|
|
176
|
+
let images = imagesByLine.get(at);
|
|
177
|
+
if (!images) {
|
|
178
|
+
imagesByLine.set(at, images = []);
|
|
179
|
+
blocks.push({
|
|
180
|
+
at,
|
|
181
|
+
images
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
images.push(imageSource(node.node, text, definitions));
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (node.name === "BlockMath") {
|
|
188
|
+
const tex = mathSource(node.node, (from, to) => text.slice(from, to));
|
|
189
|
+
if (tex !== null && tex.trim()) blocks.push({
|
|
190
|
+
at: lineEnd(node.to),
|
|
191
|
+
tex
|
|
192
|
+
});
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
} });
|
|
196
|
+
return blocks;
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region packages/model/src/links.ts
|
|
200
|
+
/** Where a URL node is a link's target rather than a bare URL of its own. */
|
|
201
|
+
const LINK_PARENTS = /* @__PURE__ */ new Set([
|
|
202
|
+
"Link",
|
|
203
|
+
"Image",
|
|
204
|
+
"Autolink",
|
|
205
|
+
"LinkReference"
|
|
206
|
+
]);
|
|
207
|
+
function targetOf(node, text, definitions) {
|
|
208
|
+
const url = node.name === "URL" ? node : node.getChild("URL");
|
|
209
|
+
if (url) return destination(text, url);
|
|
210
|
+
const marks = node.getChildren("LinkMark");
|
|
211
|
+
const content = marks.length >= 2 ? text.slice(marks[0].to, marks[1].from) : "";
|
|
212
|
+
const label = node.getChild("LinkLabel");
|
|
213
|
+
const labelText = label && label.to - label.from > 2 ? text.slice(label.from, label.to) : `[${content}]`;
|
|
214
|
+
return definitions().get(normalizeLabel(labelText)) ?? null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The link at `pos`: a link, image, autolink, bare URL, or the URL of a definition
|
|
218
|
+
* (`[label]: url`). `text` must use one character per line break, so offsets match the tree.
|
|
219
|
+
*/
|
|
220
|
+
function linkAt(tree, text, pos) {
|
|
221
|
+
let definitions;
|
|
222
|
+
const lazyDefinitions = () => definitions ??= linkDefinitions(tree, text);
|
|
223
|
+
for (const side of [1, -1]) for (let node = tree.resolveInner(pos, side); node; node = node.parent) {
|
|
224
|
+
if (node.from > pos || node.to < pos) continue;
|
|
225
|
+
if (node.name === "Link" || node.name === "Image" || node.name === "Autolink") return {
|
|
226
|
+
kind: node.name === "Image" ? "image" : "link",
|
|
227
|
+
target: targetOf(node, text, lazyDefinitions),
|
|
228
|
+
from: node.from,
|
|
229
|
+
to: node.to
|
|
230
|
+
};
|
|
231
|
+
if (node.name === "URL" && !LINK_PARENTS.has(node.parent?.name ?? "")) return {
|
|
232
|
+
kind: "link",
|
|
233
|
+
target: destination(text, node),
|
|
234
|
+
from: node.from,
|
|
235
|
+
to: node.to
|
|
236
|
+
};
|
|
237
|
+
if (node.name === "URL" && node.parent?.name === "LinkReference") return {
|
|
238
|
+
kind: "link",
|
|
239
|
+
target: destination(text, node),
|
|
240
|
+
from: node.from,
|
|
241
|
+
to: node.to
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* What a link target is:
|
|
248
|
+
* - `web`: http(s), mailto, or a bare `www.` address (given back as a full URL)
|
|
249
|
+
* - `anchor`: `#heading` in the same document
|
|
250
|
+
* - `file`: a relative or absolute path (an image, another Markdown file, …)
|
|
251
|
+
* - `unsafe`: any other scheme (javascript:, data:, file:, …), which should not be opened
|
|
252
|
+
*/
|
|
253
|
+
function linkKind(target) {
|
|
254
|
+
const url = target.trim();
|
|
255
|
+
if (/^(https?:\/\/|mailto:)/i.test(url)) return {
|
|
256
|
+
kind: "web",
|
|
257
|
+
url
|
|
258
|
+
};
|
|
259
|
+
if (/^www\./i.test(url)) return {
|
|
260
|
+
kind: "web",
|
|
261
|
+
url: `https://${url}`
|
|
262
|
+
};
|
|
263
|
+
if (/^[^\s/:@]+@[^\s/:@]+\.[^\s/:@]+$/.test(url)) return {
|
|
264
|
+
kind: "web",
|
|
265
|
+
url: `mailto:${url}`
|
|
266
|
+
};
|
|
267
|
+
if (url.startsWith("#")) return {
|
|
268
|
+
kind: "anchor",
|
|
269
|
+
url
|
|
270
|
+
};
|
|
271
|
+
if (/^[a-z][a-z0-9+.-]+:/i.test(url)) return {
|
|
272
|
+
kind: "unsafe",
|
|
273
|
+
url
|
|
274
|
+
};
|
|
275
|
+
return {
|
|
276
|
+
kind: "file",
|
|
277
|
+
url
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The anchor of a heading, as GitHub makes it: lower case, punctuation removed, spaces as
|
|
282
|
+
* hyphens ("Next Steps!" → "next-steps", "다음 단계" → "다음-단계").
|
|
283
|
+
*/
|
|
284
|
+
function headingSlug(heading) {
|
|
285
|
+
return heading.trim().toLowerCase().replace(/[^\p{L}\p{N}\p{M}\s_-]/gu, "").replace(/\s/g, "-");
|
|
286
|
+
}
|
|
287
|
+
const HEADINGS = /^(ATXHeading[1-6]|SetextHeading[12])$/;
|
|
288
|
+
/**
|
|
289
|
+
* The headings of a document with their anchors; repeated anchors get -1, -2, … like GitHub.
|
|
290
|
+
* `from` is where each heading starts.
|
|
291
|
+
*/
|
|
292
|
+
function headingAnchors(tree, text) {
|
|
293
|
+
const out = [];
|
|
294
|
+
const seen = /* @__PURE__ */ new Map();
|
|
295
|
+
for (let node = tree.topNode.firstChild; node; node = node.nextSibling) {
|
|
296
|
+
if (!HEADINGS.test(node.name)) continue;
|
|
297
|
+
let title = "";
|
|
298
|
+
let pos = node.from;
|
|
299
|
+
for (const mark of node.getChildren("HeaderMark")) {
|
|
300
|
+
title += text.slice(pos, mark.from);
|
|
301
|
+
pos = mark.to;
|
|
302
|
+
}
|
|
303
|
+
title += text.slice(pos, node.to);
|
|
304
|
+
const base = headingSlug(title.split("\n")[0]);
|
|
305
|
+
const count = seen.get(base) ?? 0;
|
|
306
|
+
seen.set(base, count + 1);
|
|
307
|
+
out.push({
|
|
308
|
+
anchor: count ? `${base}-${count}` : base,
|
|
309
|
+
from: node.from
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
106
315
|
//#region packages/model/src/styles.ts
|
|
107
316
|
/** Styles of whole lines, in precedence order (later wins when two set the same property). */
|
|
108
317
|
const BLOCK_STYLES = [
|
|
@@ -505,4 +714,4 @@ function resolveLook(blocks, inline, theme = defaultTheme) {
|
|
|
505
714
|
return look;
|
|
506
715
|
}
|
|
507
716
|
//#endregion
|
|
508
|
-
export { BLOCK_STYLES, INLINE_STYLES, cellAt, charWidth, defaultTheme, displayWidth, flattenSpans, markdownExtensions, markdownParser, markdownStyles, parseTable, renderTable, resolveLineLook, resolveLook, rowLine, splitRow };
|
|
717
|
+
export { BLOCK_STYLES, INLINE_STYLES, cellAt, charWidth, defaultTheme, displayWidth, flattenSpans, headingAnchors, headingSlug, linkAt, linkKind, markdownExtensions, markdownParser, markdownStyles, mathSource, parseTable, previewBlocks, renderTable, resolveLineLook, resolveLook, rowLine, splitRow };
|