@simple-mark/model 0.2.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.
Files changed (3) hide show
  1. package/index.d.ts +41 -1
  2. package/index.js +118 -1
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -6,6 +6,46 @@ 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 (`![alt](src)`) 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
9
49
  //#region packages/model/src/previews.d.ts
10
50
  /** An image as written in the document. */
11
51
  interface ImageSource {
@@ -142,4 +182,4 @@ export declare function resolveLineLook(blocks: Iterable<BlockStyle>, theme?: Th
142
182
  /** The look of text with these block and inline styles (any order). */
143
183
  export declare function resolveLook(blocks: Iterable<BlockStyle>, inline: Iterable<InlineStyle>, theme?: Theme): Look;
144
184
  //#endregion
145
- export type { BlockRange, BlockStyle, ColumnAlign, ImageSource, InlineStyle, LineLook, Look, MarkdownStyles, PreviewBlock, 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
@@ -104,6 +104,7 @@ const markdownExtensions = [GFM, {
104
104
  const markdownParser = parser.configure(markdownExtensions);
105
105
  //#endregion
106
106
  //#region packages/model/src/previews.ts
107
+ /** A reference label (`[Logo]`) as definitions are looked up: case and spacing ignored. */
107
108
  const normalizeLabel = (label) => label.slice(1, -1).trim().replace(/\s+/g, " ").toLowerCase();
108
109
  /** A link destination as written, without the optional angle brackets (`<a b.png>`). */
109
110
  const destination = (text, url) => text.slice(url.from, url.to).replace(/^<(.*)>$/, "$1");
@@ -195,6 +196,122 @@ function previewBlocks(tree, text) {
195
196
  return blocks;
196
197
  }
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
198
315
  //#region packages/model/src/styles.ts
199
316
  /** Styles of whole lines, in precedence order (later wins when two set the same property). */
200
317
  const BLOCK_STYLES = [
@@ -597,4 +714,4 @@ function resolveLook(blocks, inline, theme = defaultTheme) {
597
714
  return look;
598
715
  }
599
716
  //#endregion
600
- export { BLOCK_STYLES, INLINE_STYLES, cellAt, charWidth, defaultTheme, displayWidth, flattenSpans, markdownExtensions, markdownParser, markdownStyles, mathSource, parseTable, previewBlocks, 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simple-mark/model",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Markdown parsing and styling model shared by every renderer. Pure TypeScript, no DOM.",
5
5
  "license": "MIT",
6
6
  "type": "module",