@osdlc/markdown-link 0.5.0 → 0.8.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/CHANGELOG.md +17 -0
- package/README.md +21 -4
- package/dist/body-links.d.ts +67 -1
- package/dist/body-links.js +120 -8
- package/dist/display.js +2 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/parse.d.ts +35 -0
- package/dist/parse.js +62 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @osdlc/markdown-link
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 96a1bd2: New: `scanBodyWith(body, options)` returns `{ links, abandoned }`. `links` is what `bodyLinksSpannedWith` returns. `abandoned` lists each `[[` the scan found outside code and did not read as a link, as an `AbandonedLink` with `reason`, `embed`, `start` and `end`, so a caller can flag link syntax left in the text. The reasons (`AbandonReason`) are `'bracket'` (a `[` or `]` before the closing `]]`), `'code'` (the target is only code), `'empty-target'` (`[[]]`, `[[ |alias]]`, or `[[#Heading]]` without `selfLinks`) and `'unclosed'`. A run of `[` with no link after it, such as `[[[TODO`, is one entry: from the first `[[`, with the reason and end of the last. Matches the Rust crate's `scan_body_with`. This release is 0.8.0: `package.json` was set to 0.7.0 by hand so the package joins the Rust crate on 0.8 and skips 0.7.
|
|
8
|
+
- 3623b05: **Breaking:** a `#^id` fragment is now a block, not a heading. `parseInner('Note#^abc', false)` gives `heading: null` and `block: 'abc'`, where it gave `heading: '^abc'`. `WikiLink` and `BodyLink` gain `block` and `size` (an embed's `|300` or `|300x200`, as `{ width, height }`; the alias still holds it as written), and `BodyLink` gains `heading`. New: `bodyLinksSpannedWith(body, options)` with `ScanOptions` (`codeRules`, `skipHtmlComments`, `selfLinks`, all off or CommonMark by default, so `bodyLinksSpanned` is unchanged), `parseInnerSelf` (reads `#Heading` as a link to the note it is in, with an empty target), `isSizeSpec` and `parseSize`. Released together with `scanBodyWith` as 0.8.0, which matches the Rust crate's 0.8.0; there is no 0.7.0 of this package.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 96a1bd2: Fixed: `percentDecode` takes an escape only when both characters after `%` are hex digits, so `%+5` stays as written instead of decoding. It had copied a quirk of Rust's `u8::from_str_radix`, which accepts a sign; the Rust crate's `percent_decode` is fixed the same way.
|
|
13
|
+
|
|
14
|
+
## 0.6.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 743c7bf: Export `percentDecode`, the decoding `mdLinkTarget` already uses, as the Rust crate exports `percent_decode`. Each valid `%XX` decodes on its own, a `%` that is not valid hex stays as written, and a result that is not UTF-8 leaves the whole string as written.
|
|
19
|
+
|
|
3
20
|
## 0.5.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -60,7 +60,8 @@ npm install @osdlc/markdown-link
|
|
|
60
60
|
import { bodyLinksSpanned } from '@osdlc/markdown-link'
|
|
61
61
|
|
|
62
62
|
bodyLinksSpanned('See [[Note#h|alias]] and `[[not a link]]`')
|
|
63
|
-
// [{ kind: 'wikilink', raw: 'Note#h|alias', target: 'Note',
|
|
63
|
+
// [{ kind: 'wikilink', raw: 'Note#h|alias', target: 'Note', heading: 'h', block: null,
|
|
64
|
+
// alias: 'alias', size: null, start: 4, end: 20 }]
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Exports
|
|
@@ -69,12 +70,26 @@ bodyLinksSpanned('See [[Note#h|alias]] and `[[not a link]]`')
|
|
|
69
70
|
`\n` and the code point count, so links in code are not harvested.
|
|
70
71
|
`stripCodeWith(text, rules)` takes `'commonmark'` (the default) or
|
|
71
72
|
`'skill-export'`.
|
|
72
|
-
- One link: `parseInner` splits a wikilink body into target, heading
|
|
73
|
-
alias; `targetRange` is where the target
|
|
73
|
+
- One link: `parseInner` splits a wikilink body into target, heading, block
|
|
74
|
+
(`#^id`), alias and, for an embed, size; `targetRange` is where the target
|
|
75
|
+
sits in it. `parseInnerSelf` also reads a body with no target and a `#`
|
|
76
|
+
fragment, `#Heading`, as a link to the note it is in, with an empty target.
|
|
77
|
+
`isSizeSpec` and `parseSize` read `300` or `300x200`.
|
|
74
78
|
- Markdown link URLs: `isVaultLinkUrl` says whether a URL is a note or an
|
|
75
79
|
attachment; `mdLinkTarget` percent-decodes it and drops `.md`.
|
|
80
|
+
`percentDecode` is that decoding alone: `percentDecode('My%20Note')` is
|
|
81
|
+
`'My Note'`, and a `%` that is not valid hex or UTF-8 stays as written.
|
|
76
82
|
- A body: `bodyLinksSpanned` finds every wikilink, embed and relative markdown
|
|
77
83
|
link, with its span; `bodyLinks` is the same scan without spans.
|
|
84
|
+
`bodyLinksSpannedWith(body, options)` takes `ScanOptions`: `codeRules`,
|
|
85
|
+
`skipHtmlComments` (links in `<!-- -->` are not links) and `selfLinks`
|
|
86
|
+
(`[[#Heading]]` is a link with an empty target). The defaults are
|
|
87
|
+
`bodyLinksSpanned`'s. `scanBodyWith(body, options)` returns those links
|
|
88
|
+
and `abandoned`, each `[[` the scan gave up on as an `AbandonedLink`
|
|
89
|
+
(`reason`, `embed`, `start`, `end`), so a caller can flag link syntax left
|
|
90
|
+
in the text. The reasons (`AbandonReason`) are `'bracket'` (a `[` or `]`
|
|
91
|
+
before `]]`), `'code'` (the target is only code), `'empty-target'` and
|
|
92
|
+
`'unclosed'`. Its result is a `BodyScan`.
|
|
78
93
|
- Display text: `displayText` replaces each wikilink and embed with what a
|
|
79
94
|
reader sees, its alias or else its target. An embed's `|300` or `|300x200`
|
|
80
95
|
is a size, not text, so `![[img.png|300]]` shows `img.png`.
|
|
@@ -88,7 +103,9 @@ The Rust crate reports byte offsets.
|
|
|
88
103
|
## Twin
|
|
89
104
|
|
|
90
105
|
The Rust crate `markdown-link` has the parsing functions above under their
|
|
91
|
-
snake_case names. It also resolves links (`LinkIndex
|
|
106
|
+
snake_case names. It also resolves links (`LinkIndex`, with `candidates` and
|
|
107
|
+
`resolve_strict`), writes relative links (`relative_link`,
|
|
108
|
+
`encode_uri_component`, `encode_uri_fragment`, `escape_label`), reads links in
|
|
92
109
|
frontmatter and has smaller helpers, which this package does not have yet.
|
|
93
110
|
Both suites read one case file,
|
|
94
111
|
`packages/rust/markdown-link/tests/fixtures/conformance.json` in the
|
package/dist/body-links.d.ts
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* The body link scanner: every link in a body, with its span. A port of the
|
|
3
3
|
* `markdown-link` crate's `body_links_spanned` and `body_links`.
|
|
4
4
|
*/
|
|
5
|
+
import { type CodeRules } from './code.ts';
|
|
6
|
+
import { type Size } from './parse.ts';
|
|
7
|
+
/** What a body scan counts as a link. Every field is optional; the defaults are what `bodyLinksSpanned` uses. */
|
|
8
|
+
export interface ScanOptions {
|
|
9
|
+
/** Which rules decide what is code. Default `'commonmark'`. */
|
|
10
|
+
codeRules?: CodeRules;
|
|
11
|
+
/** Skip links inside HTML comments, `<!--` to `-->` or the end. Default `false`. */
|
|
12
|
+
skipHtmlComments?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Read `[[#Heading]]` or `[[#^block]]` as a link to the note it is written
|
|
15
|
+
* in, with an empty `target`. Default `false`: such a link is dropped.
|
|
16
|
+
*/
|
|
17
|
+
selfLinks?: boolean;
|
|
18
|
+
}
|
|
5
19
|
/** What syntax produced a `BodyLink`. */
|
|
6
20
|
export type BodyLinkKind = 'wikilink' | 'embed' | 'markdown';
|
|
7
21
|
/** One outgoing reference found in a body, with its span, before resolution. */
|
|
@@ -15,9 +29,16 @@ export interface BodyLink {
|
|
|
15
29
|
/**
|
|
16
30
|
* What resolution runs against. A wikilink's `raw` minus `#heading` /
|
|
17
31
|
* `^block` / `|alias` and the `\` escaping the pipe; a markdown link's URL
|
|
18
|
-
* percent-decoded and minus a trailing `.md`.
|
|
32
|
+
* percent-decoded and minus a trailing `.md`. Empty for a link to the note
|
|
33
|
+
* it is written in (see `ScanOptions.selfLinks`).
|
|
19
34
|
*/
|
|
20
35
|
target: string;
|
|
36
|
+
/** A wikilink's `#heading`, as `WikiLink.heading`. `null` for a markdown link. */
|
|
37
|
+
heading: string | null;
|
|
38
|
+
/** A wikilink's `#^block`, as `WikiLink.block`. `null` for a markdown link. */
|
|
39
|
+
block: string | null;
|
|
40
|
+
/** An embed's size, as `WikiLink.size`. */
|
|
41
|
+
size: Size | null;
|
|
21
42
|
/**
|
|
22
43
|
* A wikilink's or embed's `|alias`: the text after the first `|` outside
|
|
23
44
|
* code, read from the body so code inside it is kept. `null` when there is
|
|
@@ -32,6 +53,40 @@ export interface BodyLink {
|
|
|
32
53
|
start: number;
|
|
33
54
|
end: number;
|
|
34
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Why the scanner gave up on a `[[`. `'bracket'`: a `[` or `]` came before
|
|
58
|
+
* the closing `]]`. `'code'`: closed, but the target is empty once the code
|
|
59
|
+
* inside it is blanked. `'empty-target'`: closed with an empty target (`[[]]`,
|
|
60
|
+
* `[[ |alias]]`, or `[[#Heading]]` when `selfLinks` is off). `'unclosed'`: no
|
|
61
|
+
* `]]` before the end of the body.
|
|
62
|
+
*/
|
|
63
|
+
export type AbandonReason = 'bracket' | 'code' | 'empty-target' | 'unclosed';
|
|
64
|
+
/**
|
|
65
|
+
* A `[[` the scanner found outside code and did not read as a link: what a
|
|
66
|
+
* renderer would otherwise leave in its output as literal brackets. A `[[`
|
|
67
|
+
* inside code, or inside an HTML comment when `skipHtmlComments` is on, is not
|
|
68
|
+
* reported, and nor is one that a link found after it starts inside:
|
|
69
|
+
* `[[[Note]]` is a literal `[` and a link. A run of `[` with no link after it,
|
|
70
|
+
* such as `[[[TODO`, is one entry: from the first `[[`, with the reason and
|
|
71
|
+
* end of the last.
|
|
72
|
+
*/
|
|
73
|
+
export interface AbandonedLink {
|
|
74
|
+
reason: AbandonReason;
|
|
75
|
+
/** Whether a `!` came before the `[[`. */
|
|
76
|
+
embed: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* UTF-16 offsets of what the scanner read, from the `[[` (or the `!` before
|
|
79
|
+
* it): past the `]]` for `'code'` and `'empty-target'`, up to the `[` or `]`
|
|
80
|
+
* it stopped at for `'bracket'`, and the end of the body for `'unclosed'`.
|
|
81
|
+
*/
|
|
82
|
+
start: number;
|
|
83
|
+
end: number;
|
|
84
|
+
}
|
|
85
|
+
/** What `scanBodyWith` found: the links, and the `[[` it gave up on. */
|
|
86
|
+
export interface BodyScan {
|
|
87
|
+
links: BodyLink[];
|
|
88
|
+
abandoned: AbandonedLink[];
|
|
89
|
+
}
|
|
35
90
|
/** What kind of reference produced a `RawLink`. */
|
|
36
91
|
export type LinkKind = 'wikilink' | 'embed';
|
|
37
92
|
/** A body reference without its span. */
|
|
@@ -62,3 +117,14 @@ export declare function bodyLinks(body: string): RawLink[];
|
|
|
62
117
|
* is one into the body, and a table converts it to a UTF-16 offset.
|
|
63
118
|
*/
|
|
64
119
|
export declare function bodyLinksSpanned(body: string): BodyLink[];
|
|
120
|
+
/**
|
|
121
|
+
* `bodyLinksSpanned` under `options`: which code rules apply, whether HTML
|
|
122
|
+
* comments hide links, and whether `[[#Heading]]` is a link. A comment is
|
|
123
|
+
* found after code is blanked, so a `<!--` inside code does not open one.
|
|
124
|
+
*/
|
|
125
|
+
export declare function bodyLinksSpannedWith(body: string, options: ScanOptions): BodyLink[];
|
|
126
|
+
/**
|
|
127
|
+
* `bodyLinksSpannedWith`, and each `[[` it gave up on: one holding a `[` or
|
|
128
|
+
* `]`, one whose target is empty or only code, and one never closed.
|
|
129
|
+
*/
|
|
130
|
+
export declare function scanBodyWith(body: string, options: ScanOptions): BodyScan;
|
package/dist/body-links.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The body link scanner: every link in a body, with its span. A port of the
|
|
3
3
|
* `markdown-link` crate's `body_links_spanned` and `body_links`.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { isVaultLinkUrl, isWhitespace, mdLinkTarget, parseInner, trim } from "./parse.js";
|
|
5
|
+
import { stripCodeWith } from "./code.js";
|
|
6
|
+
import { isVaultLinkUrl, isWhitespace, mdLinkTarget, parseInner, parseInnerSelf, trim, } from "./parse.js";
|
|
7
7
|
/**
|
|
8
8
|
* Every wikilink, embed and relative markdown link in a note body, in the
|
|
9
9
|
* order they appear. `bodyLinksSpanned` without the spans, so the two cannot
|
|
@@ -27,7 +27,27 @@ export function bodyLinks(body) {
|
|
|
27
27
|
* is one into the body, and a table converts it to a UTF-16 offset.
|
|
28
28
|
*/
|
|
29
29
|
export function bodyLinksSpanned(body) {
|
|
30
|
-
|
|
30
|
+
return bodyLinksSpannedWith(body, {});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `bodyLinksSpanned` under `options`: which code rules apply, whether HTML
|
|
34
|
+
* comments hide links, and whether `[[#Heading]]` is a link. A comment is
|
|
35
|
+
* found after code is blanked, so a `<!--` inside code does not open one.
|
|
36
|
+
*/
|
|
37
|
+
export function bodyLinksSpannedWith(body, options) {
|
|
38
|
+
return scanBodyWith(body, options).links;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `bodyLinksSpannedWith`, and each `[[` it gave up on: one holding a `[` or
|
|
42
|
+
* `]`, one whose target is empty or only code, and one never closed.
|
|
43
|
+
*/
|
|
44
|
+
export function scanBodyWith(body, options) {
|
|
45
|
+
const codeBlanked = stripCodeWith(body, options.codeRules ?? 'commonmark');
|
|
46
|
+
const skipComments = options.skipHtmlComments === true;
|
|
47
|
+
const chars = Array.from(skipComments ? blankComments(codeBlanked) : codeBlanked);
|
|
48
|
+
// With comments blanked too, the code points with only code blanked, made
|
|
49
|
+
// the first time an abandoned link needs them.
|
|
50
|
+
let codeChars = null;
|
|
31
51
|
// Code point index -> UTF-16 offset in the body, plus an end sentinel.
|
|
32
52
|
const at = [];
|
|
33
53
|
let offset = 0;
|
|
@@ -39,18 +59,21 @@ export function bodyLinksSpanned(body) {
|
|
|
39
59
|
if (chars.length + 1 !== at.length) {
|
|
40
60
|
// `stripCode` must keep the code point count. If it ever does not,
|
|
41
61
|
// reporting no links beats reporting spans that index the wrong text.
|
|
42
|
-
return [];
|
|
62
|
+
return { links: [], abandoned: [] };
|
|
43
63
|
}
|
|
44
64
|
const out = [];
|
|
65
|
+
const abandoned = [];
|
|
45
66
|
const scan = new UrlScan();
|
|
46
67
|
let i = 0;
|
|
47
68
|
while (i < chars.length) {
|
|
48
69
|
if (chars[i] === '[' && chars[i + 1] === '[') {
|
|
49
70
|
const embed = i > 0 && chars[i - 1] === '!';
|
|
71
|
+
const start = at[embed ? i - 1 : i];
|
|
50
72
|
let j = i + 2;
|
|
51
73
|
let inner = '';
|
|
52
74
|
let pipe = -1;
|
|
53
75
|
let closed = false;
|
|
76
|
+
let bracket = false;
|
|
54
77
|
while (j + 1 < chars.length) {
|
|
55
78
|
if (chars[j] === ']' && chars[j + 1] === ']') {
|
|
56
79
|
closed = true;
|
|
@@ -58,26 +81,56 @@ export function bodyLinksSpanned(body) {
|
|
|
58
81
|
}
|
|
59
82
|
// `[[` never nests, and `[` or `]` inside is a malformed link: stop
|
|
60
83
|
// rather than swallow the rest of the note.
|
|
61
|
-
if (chars[j] === '[' || chars[j] === ']')
|
|
84
|
+
if (chars[j] === '[' || chars[j] === ']') {
|
|
85
|
+
bracket = true;
|
|
62
86
|
break;
|
|
87
|
+
}
|
|
63
88
|
if (pipe === -1 && chars[j] === '|')
|
|
64
89
|
pipe = j;
|
|
65
90
|
inner += chars[j];
|
|
66
91
|
j++;
|
|
67
92
|
}
|
|
68
|
-
if (closed
|
|
69
|
-
|
|
93
|
+
if (!closed) {
|
|
94
|
+
abandoned.push({
|
|
95
|
+
reason: bracket ? 'bracket' : 'unclosed',
|
|
96
|
+
embed,
|
|
97
|
+
start,
|
|
98
|
+
end: bracket ? at[j] : body.length,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (closed) {
|
|
102
|
+
let link = null;
|
|
103
|
+
if (inner !== '') {
|
|
104
|
+
link =
|
|
105
|
+
options.selfLinks === true ? parseInnerSelf(inner, embed) : parseInner(inner, embed);
|
|
106
|
+
}
|
|
107
|
+
if (link === null) {
|
|
108
|
+
// Code inside was blanked to spaces, so the body there differs from
|
|
109
|
+
// the code-blanked text. A blanked comment is not code.
|
|
110
|
+
const code = skipComments ? (codeChars ??= Array.from(codeBlanked)) : chars;
|
|
111
|
+
const heldCode = body.slice(at[i + 2], at[j]) !== code.slice(i + 2, j).join('');
|
|
112
|
+
abandoned.push({
|
|
113
|
+
reason: heldCode ? 'code' : 'empty-target',
|
|
114
|
+
embed,
|
|
115
|
+
start,
|
|
116
|
+
end: at[j + 2],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
70
119
|
if (link !== null) {
|
|
71
120
|
// Split at the pipe `parseInner` used, found in the stripped chars
|
|
72
121
|
// so a `|` in code is not one, then read the alias from the body so
|
|
73
122
|
// code inside it is kept.
|
|
74
123
|
const alias = pipe === -1 ? null : trim(body.slice(at[pipe + 1], at[j])) || null;
|
|
124
|
+
dropOverlapped(abandoned, at[i]);
|
|
75
125
|
out.push({
|
|
76
126
|
kind: embed ? 'embed' : 'wikilink',
|
|
77
127
|
raw: inner,
|
|
78
128
|
target: link.target,
|
|
129
|
+
heading: link.heading,
|
|
130
|
+
block: link.block,
|
|
79
131
|
alias,
|
|
80
|
-
|
|
132
|
+
size: link.size,
|
|
133
|
+
start,
|
|
81
134
|
end: at[j + 2],
|
|
82
135
|
});
|
|
83
136
|
}
|
|
@@ -88,6 +141,7 @@ export function bodyLinksSpanned(body) {
|
|
|
88
141
|
if (chars[i] === '[') {
|
|
89
142
|
const found = markdownLink(chars, i, at, scan);
|
|
90
143
|
if (found !== null) {
|
|
144
|
+
dropOverlapped(abandoned, at[i]);
|
|
91
145
|
out.push(found.link);
|
|
92
146
|
i = found.next;
|
|
93
147
|
continue;
|
|
@@ -95,8 +149,63 @@ export function bodyLinksSpanned(body) {
|
|
|
95
149
|
}
|
|
96
150
|
i++;
|
|
97
151
|
}
|
|
152
|
+
return { links: out, abandoned: mergeRuns(abandoned) };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Folds each abandoned `[[` that starts inside the one before it into that
|
|
156
|
+
* one, so a run of `[` such as `[[[TODO` is one entry: from the first `[[`,
|
|
157
|
+
* with the reason and end of the last. As in `dropOverlapped`, an embed's `!`
|
|
158
|
+
* does not count: in `[[x![[y` the embed is its own entry.
|
|
159
|
+
*/
|
|
160
|
+
function mergeRuns(abandoned) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const next of abandoned) {
|
|
163
|
+
const run = out[out.length - 1];
|
|
164
|
+
if (run !== undefined && next.start + (next.embed ? 1 : 0) < run.end) {
|
|
165
|
+
run.reason = next.reason;
|
|
166
|
+
run.end = next.end;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
out.push(next);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
98
172
|
return out;
|
|
99
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Drops each abandoned `[[` whose second `[` opens a link: `bracket` is the
|
|
176
|
+
* offset of the link's first `[`, after any `!`. After a `[` or `]` the
|
|
177
|
+
* scanner moves on one code point, so `[[[Note]]` is a literal `[` and a link,
|
|
178
|
+
* not a `[[` given up on. An embed's `!` right after an abandoned `[[`, as in
|
|
179
|
+
* `[[x![[B]]`, does not count.
|
|
180
|
+
*/
|
|
181
|
+
function dropOverlapped(abandoned, bracket) {
|
|
182
|
+
for (;;) {
|
|
183
|
+
const last = abandoned[abandoned.length - 1];
|
|
184
|
+
if (last === undefined || last.start + (last.embed ? 1 : 0) + 2 <= bracket)
|
|
185
|
+
return;
|
|
186
|
+
abandoned.pop();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* `text` with every HTML comment, `<!--` to `-->` or the end, blanked to
|
|
191
|
+
* spaces, newlines kept. The code point count does not change.
|
|
192
|
+
*/
|
|
193
|
+
function blankComments(text) {
|
|
194
|
+
let out = '';
|
|
195
|
+
let rest = text;
|
|
196
|
+
for (;;) {
|
|
197
|
+
const open = rest.indexOf('<!--');
|
|
198
|
+
if (open === -1)
|
|
199
|
+
break;
|
|
200
|
+
out += rest.slice(0, open);
|
|
201
|
+
const close = rest.indexOf('-->', open + 4);
|
|
202
|
+
const end = close === -1 ? rest.length : close + 3;
|
|
203
|
+
for (const c of rest.slice(open, end))
|
|
204
|
+
out += c === '\n' ? '\n' : ' ';
|
|
205
|
+
rest = rest.slice(end);
|
|
206
|
+
}
|
|
207
|
+
return out + rest;
|
|
208
|
+
}
|
|
100
209
|
/**
|
|
101
210
|
* `[text](relative/note.md)` starting at `start`, and the code point index
|
|
102
211
|
* just past it, or `null` when this is not one.
|
|
@@ -135,7 +244,10 @@ function markdownLink(chars, start, at, scan) {
|
|
|
135
244
|
kind: 'markdown',
|
|
136
245
|
raw: url,
|
|
137
246
|
target: mdLinkTarget(url),
|
|
247
|
+
heading: null,
|
|
248
|
+
block: null,
|
|
138
249
|
alias: null,
|
|
250
|
+
size: null,
|
|
139
251
|
start: at[start],
|
|
140
252
|
end: at[end],
|
|
141
253
|
},
|
package/dist/display.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* What a reader sees for a link, and typed references. A port of the
|
|
3
3
|
* `markdown-link` crate's `display_text` and `parse_typed_ref`.
|
|
4
4
|
*/
|
|
5
|
-
import { isWhitespace, parseInner, splitAlias, trim } from "./parse.js";
|
|
5
|
+
import { isSizeSpec, isWhitespace, parseInner, splitAlias, trim } from "./parse.js";
|
|
6
6
|
/**
|
|
7
7
|
* Replace each wikilink and embed with what a reader sees for it, for
|
|
8
8
|
* snippets and search: its alias, or else its target. An embed's trailing
|
|
@@ -50,15 +50,11 @@ function shown(link) {
|
|
|
50
50
|
if (alias !== null && link.embed) {
|
|
51
51
|
const pipe = alias.lastIndexOf('|');
|
|
52
52
|
const last = pipe === -1 ? alias : alias.slice(pipe + 1);
|
|
53
|
-
if (
|
|
53
|
+
if (isSizeSpec(trim(last)))
|
|
54
54
|
alias = pipe === -1 ? '' : trim(alias.slice(0, pipe));
|
|
55
55
|
}
|
|
56
56
|
return alias || link.target;
|
|
57
57
|
}
|
|
58
|
-
/** `300` or `300x200`: an embed's width, or width and height. */
|
|
59
|
-
function isSize(s) {
|
|
60
|
-
return /^[0-9]+(?:x[0-9]+)?$/.test(s);
|
|
61
|
-
}
|
|
62
58
|
/**
|
|
63
59
|
* Parse a whole value as a typed reference, or `null`.
|
|
64
60
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* The Rust crate reports byte offsets.
|
|
9
9
|
*/
|
|
10
10
|
export { type CodeRules, stripCode, stripCodeWith } from './code.ts';
|
|
11
|
-
export { ATTACHMENT_EXTENSIONS, type TextRange, type WikiLink, isVaultLinkUrl, mdLinkTarget, parseInner, targetRange, } from './parse.ts';
|
|
11
|
+
export { ATTACHMENT_EXTENSIONS, type Size, type TextRange, type WikiLink, isSizeSpec, isVaultLinkUrl, mdLinkTarget, parseInner, parseInnerSelf, parseSize, percentDecode, targetRange, } from './parse.ts';
|
|
12
12
|
export { type TypedRef, displayText, parseTypedRef } from './display.ts';
|
|
13
|
-
export { type BodyLink, type BodyLinkKind, type LinkKind, type RawLink, bodyLinks, bodyLinksSpanned, } from './body-links.ts';
|
|
13
|
+
export { type AbandonReason, type AbandonedLink, type BodyLink, type BodyLinkKind, type BodyScan, type LinkKind, type RawLink, type ScanOptions, bodyLinks, bodyLinksSpanned, bodyLinksSpannedWith, scanBodyWith, } from './body-links.ts';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
* The Rust crate reports byte offsets.
|
|
9
9
|
*/
|
|
10
10
|
export { stripCode, stripCodeWith } from "./code.js";
|
|
11
|
-
export { ATTACHMENT_EXTENSIONS, isVaultLinkUrl, mdLinkTarget, parseInner, targetRange, } from "./parse.js";
|
|
11
|
+
export { ATTACHMENT_EXTENSIONS, isSizeSpec, isVaultLinkUrl, mdLinkTarget, parseInner, parseInnerSelf, parseSize, percentDecode, targetRange, } from "./parse.js";
|
|
12
12
|
export { displayText, parseTypedRef } from "./display.js";
|
|
13
|
-
export { bodyLinks, bodyLinksSpanned, } from "./body-links.js";
|
|
13
|
+
export { bodyLinks, bodyLinksSpanned, bodyLinksSpannedWith, scanBodyWith, } from "./body-links.js";
|
package/dist/parse.d.ts
CHANGED
|
@@ -5,11 +5,33 @@
|
|
|
5
5
|
*/
|
|
6
6
|
/** A parsed `[[target#heading|alias]]`. */
|
|
7
7
|
export interface WikiLink {
|
|
8
|
+
/** The target, trimmed, minus any `#heading`, `#^block` and `|alias`. */
|
|
8
9
|
target: string;
|
|
10
|
+
/**
|
|
11
|
+
* The text after the first `#`, trimmed, unless it starts with `^`, which
|
|
12
|
+
* makes it a `block`. A heading path such as `Parent#Child` keeps its inner `#`.
|
|
13
|
+
*/
|
|
9
14
|
heading: string | null;
|
|
15
|
+
/** A block reference, `Note#^id`: the text after `#^`, trimmed. */
|
|
16
|
+
block: string | null;
|
|
17
|
+
/** The text after the first `|`, trimmed. An embed's size stays in it as written. */
|
|
10
18
|
alias: string | null;
|
|
19
|
+
/** An embed's `|300` or `|300x200`, read from the last `|` part of its alias. */
|
|
20
|
+
size: Size | null;
|
|
11
21
|
embed: boolean;
|
|
12
22
|
}
|
|
23
|
+
/** An embed's display size, `|300` (width) or `|300x200` (width and height). */
|
|
24
|
+
export interface Size {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number | null;
|
|
27
|
+
}
|
|
28
|
+
/** Whether `spec` is an image size, `300` or `300x200`. */
|
|
29
|
+
export declare function isSizeSpec(spec: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Read `300` or `300x200` as a `Size`. `null` for anything else, and for a
|
|
32
|
+
* number too large for the Rust crate's `u32`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSize(spec: string): Size | null;
|
|
13
35
|
/** A half-open range of UTF-16 offsets, `[start, end)`. */
|
|
14
36
|
export interface TextRange {
|
|
15
37
|
start: number;
|
|
@@ -25,6 +47,12 @@ export interface TextRange {
|
|
|
25
47
|
* a `#heading` or an `|alias`.
|
|
26
48
|
*/
|
|
27
49
|
export declare function parseInner(inner: string, embed: boolean): WikiLink | null;
|
|
50
|
+
/**
|
|
51
|
+
* `parseInner`, except that a body with no target and a `#` fragment, such as
|
|
52
|
+
* `#Heading` or `#^block`, is a link to the note it is written in: its target
|
|
53
|
+
* is empty. This is how Obsidian reads `[[#Heading]]`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseInnerSelf(inner: string, embed: boolean): WikiLink | null;
|
|
28
56
|
/**
|
|
29
57
|
* The range of `parseInner`'s target within `inner`, for a caller that
|
|
30
58
|
* rewrites the target in place and must leave the rest of the link as written.
|
|
@@ -66,3 +94,10 @@ export declare function isVaultLinkUrl(url: string): boolean;
|
|
|
66
94
|
* off.
|
|
67
95
|
*/
|
|
68
96
|
export declare function mdLinkTarget(url: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* Percent-decode a string over its UTF-8 bytes, as the Rust crate's
|
|
99
|
+
* `percent_decode` does. Each valid `%XX` decodes on its own, so a stray `%`
|
|
100
|
+
* (`100%`) stays as written. Bytes that do not decode to UTF-8 leave the whole
|
|
101
|
+
* string as written, rather than becoming U+FFFD.
|
|
102
|
+
*/
|
|
103
|
+
export declare function percentDecode(s: string): string;
|
package/dist/parse.js
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
* crate's `parse_inner`, `target_range`, `is_vault_link_url` and
|
|
4
4
|
* `md_link_target`.
|
|
5
5
|
*/
|
|
6
|
+
/** Whether `spec` is an image size, `300` or `300x200`. */
|
|
7
|
+
export function isSizeSpec(spec) {
|
|
8
|
+
return /^[0-9]+(?:x[0-9]+)?$/.test(spec);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read `300` or `300x200` as a `Size`. `null` for anything else, and for a
|
|
12
|
+
* number too large for the Rust crate's `u32`.
|
|
13
|
+
*/
|
|
14
|
+
export function parseSize(spec) {
|
|
15
|
+
if (!isSizeSpec(spec))
|
|
16
|
+
return null;
|
|
17
|
+
const [w, h] = spec.split('x');
|
|
18
|
+
const width = Number(w);
|
|
19
|
+
const height = h === undefined ? null : Number(h);
|
|
20
|
+
const fits = (n) => n <= 0xffffffff;
|
|
21
|
+
if (!fits(width) || (height !== null && !fits(height)))
|
|
22
|
+
return null;
|
|
23
|
+
return { width, height };
|
|
24
|
+
}
|
|
6
25
|
/**
|
|
7
26
|
* Parse a single wikilink body (the text between the brackets).
|
|
8
27
|
*
|
|
@@ -16,14 +35,42 @@ export function parseInner(inner, embed) {
|
|
|
16
35
|
const target = targetRange(inner);
|
|
17
36
|
if (target === null)
|
|
18
37
|
return null;
|
|
19
|
-
|
|
38
|
+
return parseParts(inner, inner.slice(target.start, target.end), embed);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* `parseInner`, except that a body with no target and a `#` fragment, such as
|
|
42
|
+
* `#Heading` or `#^block`, is a link to the note it is written in: its target
|
|
43
|
+
* is empty. This is how Obsidian reads `[[#Heading]]`.
|
|
44
|
+
*/
|
|
45
|
+
export function parseInnerSelf(inner, embed) {
|
|
46
|
+
const link = parseInner(inner, embed);
|
|
47
|
+
if (link !== null)
|
|
48
|
+
return link;
|
|
49
|
+
const [beforeAlias] = splitAlias(inner);
|
|
50
|
+
if (!trimStart(beforeAlias).startsWith('#'))
|
|
51
|
+
return null;
|
|
52
|
+
return parseParts(inner, '', embed);
|
|
53
|
+
}
|
|
54
|
+
/** The fragment, alias and size of `inner`, around an already-found target. */
|
|
55
|
+
function parseParts(inner, target, embed) {
|
|
56
|
+
const [beforeAlias, rawAlias] = splitAlias(inner);
|
|
20
57
|
const hash = beforeAlias.indexOf('#');
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
let heading = null;
|
|
59
|
+
let block = null;
|
|
60
|
+
if (hash !== -1) {
|
|
61
|
+
const fragment = trim(beforeAlias.slice(hash + 1));
|
|
62
|
+
if (fragment.startsWith('^'))
|
|
63
|
+
block = trim(fragment.slice(1));
|
|
64
|
+
else
|
|
65
|
+
heading = fragment;
|
|
66
|
+
}
|
|
67
|
+
const alias = rawAlias === null ? null : trim(rawAlias);
|
|
68
|
+
let size = null;
|
|
69
|
+
if (embed && alias !== null) {
|
|
70
|
+
const pipe = alias.lastIndexOf('|');
|
|
71
|
+
size = parseSize(trim(pipe === -1 ? alias : alias.slice(pipe + 1)));
|
|
72
|
+
}
|
|
73
|
+
return { target, heading, block, alias, size, embed };
|
|
27
74
|
}
|
|
28
75
|
/**
|
|
29
76
|
* The range of `parseInner`'s target within `inner`, for a caller that
|
|
@@ -165,10 +212,12 @@ function stripMdExt(s) {
|
|
|
165
212
|
}
|
|
166
213
|
const utf8Decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
167
214
|
/**
|
|
168
|
-
* Percent-decode a
|
|
169
|
-
*
|
|
215
|
+
* Percent-decode a string over its UTF-8 bytes, as the Rust crate's
|
|
216
|
+
* `percent_decode` does. Each valid `%XX` decodes on its own, so a stray `%`
|
|
217
|
+
* (`100%`) stays as written. Bytes that do not decode to UTF-8 leave the whole
|
|
218
|
+
* string as written, rather than becoming U+FFFD.
|
|
170
219
|
*/
|
|
171
|
-
function percentDecode(s) {
|
|
220
|
+
export function percentDecode(s) {
|
|
172
221
|
if (!s.includes('%'))
|
|
173
222
|
return s;
|
|
174
223
|
const bytes = new TextEncoder().encode(s);
|
|
@@ -193,18 +242,11 @@ function percentDecode(s) {
|
|
|
193
242
|
return s;
|
|
194
243
|
}
|
|
195
244
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Two bytes read as Rust's `u8::from_str_radix(_, 16)` reads them: two hex
|
|
198
|
-
* digits, or a `+` sign and one hex digit.
|
|
199
|
-
*/
|
|
245
|
+
/** Two bytes read as two hex digits, or `null`. A sign is not a digit. */
|
|
200
246
|
function hexByte(a, b) {
|
|
201
|
-
const low = hexDigit(b);
|
|
202
|
-
if (low === null)
|
|
203
|
-
return null;
|
|
204
|
-
if (a === 0x2b)
|
|
205
|
-
return low;
|
|
206
247
|
const high = hexDigit(a);
|
|
207
|
-
|
|
248
|
+
const low = hexDigit(b);
|
|
249
|
+
return high === null || low === null ? null : high * 16 + low;
|
|
208
250
|
}
|
|
209
251
|
function hexDigit(byte) {
|
|
210
252
|
if (byte >= 0x30 && byte <= 0x39)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osdlc/markdown-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Wikilink parsing that follows documented link-resolution conventions: every [[link]], ![[embed]] and relative markdown link in a note body, with spans, and the code lexer that keeps links in code out. The TypeScript twin of the parsing half of the Rust markdown-link crate, held to it by a shared case file. No dependencies.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "sksizer",
|