@osdlc/markdown-link 0.0.0-stage → 0.5.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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * The body link scanner: every link in a body, with its span. A port of the
3
+ * `markdown-link` crate's `body_links_spanned` and `body_links`.
4
+ */
5
+ import { stripCode } from "./code.js";
6
+ import { isVaultLinkUrl, isWhitespace, mdLinkTarget, parseInner, trim } from "./parse.js";
7
+ /**
8
+ * Every wikilink, embed and relative markdown link in a note body, in the
9
+ * order they appear. `bodyLinksSpanned` without the spans, so the two cannot
10
+ * disagree. A markdown link is reported as `'wikilink'`.
11
+ */
12
+ export function bodyLinks(body) {
13
+ return bodyLinksSpanned(body).map((link) => ({
14
+ target: link.target,
15
+ kind: link.kind === 'embed' ? 'embed' : 'wikilink',
16
+ }));
17
+ }
18
+ /**
19
+ * Every wikilink, embed and relative markdown link in a body, in encounter
20
+ * order, with spans.
21
+ *
22
+ * Code is blanked before matching, `[[` never nests, and a markdown link
23
+ * counts only when `isVaultLinkUrl` accepts its URL and it carries no scheme.
24
+ *
25
+ * The scan walks code points. `stripCode` keeps the code point count of its
26
+ * input, not its UTF-16 length, so a code point index into the stripped text
27
+ * is one into the body, and a table converts it to a UTF-16 offset.
28
+ */
29
+ export function bodyLinksSpanned(body) {
30
+ const chars = Array.from(stripCode(body));
31
+ // Code point index -> UTF-16 offset in the body, plus an end sentinel.
32
+ const at = [];
33
+ let offset = 0;
34
+ for (const c of body) {
35
+ at.push(offset);
36
+ offset += c.length;
37
+ }
38
+ at.push(body.length);
39
+ if (chars.length + 1 !== at.length) {
40
+ // `stripCode` must keep the code point count. If it ever does not,
41
+ // reporting no links beats reporting spans that index the wrong text.
42
+ return [];
43
+ }
44
+ const out = [];
45
+ const scan = new UrlScan();
46
+ let i = 0;
47
+ while (i < chars.length) {
48
+ if (chars[i] === '[' && chars[i + 1] === '[') {
49
+ const embed = i > 0 && chars[i - 1] === '!';
50
+ let j = i + 2;
51
+ let inner = '';
52
+ let pipe = -1;
53
+ let closed = false;
54
+ while (j + 1 < chars.length) {
55
+ if (chars[j] === ']' && chars[j + 1] === ']') {
56
+ closed = true;
57
+ break;
58
+ }
59
+ // `[[` never nests, and `[` or `]` inside is a malformed link: stop
60
+ // rather than swallow the rest of the note.
61
+ if (chars[j] === '[' || chars[j] === ']')
62
+ break;
63
+ if (pipe === -1 && chars[j] === '|')
64
+ pipe = j;
65
+ inner += chars[j];
66
+ j++;
67
+ }
68
+ if (closed && inner !== '') {
69
+ const link = parseInner(inner, embed);
70
+ if (link !== null) {
71
+ // Split at the pipe `parseInner` used, found in the stripped chars
72
+ // so a `|` in code is not one, then read the alias from the body so
73
+ // code inside it is kept.
74
+ const alias = pipe === -1 ? null : trim(body.slice(at[pipe + 1], at[j])) || null;
75
+ out.push({
76
+ kind: embed ? 'embed' : 'wikilink',
77
+ raw: inner,
78
+ target: link.target,
79
+ alias,
80
+ start: at[embed ? i - 1 : i],
81
+ end: at[j + 2],
82
+ });
83
+ }
84
+ i = j + 2;
85
+ continue;
86
+ }
87
+ }
88
+ if (chars[i] === '[') {
89
+ const found = markdownLink(chars, i, at, scan);
90
+ if (found !== null) {
91
+ out.push(found.link);
92
+ i = found.next;
93
+ continue;
94
+ }
95
+ }
96
+ i++;
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * `[text](relative/note.md)` starting at `start`, and the code point index
102
+ * just past it, or `null` when this is not one.
103
+ *
104
+ * The shape is the regex `\[[^\]]*\]\(<?([^)>\s]+?)>?\)`, walked by hand so
105
+ * positions index the body. A URL containing whitespace is not a link. The
106
+ * URL is read in place and built only for a link, so a body of failed
107
+ * attempts stays linear.
108
+ */
109
+ function markdownLink(chars, start, at, scan) {
110
+ let j = start + 1;
111
+ while (j < chars.length && chars[j] !== ']' && chars[j] !== '[')
112
+ j++;
113
+ if (chars[j] !== ']' || chars[j + 1] !== '(')
114
+ return null;
115
+ const angled = chars[j + 2] === '<';
116
+ const urlStart = angled ? j + 3 : j + 2;
117
+ const urlEnd = scan.urlEnd(chars, urlStart);
118
+ let end = urlEnd;
119
+ if (angled) {
120
+ if (chars[end] !== '>')
121
+ return null;
122
+ end++;
123
+ }
124
+ if (chars[end] !== ')')
125
+ return null;
126
+ end++;
127
+ // Not a link this dialect harvests. The caller advances one code point, so
128
+ // a match may start at an inner `[`: `[a[b](x.md)` is a link.
129
+ if (!scan.isVaultLink(chars, urlStart, urlEnd) || hasScheme(chars, urlStart, urlEnd)) {
130
+ return null;
131
+ }
132
+ const url = chars.slice(urlStart, urlEnd).join('');
133
+ return {
134
+ link: {
135
+ kind: 'markdown',
136
+ raw: url,
137
+ target: mdLinkTarget(url),
138
+ alias: null,
139
+ start: at[start],
140
+ end: at[end],
141
+ },
142
+ next: end,
143
+ };
144
+ }
145
+ /**
146
+ * What earlier markdown link attempts learned about the URLs they read. Each
147
+ * attempt starts later than the one before, so remembering the last answer of
148
+ * each kind keeps a body of failed attempts linear.
149
+ */
150
+ class UrlScan {
151
+ /** The first `)`, `>` or whitespace at or after `from` is at `stop`. */
152
+ from = -1;
153
+ stop = -1;
154
+ end = -1;
155
+ markFrom = -1;
156
+ /**
157
+ * The last `.` or `/` in `[markFrom, end)`, or below `markFrom` when there
158
+ * is none.
159
+ */
160
+ mark = -1;
161
+ /** `isVaultLinkUrl` of `chars[tailStart, end)`, by `tailStart`. */
162
+ answers = new Map();
163
+ /** Where a URL starting at `from` ends: the first `)`, `>` or whitespace. */
164
+ urlEnd(chars, from) {
165
+ if (from >= this.from && from <= this.stop)
166
+ return this.stop;
167
+ let k = from;
168
+ while (k < chars.length && chars[k] !== ')' && chars[k] !== '>' && !isWhitespace(chars[k])) {
169
+ k++;
170
+ }
171
+ this.from = from;
172
+ this.stop = k;
173
+ return k;
174
+ }
175
+ /**
176
+ * `isVaultLinkUrl` of `chars[from, end)`. That answer depends only on
177
+ * whether the URL ends in `.md` after at least one character and on the
178
+ * extension of its last segment. So the URL from the character before its
179
+ * last `.` gives the same answer, and that tail is shared by every attempt
180
+ * ending at `end`.
181
+ */
182
+ isVaultLink(chars, from, end) {
183
+ if (end !== this.end || from < this.markFrom) {
184
+ let mark = end - 1;
185
+ while (mark >= from && chars[mark] !== '.' && chars[mark] !== '/')
186
+ mark--;
187
+ this.end = end;
188
+ this.markFrom = from;
189
+ this.mark = mark;
190
+ this.answers.clear();
191
+ }
192
+ if (this.mark < from || chars[this.mark] === '/')
193
+ return false;
194
+ const tailStart = Math.max(from, this.mark - 1);
195
+ let answer = this.answers.get(tailStart);
196
+ if (answer === undefined) {
197
+ answer = isVaultLinkUrl(chars.slice(tailStart, end).join(''));
198
+ this.answers.set(tailStart, answer);
199
+ }
200
+ return answer;
201
+ }
202
+ }
203
+ /**
204
+ * Whether `chars[from, end)` starts with a URI scheme (`http:`, `https:`,
205
+ * `mailto:`), which marks the URL external. Read in place, like the rest of
206
+ * the URL: `/^[A-Za-z][A-Za-z0-9+.-]*:/` on a built string would cost the
207
+ * whole URL on every failed attempt.
208
+ */
209
+ function hasScheme(chars, from, end) {
210
+ if (from >= end || !/^[A-Za-z]$/.test(chars[from]))
211
+ return false;
212
+ for (let i = from + 1; i < end; i++) {
213
+ if (chars[i] === ':')
214
+ return true;
215
+ if (!/^[A-Za-z0-9+.-]$/.test(chars[i]))
216
+ return false;
217
+ }
218
+ return false;
219
+ }
package/dist/code.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Code fences and inline code spans: finding them, and blanking them so a
3
+ * lexer never harvests out of them. A port of the `markdown-link` crate's
4
+ * `wikilink::code` module; that module's doc states both rule sets in full.
5
+ *
6
+ * In short, `'commonmark'` (the default) follows CommonMark, and
7
+ * `'skill-export'` keeps the looser rules of `skill-export`'s renderer. Both read one line at a time, so neither knows about container
8
+ * blocks, and indented code blocks are not code.
9
+ *
10
+ * Every marker is ASCII, so a scan over UTF-16 code units never splits a
11
+ * character.
12
+ */
13
+ /** Which rules the lexer follows. */
14
+ export type CodeRules = 'commonmark' | 'skill-export';
15
+ /**
16
+ * Blank out fenced and inline code so links and tags inside it are not
17
+ * harvested. `stripCodeWith` under `'commonmark'`.
18
+ */
19
+ export declare function stripCode(text: string): string;
20
+ /**
21
+ * Blank out fenced and inline code under `rules`.
22
+ *
23
+ * Every character of code, fence marker lines included, becomes a space, and
24
+ * every `\n` stays. The output has the same number of **code points** as the
25
+ * input, as the Rust crate keeps the same number of `char`s. It is not the
26
+ * same UTF-16 length: a character outside the Basic Multilingual Plane in code
27
+ * becomes one space, not two.
28
+ */
29
+ export declare function stripCodeWith(text: string, rules: CodeRules): string;
package/dist/code.js ADDED
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Code fences and inline code spans: finding them, and blanking them so a
3
+ * lexer never harvests out of them. A port of the `markdown-link` crate's
4
+ * `wikilink::code` module; that module's doc states both rule sets in full.
5
+ *
6
+ * In short, `'commonmark'` (the default) follows CommonMark, and
7
+ * `'skill-export'` keeps the looser rules of `skill-export`'s renderer. Both read one line at a time, so neither knows about container
8
+ * blocks, and indented code blocks are not code.
9
+ *
10
+ * Every marker is ASCII, so a scan over UTF-16 code units never splits a
11
+ * character.
12
+ */
13
+ const NEWLINE = 10;
14
+ const CR = 13;
15
+ const TAB = 9;
16
+ const SPACE = 32;
17
+ const BACKTICK = 96;
18
+ const TILDE = 126;
19
+ /**
20
+ * `^ {0,3}(`{3,}|~{3,})` on the line `s[start, end)`: the marker's shape only.
21
+ */
22
+ function fenceRun(s, start, end) {
23
+ const at = start + leadingSpaces(s, start, end, 3);
24
+ if (at >= end)
25
+ return null;
26
+ const char = s.charCodeAt(at);
27
+ if (char !== BACKTICK && char !== TILDE)
28
+ return null;
29
+ let runEnd = at;
30
+ while (runEnd < end && s.charCodeAt(runEnd) === char)
31
+ runEnd++;
32
+ const length = runEnd - at;
33
+ return length >= 3 ? { char, length, end: runEnd } : null;
34
+ }
35
+ function opensFence(s, start, end, rules) {
36
+ const run = fenceRun(s, start, end);
37
+ if (run === null)
38
+ return null;
39
+ const backtickInInfo = run.char === BACKTICK && s.slice(run.end, end).includes('`');
40
+ if (rules === 'commonmark' && backtickInInfo)
41
+ return null;
42
+ return { char: run.char, length: run.length };
43
+ }
44
+ function closesFence(s, start, end, open, rules) {
45
+ const run = fenceRun(s, start, end);
46
+ if (run === null)
47
+ return false;
48
+ return (run.char === open.char &&
49
+ run.length >= open.length &&
50
+ (rules === 'skill-export' || isBlank(s, run.end, end)));
51
+ }
52
+ /** The fence state of a line-by-line scan: which fence, if any, is open. */
53
+ class Fences {
54
+ open = null;
55
+ rules;
56
+ constructor(rules) {
57
+ this.rules = rules;
58
+ }
59
+ /** Classify the line `s[start, end)` and step past it. */
60
+ line(s, start, end) {
61
+ if (this.open === null) {
62
+ this.open = opensFence(s, start, end, this.rules);
63
+ return this.open === null ? 'outside' : 'marker';
64
+ }
65
+ if (closesFence(s, start, end, this.open, this.rules)) {
66
+ this.open = null;
67
+ return 'marker';
68
+ }
69
+ if (this.rules === 'skill-export' && fenceRun(s, start, end) !== null)
70
+ return 'marker';
71
+ return 'inside';
72
+ }
73
+ }
74
+ /**
75
+ * The backtick runs in `start..end`, the rest of a block from where a search
76
+ * found no close.
77
+ */
78
+ class BlockRuns {
79
+ /**
80
+ * `[length, start]` for each run length that occurs, sorted by length:
81
+ * where the last run that would close a span of that length starts.
82
+ */
83
+ lastClose;
84
+ start;
85
+ end;
86
+ rules;
87
+ constructor(s, start, end, rules) {
88
+ this.start = start;
89
+ this.end = end;
90
+ this.rules = rules;
91
+ const last = new Map();
92
+ let at = start;
93
+ while (at < end) {
94
+ if (s.charCodeAt(at) !== BACKTICK) {
95
+ at++;
96
+ continue;
97
+ }
98
+ const run = runLength(s, at, end);
99
+ last.set(run, at);
100
+ at += run;
101
+ }
102
+ this.lastClose = [...last].sort((a, b) => a[0] - b[0]);
103
+ if (rules === 'skill-export') {
104
+ // A longer run closes a shorter span too.
105
+ for (let i = this.lastClose.length - 2; i >= 0; i--) {
106
+ const entry = this.lastClose[i];
107
+ entry[1] = Math.max(entry[1], this.lastClose[i + 1][1]);
108
+ }
109
+ }
110
+ }
111
+ /** Whether a run at or after `from` closes a span opened by `length` backticks. */
112
+ hasClose(length, from) {
113
+ let lo = 0;
114
+ let hi = this.lastClose.length;
115
+ while (lo < hi) {
116
+ const mid = (lo + hi) >> 1;
117
+ if (this.lastClose[mid][0] < length)
118
+ lo = mid + 1;
119
+ else
120
+ hi = mid;
121
+ }
122
+ const entry = this.lastClose[lo];
123
+ if (entry === undefined)
124
+ return false;
125
+ return entry[1] >= from && (entry[0] === length || this.rules === 'skill-export');
126
+ }
127
+ }
128
+ /**
129
+ * Finds where inline code spans close, for a scan that reads its input once,
130
+ * left to right. Call `line` at each line start the scan reaches outside a
131
+ * fence, and `close` at each backtick run it reaches. A whole pass is linear
132
+ * in the size of its input.
133
+ */
134
+ class Spans {
135
+ /** Whether the current line is one a span cannot leave. */
136
+ oneLine = false;
137
+ noClose = null;
138
+ rules;
139
+ s;
140
+ constructor(rules, s) {
141
+ this.rules = rules;
142
+ this.s = s;
143
+ }
144
+ /**
145
+ * Note the line `s[start, end)` the scan has reached. Under `'commonmark'`,
146
+ * a span opened on an ATX heading or a line starting with `|` stays on that
147
+ * line.
148
+ */
149
+ line(start, end) {
150
+ this.oneLine = this.rules === 'commonmark' && isOneLineBlock(this.s, start, end);
151
+ }
152
+ /**
153
+ * The backtick run that starts at `start`: its length, and where the span it
154
+ * opens closes (the start of the closing run), or `null` if it does not.
155
+ */
156
+ close(start) {
157
+ const length = runLength(this.s, start, this.s.length);
158
+ const from = start + length;
159
+ const runs = this.noClose;
160
+ if (runs !== null && runs.start <= from && from < runs.end && !runs.hasClose(length, from)) {
161
+ return [length, null];
162
+ }
163
+ const found = this.search(length, from);
164
+ if ('close' in found)
165
+ return [length, found.close];
166
+ this.noClose = new BlockRuns(this.s, from, found.blockEnd, this.rules);
167
+ return [length, null];
168
+ }
169
+ /** The close, or the end of the block when there is none. */
170
+ search(length, from) {
171
+ const s = this.s;
172
+ let at = from;
173
+ for (;;) {
174
+ while (at < s.length && s.charCodeAt(at) !== NEWLINE) {
175
+ if (s.charCodeAt(at) !== BACKTICK) {
176
+ at++;
177
+ continue;
178
+ }
179
+ const run = runLength(s, at, s.length);
180
+ if (closes(run, length, this.rules))
181
+ return { close: at };
182
+ at += run;
183
+ }
184
+ const next = at + 1;
185
+ if (this.oneLine || next >= s.length)
186
+ return { blockEnd: Math.min(next, s.length) };
187
+ if (endsSpanBlock(s, next, lineEnd(s, next), this.rules))
188
+ return { blockEnd: next };
189
+ at = next;
190
+ }
191
+ }
192
+ }
193
+ function closes(run, length, rules) {
194
+ return rules === 'commonmark' ? run === length : run >= length;
195
+ }
196
+ function runLength(s, at, end) {
197
+ let i = at;
198
+ while (i < end && s.charCodeAt(i) === BACKTICK)
199
+ i++;
200
+ return i - at;
201
+ }
202
+ /** Whether the line `s[start, end)` starts a block that a span cannot reach into. */
203
+ function endsSpanBlock(s, start, end, rules) {
204
+ const stop = end > start && s.charCodeAt(end - 1) === CR ? end - 1 : end;
205
+ if (isBlank(s, start, stop))
206
+ return true;
207
+ if (rules === 'skill-export')
208
+ return fenceRun(s, start, stop) !== null;
209
+ return opensFence(s, start, stop, rules) !== null || startsBlock(s, start, stop);
210
+ }
211
+ /** An ATX heading or a line starting with `|`: a block that is one line long. */
212
+ function isOneLineBlock(s, start, end) {
213
+ const at = start + leadingSpaces(s, start, end, 3);
214
+ return atxHeading(s, at, end) || (at < end && s[at] === '|');
215
+ }
216
+ /** `#{1,6}` then a space, a tab or the end of the line, from `at`. */
217
+ function atxHeading(s, at, end) {
218
+ let i = at;
219
+ while (i < end && s[i] === '#')
220
+ i++;
221
+ const hashes = i - at;
222
+ if (hashes < 1 || hashes > 6)
223
+ return false;
224
+ if (i >= end)
225
+ return true;
226
+ const after = s.charCodeAt(i);
227
+ return after === SPACE || after === TAB || after === CR;
228
+ }
229
+ /**
230
+ * An ATX heading, a line starting with `|`, a setext underline, a thematic
231
+ * break, a block quote or a list item.
232
+ */
233
+ function startsBlock(s, start, end) {
234
+ const indented = start + leadingSpaces(s, start, end, 3);
235
+ let rest = start;
236
+ while (rest < end && (s.charCodeAt(rest) === SPACE || s.charCodeAt(rest) === TAB))
237
+ rest++;
238
+ const endsMarker = (at) => at >= end || s.charCodeAt(at) === SPACE || s.charCodeAt(at) === TAB;
239
+ let equals = indented;
240
+ while (equals < end && s[equals] === '=')
241
+ equals++;
242
+ const setext = equals > indented && isBlank(s, equals, end);
243
+ const quote = rest < end && s[rest] === '>';
244
+ const bullet = rest < end && '-*+'.includes(s[rest]) && endsMarker(rest + 1);
245
+ let digitsEnd = rest;
246
+ while (digitsEnd < end && isAsciiDigit(s.charCodeAt(digitsEnd)))
247
+ digitsEnd++;
248
+ const digits = digitsEnd - rest;
249
+ const ordered = digits >= 1 &&
250
+ digits <= 9 &&
251
+ digitsEnd < end &&
252
+ (s[digitsEnd] === '.' || s[digitsEnd] === ')') &&
253
+ endsMarker(digitsEnd + 1);
254
+ return (isOneLineBlock(s, start, end) ||
255
+ setext ||
256
+ quote ||
257
+ bullet ||
258
+ ordered ||
259
+ thematicBreak(s, indented, end));
260
+ }
261
+ /** Three or more of one of `-`, `*` or `_`, with only spaces or tabs between. */
262
+ function thematicBreak(s, start, end) {
263
+ if (start >= end)
264
+ return false;
265
+ const mark = s[start];
266
+ if (mark !== '-' && mark !== '*' && mark !== '_')
267
+ return false;
268
+ let count = 0;
269
+ for (let i = start; i < end; i++) {
270
+ const c = s[i];
271
+ if (c === mark)
272
+ count++;
273
+ else if (c !== ' ' && c !== '\t')
274
+ return false;
275
+ }
276
+ return count >= 3;
277
+ }
278
+ function isAsciiDigit(code) {
279
+ return code >= 48 && code <= 57;
280
+ }
281
+ function leadingSpaces(s, start, end, max) {
282
+ let n = 0;
283
+ while (n < max && start + n < end && s.charCodeAt(start + n) === SPACE)
284
+ n++;
285
+ return n;
286
+ }
287
+ function isBlank(s, start, end) {
288
+ for (let i = start; i < end; i++) {
289
+ const c = s.charCodeAt(i);
290
+ if (c !== SPACE && c !== TAB && c !== CR)
291
+ return false;
292
+ }
293
+ return true;
294
+ }
295
+ /** The index of the `\n` ending the line that holds `from`, or the end of input. */
296
+ function lineEnd(s, from) {
297
+ const at = s.indexOf('\n', from);
298
+ return at === -1 ? s.length : at;
299
+ }
300
+ /**
301
+ * Blank out fenced and inline code so links and tags inside it are not
302
+ * harvested. `stripCodeWith` under `'commonmark'`.
303
+ */
304
+ export function stripCode(text) {
305
+ return stripCodeWith(text, 'commonmark');
306
+ }
307
+ /**
308
+ * Blank out fenced and inline code under `rules`.
309
+ *
310
+ * Every character of code, fence marker lines included, becomes a space, and
311
+ * every `\n` stays. The output has the same number of **code points** as the
312
+ * input, as the Rust crate keeps the same number of `char`s. It is not the
313
+ * same UTF-16 length: a character outside the Basic Multilingual Plane in code
314
+ * becomes one space, not two.
315
+ */
316
+ export function stripCodeWith(text, rules) {
317
+ const out = [];
318
+ const fences = new Fences(rules);
319
+ const spans = new Spans(rules, text);
320
+ let writtenUpTo = 0;
321
+ const blank = (start, end) => {
322
+ out.push(text.slice(writtenUpTo, start));
323
+ for (const c of text.slice(start, end))
324
+ out.push(c === '\n' ? '\n' : ' ');
325
+ writtenUpTo = end;
326
+ };
327
+ let offset = 0;
328
+ while (offset < text.length) {
329
+ if (offset === 0 || text.charCodeAt(offset - 1) === NEWLINE) {
330
+ const end = lineEnd(text, offset);
331
+ if (fences.line(text, offset, end) !== 'outside') {
332
+ blank(offset, end);
333
+ offset = end + 1;
334
+ continue;
335
+ }
336
+ spans.line(offset, end);
337
+ }
338
+ if (text.charCodeAt(offset) === BACKTICK) {
339
+ const [run, close] = spans.close(offset);
340
+ if (close === null) {
341
+ offset += run;
342
+ }
343
+ else {
344
+ blank(offset, close + run);
345
+ offset = close + run;
346
+ }
347
+ continue;
348
+ }
349
+ offset++;
350
+ }
351
+ out.push(text.slice(writtenUpTo));
352
+ return out.join('');
353
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What a reader sees for a link, and typed references. A port of the
3
+ * `markdown-link` crate's `display_text` and `parse_typed_ref`.
4
+ */
5
+ /**
6
+ * Replace each wikilink and embed with what a reader sees for it, for
7
+ * snippets and search: its alias, or else its target. An embed's trailing
8
+ * `|300` or `|300x200` is a size, not an alias, so
9
+ * `![[img.png|300]]` shows `img.png`. An empty alias shows the target. Code
10
+ * is not skipped.
11
+ *
12
+ * Every delimiter is ASCII, so the scan can step UTF-16 units: a surrogate
13
+ * never matches one.
14
+ */
15
+ export declare function displayText(text: string): string;
16
+ /**
17
+ * A typed reference, `[[type/slug]]` or `[[type/slug|Alias]]`: a frontmatter
18
+ * value naming one note of a known type, such as `location: "[[location/home]]"`.
19
+ */
20
+ export interface TypedRef {
21
+ /** The text before the first `/`, such as `location`. */
22
+ kind: string;
23
+ /** The rest of the target, such as `home` or `2026/spring`. */
24
+ slug: string;
25
+ /** The text after the first `|`, as written. */
26
+ alias: string | null;
27
+ }
28
+ /**
29
+ * Parse a whole value as a typed reference, or `null`.
30
+ *
31
+ * Shape only: the target is not resolved. The value is exactly `[[...]]`
32
+ * with no `[`, `]` or line break inside. The target ends at the first `|`,
33
+ * minus a `\` that escapes it, as in `parseInner`. Its type and each
34
+ * `/`-separated slug segment are non-empty and hold no whitespace.
35
+ */
36
+ export declare function parseTypedRef(value: string): TypedRef | null;