agent-sanitizer 2.53.0 → 2.54.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/LICENSE-THIRD-PARTY +28 -0
- package/package.json +12 -2
- package/src/gfm.mjs +54 -0
- package/src/html.mjs +1 -1
- package/src/vendor/gfm-autolink-literal.mjs +1015 -0
- package/types/gfm.d.mts +8 -0
- package/types/vendor/gfm-autolink-literal.d.mts +10 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Third-party code redistributed in this package.
|
|
2
|
+
|
|
3
|
+
src/vendor/gfm-autolink-literal.mjs
|
|
4
|
+
Copied from micromark-extension-gfm-autolink-literal@2.1.0, with one
|
|
5
|
+
function changed (see that file's header). Original licence follows.
|
|
6
|
+
|
|
7
|
+
(The MIT License)
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2020 Titus Wormer <tituswormer@gmail.com>
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
12
|
+
a copy of this software and associated documentation files (the
|
|
13
|
+
'Software'), to deal in the Software without restriction, including
|
|
14
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
15
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
16
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
17
|
+
the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be
|
|
20
|
+
included in all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
23
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
24
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
25
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
26
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
27
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
28
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.54.0",
|
|
4
4
|
"description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"lint-staged": "^17.0.5",
|
|
66
66
|
"prettier": "^3.0.0",
|
|
67
67
|
"rehype-parse": "9.0.1",
|
|
68
|
+
"remark-gfm": "4.0.1",
|
|
68
69
|
"smol-toml": "^1.7.1",
|
|
69
70
|
"typescript": "6.0.3",
|
|
70
71
|
"typescript-eslint": "8.61.0",
|
|
@@ -204,6 +205,7 @@
|
|
|
204
205
|
},
|
|
205
206
|
"files": [
|
|
206
207
|
"src/*.mjs",
|
|
208
|
+
"src/vendor/*.mjs",
|
|
207
209
|
"python/agent_sanitizer/data/invisible-charset.json",
|
|
208
210
|
"python/agent_sanitizer/secrets/data/credential-names.json",
|
|
209
211
|
"python/agent_sanitizer/secrets/data/redaction-floor.json",
|
|
@@ -213,6 +215,7 @@
|
|
|
213
215
|
"bin/sanitize-cli.mjs",
|
|
214
216
|
"types",
|
|
215
217
|
"LICENSE",
|
|
218
|
+
"LICENSE-THIRD-PARTY",
|
|
216
219
|
"README.md",
|
|
217
220
|
"THREAT-MODEL.md",
|
|
218
221
|
"SECURITY.md"
|
|
@@ -221,9 +224,16 @@
|
|
|
221
224
|
"agent-control-plane-core": "0.3.0",
|
|
222
225
|
"css-tree": "^3.2.1",
|
|
223
226
|
"hast-util-from-parse5": "8.0.3",
|
|
227
|
+
"mdast-util-gfm": "3.1.0",
|
|
228
|
+
"micromark-extension-gfm-footnote": "2.1.0",
|
|
229
|
+
"micromark-extension-gfm-strikethrough": "2.1.0",
|
|
230
|
+
"micromark-extension-gfm-table": "2.1.1",
|
|
231
|
+
"micromark-extension-gfm-task-list-item": "2.1.0",
|
|
232
|
+
"micromark-util-character": "2.1.1",
|
|
233
|
+
"micromark-util-combine-extensions": "2.0.1",
|
|
234
|
+
"micromark-util-types": "2.0.2",
|
|
224
235
|
"namespace-guard": "0.20.0",
|
|
225
236
|
"parse5": "7.3.0",
|
|
226
|
-
"remark-gfm": "4.0.1",
|
|
227
237
|
"remark-parse": "11.0.0",
|
|
228
238
|
"unified": "11.0.5",
|
|
229
239
|
"unist-util-visit": "5.1.0",
|
package/src/gfm.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `remark-gfm` with one extension swapped for a corrected copy.
|
|
3
|
+
*
|
|
4
|
+
* This is `remark-gfm@4.0.1`'s own plugin body and
|
|
5
|
+
* `micromark-extension-gfm@3.0.0`'s own `gfm()` composition, inlined so that
|
|
6
|
+
* `gfmAutolinkLiteral` can come from `./vendor/gfm-autolink-literal.mjs`
|
|
7
|
+
* instead of the published package — see that file for what differs and when
|
|
8
|
+
* to delete both. Every other extension, and the mdast layer, are the upstream
|
|
9
|
+
* ones at the versions `remark-gfm` pins.
|
|
10
|
+
*
|
|
11
|
+
* Assembling the list here rather than adding an extension beside `remark-gfm`
|
|
12
|
+
* is deliberate: micromark tries the constructs registered for a character in
|
|
13
|
+
* order, so a second autolink extension would run AFTER the upstream one and
|
|
14
|
+
* change nothing.
|
|
15
|
+
*
|
|
16
|
+
* `test/gfm-autolink-parity.test.mjs` pins this against upstream's answers.
|
|
17
|
+
*/
|
|
18
|
+
import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
|
|
19
|
+
import { gfmFootnote } from "micromark-extension-gfm-footnote";
|
|
20
|
+
import { gfmStrikethrough } from "micromark-extension-gfm-strikethrough";
|
|
21
|
+
import { gfmTable } from "micromark-extension-gfm-table";
|
|
22
|
+
import { gfmTaskListItem } from "micromark-extension-gfm-task-list-item";
|
|
23
|
+
import { combineExtensions } from "micromark-util-combine-extensions";
|
|
24
|
+
|
|
25
|
+
import { gfmAutolinkLiteral } from "./vendor/gfm-autolink-literal.mjs";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A unified plugin adding GFM support: autolink literals, footnotes,
|
|
29
|
+
* strikethrough, tables and task lists — the same set, in the same order, as
|
|
30
|
+
* `remark-gfm`.
|
|
31
|
+
* @this {any} unified processor
|
|
32
|
+
* @returns {undefined}
|
|
33
|
+
*/
|
|
34
|
+
export default function remarkGfmFixed() {
|
|
35
|
+
const data = this.data();
|
|
36
|
+
const micromarkExtensions =
|
|
37
|
+
data.micromarkExtensions || (data.micromarkExtensions = []);
|
|
38
|
+
const fromMarkdownExtensions =
|
|
39
|
+
data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
|
|
40
|
+
const toMarkdownExtensions =
|
|
41
|
+
data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
|
|
42
|
+
|
|
43
|
+
micromarkExtensions.push(
|
|
44
|
+
combineExtensions([
|
|
45
|
+
gfmAutolinkLiteral(),
|
|
46
|
+
gfmFootnote(),
|
|
47
|
+
gfmStrikethrough(),
|
|
48
|
+
gfmTable(),
|
|
49
|
+
gfmTaskListItem(),
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
fromMarkdownExtensions.push(gfmFromMarkdown());
|
|
53
|
+
toMarkdownExtensions.push(gfmToMarkdown());
|
|
54
|
+
}
|
package/src/html.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import cssGenerate from "css-tree/generator";
|
|
|
48
48
|
import { ident as cssIdent } from "css-tree/utils";
|
|
49
49
|
import { unified } from "unified";
|
|
50
50
|
import remarkParse from "remark-parse";
|
|
51
|
-
import remarkGfm from "
|
|
51
|
+
import remarkGfm from "./gfm.mjs";
|
|
52
52
|
import { parseHtmlFragment } from "./html-tree-adapter.mjs";
|
|
53
53
|
import { SKIP, EXIT } from "unist-util-visit";
|
|
54
54
|
import {
|
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GFM autolink literals for micromark, vendored from
|
|
3
|
+
* `micromark-extension-gfm-autolink-literal@2.1.0` (MIT, (c) 2020 Titus
|
|
4
|
+
* Wormer). The licence is in `LICENSE-THIRD-PARTY` at the repository root.
|
|
5
|
+
*
|
|
6
|
+
* WHY A COPY. `previousUnbalanced` below decides whether an autolink literal
|
|
7
|
+
* may start at this position, and upstream answers it by walking back over
|
|
8
|
+
* every event the document has produced so far. It memoizes only the "no"
|
|
9
|
+
* answer, so one unclosed `[` — a JSON array is the everyday shape, since its
|
|
10
|
+
* `]` is the last byte — makes the answer "yes" for the rest of the document
|
|
11
|
+
* and costs one full walk per word character. 172 KB of prose behind a single
|
|
12
|
+
* `[` took 20.2s to parse; the same text takes 0.17s here. Layer 3 reads its
|
|
13
|
+
* URLs off this parse, so the cost lands on every `detectExfil` call.
|
|
14
|
+
*
|
|
15
|
+
* WHAT CHANGED. One function. `previousUnbalanced` now also records where the
|
|
16
|
+
* walk stopped, and a later walk resumes from that index rather than re-reading
|
|
17
|
+
* the run before it. The record is used only while `events[index]` still holds
|
|
18
|
+
* the same token, so a resolver that splices events invalidates it and the walk
|
|
19
|
+
* falls back to reading everything. Nothing else in this file differs from
|
|
20
|
+
* upstream — diff it against the published package to confirm.
|
|
21
|
+
*
|
|
22
|
+
* WHY NOT A PATCH. A `pnpm` patch fixes this repository's own installs and the
|
|
23
|
+
* plugin bundle built from them, and reaches no consumer of the published npm
|
|
24
|
+
* package (see the npm-consumer gap in the project history). A copy under
|
|
25
|
+
* `src/` ships.
|
|
26
|
+
*
|
|
27
|
+
* WHEN TO DELETE IT. When a published release of the upstream package carries
|
|
28
|
+
* an equivalent fix: drop this file and the assembly in `./gfm.mjs`, and go
|
|
29
|
+
* back to `remark-gfm`. `test/gfm-autolink-parity.test.mjs` pins the behaviour
|
|
30
|
+
* either way.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @import {Code, ConstructRecord, Event, Extension, Previous, State, TokenizeContext, Tokenizer} from 'micromark-util-types'
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
asciiAlpha,
|
|
39
|
+
asciiAlphanumeric,
|
|
40
|
+
asciiControl,
|
|
41
|
+
markdownLineEndingOrSpace,
|
|
42
|
+
unicodePunctuation,
|
|
43
|
+
unicodeWhitespace,
|
|
44
|
+
} from "micromark-util-character";
|
|
45
|
+
const wwwPrefix = {
|
|
46
|
+
tokenize: tokenizeWwwPrefix,
|
|
47
|
+
partial: true,
|
|
48
|
+
};
|
|
49
|
+
const domain = {
|
|
50
|
+
tokenize: tokenizeDomain,
|
|
51
|
+
partial: true,
|
|
52
|
+
};
|
|
53
|
+
const path = {
|
|
54
|
+
tokenize: tokenizePath,
|
|
55
|
+
partial: true,
|
|
56
|
+
};
|
|
57
|
+
const trail = {
|
|
58
|
+
tokenize: tokenizeTrail,
|
|
59
|
+
partial: true,
|
|
60
|
+
};
|
|
61
|
+
const emailDomainDotTrail = {
|
|
62
|
+
tokenize: tokenizeEmailDomainDotTrail,
|
|
63
|
+
partial: true,
|
|
64
|
+
};
|
|
65
|
+
const wwwAutolink = {
|
|
66
|
+
name: "wwwAutolink",
|
|
67
|
+
tokenize: tokenizeWwwAutolink,
|
|
68
|
+
previous: previousWww,
|
|
69
|
+
};
|
|
70
|
+
const protocolAutolink = {
|
|
71
|
+
name: "protocolAutolink",
|
|
72
|
+
tokenize: tokenizeProtocolAutolink,
|
|
73
|
+
previous: previousProtocol,
|
|
74
|
+
};
|
|
75
|
+
const emailAutolink = {
|
|
76
|
+
name: "emailAutolink",
|
|
77
|
+
tokenize: tokenizeEmailAutolink,
|
|
78
|
+
previous: previousEmail,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** @type {ConstructRecord} */
|
|
82
|
+
const text = {};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create an extension for `micromark` to support GitHub autolink literal
|
|
86
|
+
* syntax.
|
|
87
|
+
*
|
|
88
|
+
* @returns {Extension}
|
|
89
|
+
* Extension for `micromark` that can be passed in `extensions` to enable GFM
|
|
90
|
+
* autolink literal syntax.
|
|
91
|
+
*/
|
|
92
|
+
export function gfmAutolinkLiteral() {
|
|
93
|
+
return {
|
|
94
|
+
text,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** @type {Code} */
|
|
99
|
+
let code = 48;
|
|
100
|
+
|
|
101
|
+
// Add alphanumerics.
|
|
102
|
+
while (code < 123) {
|
|
103
|
+
text[code] = emailAutolink;
|
|
104
|
+
code++;
|
|
105
|
+
if (code === 58) code = 65;
|
|
106
|
+
else if (code === 91) code = 97;
|
|
107
|
+
}
|
|
108
|
+
text[43] = emailAutolink;
|
|
109
|
+
text[45] = emailAutolink;
|
|
110
|
+
text[46] = emailAutolink;
|
|
111
|
+
text[95] = emailAutolink;
|
|
112
|
+
text[72] = [emailAutolink, protocolAutolink];
|
|
113
|
+
text[104] = [emailAutolink, protocolAutolink];
|
|
114
|
+
text[87] = [emailAutolink, wwwAutolink];
|
|
115
|
+
text[119] = [emailAutolink, wwwAutolink];
|
|
116
|
+
|
|
117
|
+
// To do: perform email autolink literals on events, afterwards.
|
|
118
|
+
// That’s where `markdown-rs` and `cmark-gfm` perform it.
|
|
119
|
+
// It should look for `@`, then for atext backwards, and then for a label
|
|
120
|
+
// forwards.
|
|
121
|
+
// To do: `mailto:`, `xmpp:` protocol as prefix.
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Email autolink literal.
|
|
125
|
+
*
|
|
126
|
+
* ```markdown
|
|
127
|
+
* > | a contact@example.org b
|
|
128
|
+
* ^^^^^^^^^^^^^^^^^^^
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* @this {TokenizeContext}
|
|
132
|
+
* @type {Tokenizer}
|
|
133
|
+
*/
|
|
134
|
+
function tokenizeEmailAutolink(effects, ok, nok) {
|
|
135
|
+
const self = this;
|
|
136
|
+
/** @type {boolean | undefined} */
|
|
137
|
+
let dot;
|
|
138
|
+
/** @type {boolean} */
|
|
139
|
+
let data;
|
|
140
|
+
return start;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Start of email autolink literal.
|
|
144
|
+
*
|
|
145
|
+
* ```markdown
|
|
146
|
+
* > | a contact@example.org b
|
|
147
|
+
* ^
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
150
|
+
* @type {State}
|
|
151
|
+
*/
|
|
152
|
+
function start(code) {
|
|
153
|
+
if (
|
|
154
|
+
!gfmAtext(code) ||
|
|
155
|
+
!previousEmail.call(self, self.previous) ||
|
|
156
|
+
previousUnbalanced(self.events)
|
|
157
|
+
) {
|
|
158
|
+
return nok(code);
|
|
159
|
+
}
|
|
160
|
+
effects.enter("literalAutolink");
|
|
161
|
+
effects.enter("literalAutolinkEmail");
|
|
162
|
+
return atext(code);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* In email atext.
|
|
167
|
+
*
|
|
168
|
+
* ```markdown
|
|
169
|
+
* > | a contact@example.org b
|
|
170
|
+
* ^
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* @type {State}
|
|
174
|
+
*/
|
|
175
|
+
function atext(code) {
|
|
176
|
+
if (gfmAtext(code)) {
|
|
177
|
+
effects.consume(code);
|
|
178
|
+
return atext;
|
|
179
|
+
}
|
|
180
|
+
if (code === 64) {
|
|
181
|
+
effects.consume(code);
|
|
182
|
+
return emailDomain;
|
|
183
|
+
}
|
|
184
|
+
return nok(code);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* In email domain.
|
|
189
|
+
*
|
|
190
|
+
* The reference code is a bit overly complex as it handles the `@`, of which
|
|
191
|
+
* there may be just one.
|
|
192
|
+
* Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L318>
|
|
193
|
+
*
|
|
194
|
+
* ```markdown
|
|
195
|
+
* > | a contact@example.org b
|
|
196
|
+
* ^
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* @type {State}
|
|
200
|
+
*/
|
|
201
|
+
function emailDomain(code) {
|
|
202
|
+
// Dot followed by alphanumerical (not `-` or `_`).
|
|
203
|
+
if (code === 46) {
|
|
204
|
+
return effects.check(
|
|
205
|
+
emailDomainDotTrail,
|
|
206
|
+
emailDomainAfter,
|
|
207
|
+
emailDomainDot,
|
|
208
|
+
)(code);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Alphanumerical, `-`, and `_`.
|
|
212
|
+
if (code === 45 || code === 95 || asciiAlphanumeric(code)) {
|
|
213
|
+
data = true;
|
|
214
|
+
effects.consume(code);
|
|
215
|
+
return emailDomain;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// To do: `/` if xmpp.
|
|
219
|
+
|
|
220
|
+
// Note: normally we’d truncate trailing punctuation from the link.
|
|
221
|
+
// However, email autolink literals cannot contain any of those markers,
|
|
222
|
+
// except for `.`, but that can only occur if it isn’t trailing.
|
|
223
|
+
// So we can ignore truncating!
|
|
224
|
+
return emailDomainAfter(code);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* In email domain, on dot that is not a trail.
|
|
229
|
+
*
|
|
230
|
+
* ```markdown
|
|
231
|
+
* > | a contact@example.org b
|
|
232
|
+
* ^
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* @type {State}
|
|
236
|
+
*/
|
|
237
|
+
function emailDomainDot(code) {
|
|
238
|
+
effects.consume(code);
|
|
239
|
+
dot = true;
|
|
240
|
+
return emailDomain;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* After email domain.
|
|
245
|
+
*
|
|
246
|
+
* ```markdown
|
|
247
|
+
* > | a contact@example.org b
|
|
248
|
+
* ^
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* @type {State}
|
|
252
|
+
*/
|
|
253
|
+
function emailDomainAfter(code) {
|
|
254
|
+
// Domain must not be empty, must include a dot, and must end in alphabetical.
|
|
255
|
+
// Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L332>.
|
|
256
|
+
if (data && dot && asciiAlpha(self.previous)) {
|
|
257
|
+
effects.exit("literalAutolinkEmail");
|
|
258
|
+
effects.exit("literalAutolink");
|
|
259
|
+
return ok(code);
|
|
260
|
+
}
|
|
261
|
+
return nok(code);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* `www` autolink literal.
|
|
267
|
+
*
|
|
268
|
+
* ```markdown
|
|
269
|
+
* > | a www.example.org b
|
|
270
|
+
* ^^^^^^^^^^^^^^^
|
|
271
|
+
* ```
|
|
272
|
+
*
|
|
273
|
+
* @this {TokenizeContext}
|
|
274
|
+
* @type {Tokenizer}
|
|
275
|
+
*/
|
|
276
|
+
function tokenizeWwwAutolink(effects, ok, nok) {
|
|
277
|
+
const self = this;
|
|
278
|
+
return wwwStart;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Start of www autolink literal.
|
|
282
|
+
*
|
|
283
|
+
* ```markdown
|
|
284
|
+
* > | www.example.com/a?b#c
|
|
285
|
+
* ^
|
|
286
|
+
* ```
|
|
287
|
+
*
|
|
288
|
+
* @type {State}
|
|
289
|
+
*/
|
|
290
|
+
function wwwStart(code) {
|
|
291
|
+
if (
|
|
292
|
+
(code !== 87 && code !== 119) ||
|
|
293
|
+
!previousWww.call(self, self.previous) ||
|
|
294
|
+
previousUnbalanced(self.events)
|
|
295
|
+
) {
|
|
296
|
+
return nok(code);
|
|
297
|
+
}
|
|
298
|
+
effects.enter("literalAutolink");
|
|
299
|
+
effects.enter("literalAutolinkWww");
|
|
300
|
+
// Note: we *check*, so we can discard the `www.` we parsed.
|
|
301
|
+
// If it worked, we consider it as a part of the domain.
|
|
302
|
+
return effects.check(
|
|
303
|
+
wwwPrefix,
|
|
304
|
+
effects.attempt(domain, effects.attempt(path, wwwAfter), nok),
|
|
305
|
+
nok,
|
|
306
|
+
)(code);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* After a www autolink literal.
|
|
311
|
+
*
|
|
312
|
+
* ```markdown
|
|
313
|
+
* > | www.example.com/a?b#c
|
|
314
|
+
* ^
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* @type {State}
|
|
318
|
+
*/
|
|
319
|
+
function wwwAfter(code) {
|
|
320
|
+
effects.exit("literalAutolinkWww");
|
|
321
|
+
effects.exit("literalAutolink");
|
|
322
|
+
return ok(code);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Protocol autolink literal.
|
|
328
|
+
*
|
|
329
|
+
* ```markdown
|
|
330
|
+
* > | a https://example.org b
|
|
331
|
+
* ^^^^^^^^^^^^^^^^^^^
|
|
332
|
+
* ```
|
|
333
|
+
*
|
|
334
|
+
* @this {TokenizeContext}
|
|
335
|
+
* @type {Tokenizer}
|
|
336
|
+
*/
|
|
337
|
+
function tokenizeProtocolAutolink(effects, ok, nok) {
|
|
338
|
+
const self = this;
|
|
339
|
+
let buffer = "";
|
|
340
|
+
let seen = false;
|
|
341
|
+
return protocolStart;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Start of protocol autolink literal.
|
|
345
|
+
*
|
|
346
|
+
* ```markdown
|
|
347
|
+
* > | https://example.com/a?b#c
|
|
348
|
+
* ^
|
|
349
|
+
* ```
|
|
350
|
+
*
|
|
351
|
+
* @type {State}
|
|
352
|
+
*/
|
|
353
|
+
function protocolStart(code) {
|
|
354
|
+
if (
|
|
355
|
+
(code === 72 || code === 104) &&
|
|
356
|
+
previousProtocol.call(self, self.previous) &&
|
|
357
|
+
!previousUnbalanced(self.events)
|
|
358
|
+
) {
|
|
359
|
+
effects.enter("literalAutolink");
|
|
360
|
+
effects.enter("literalAutolinkHttp");
|
|
361
|
+
buffer += String.fromCodePoint(code);
|
|
362
|
+
effects.consume(code);
|
|
363
|
+
return protocolPrefixInside;
|
|
364
|
+
}
|
|
365
|
+
return nok(code);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* In protocol.
|
|
370
|
+
*
|
|
371
|
+
* ```markdown
|
|
372
|
+
* > | https://example.com/a?b#c
|
|
373
|
+
* ^^^^^
|
|
374
|
+
* ```
|
|
375
|
+
*
|
|
376
|
+
* @type {State}
|
|
377
|
+
*/
|
|
378
|
+
function protocolPrefixInside(code) {
|
|
379
|
+
// `5` is size of `https`
|
|
380
|
+
if (asciiAlpha(code) && buffer.length < 5) {
|
|
381
|
+
// @ts-expect-error: definitely number.
|
|
382
|
+
buffer += String.fromCodePoint(code);
|
|
383
|
+
effects.consume(code);
|
|
384
|
+
return protocolPrefixInside;
|
|
385
|
+
}
|
|
386
|
+
if (code === 58) {
|
|
387
|
+
const protocol = buffer.toLowerCase();
|
|
388
|
+
if (protocol === "http" || protocol === "https") {
|
|
389
|
+
effects.consume(code);
|
|
390
|
+
return protocolSlashesInside;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return nok(code);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* In slashes.
|
|
398
|
+
*
|
|
399
|
+
* ```markdown
|
|
400
|
+
* > | https://example.com/a?b#c
|
|
401
|
+
* ^^
|
|
402
|
+
* ```
|
|
403
|
+
*
|
|
404
|
+
* @type {State}
|
|
405
|
+
*/
|
|
406
|
+
function protocolSlashesInside(code) {
|
|
407
|
+
if (code === 47) {
|
|
408
|
+
effects.consume(code);
|
|
409
|
+
if (seen) {
|
|
410
|
+
return afterProtocol;
|
|
411
|
+
}
|
|
412
|
+
seen = true;
|
|
413
|
+
return protocolSlashesInside;
|
|
414
|
+
}
|
|
415
|
+
return nok(code);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* After protocol, before domain.
|
|
420
|
+
*
|
|
421
|
+
* ```markdown
|
|
422
|
+
* > | https://example.com/a?b#c
|
|
423
|
+
* ^
|
|
424
|
+
* ```
|
|
425
|
+
*
|
|
426
|
+
* @type {State}
|
|
427
|
+
*/
|
|
428
|
+
function afterProtocol(code) {
|
|
429
|
+
// To do: this is different from `markdown-rs`:
|
|
430
|
+
// https://github.com/wooorm/markdown-rs/blob/b3a921c761309ae00a51fe348d8a43adbc54b518/src/construct/gfm_autolink_literal.rs#L172-L182
|
|
431
|
+
return code === null ||
|
|
432
|
+
asciiControl(code) ||
|
|
433
|
+
markdownLineEndingOrSpace(code) ||
|
|
434
|
+
unicodeWhitespace(code) ||
|
|
435
|
+
unicodePunctuation(code)
|
|
436
|
+
? nok(code)
|
|
437
|
+
: effects.attempt(
|
|
438
|
+
domain,
|
|
439
|
+
effects.attempt(path, protocolAfter),
|
|
440
|
+
nok,
|
|
441
|
+
)(code);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* After a protocol autolink literal.
|
|
446
|
+
*
|
|
447
|
+
* ```markdown
|
|
448
|
+
* > | https://example.com/a?b#c
|
|
449
|
+
* ^
|
|
450
|
+
* ```
|
|
451
|
+
*
|
|
452
|
+
* @type {State}
|
|
453
|
+
*/
|
|
454
|
+
function protocolAfter(code) {
|
|
455
|
+
effects.exit("literalAutolinkHttp");
|
|
456
|
+
effects.exit("literalAutolink");
|
|
457
|
+
return ok(code);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* `www` prefix.
|
|
463
|
+
*
|
|
464
|
+
* ```markdown
|
|
465
|
+
* > | a www.example.org b
|
|
466
|
+
* ^^^^
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* @this {TokenizeContext}
|
|
470
|
+
* @type {Tokenizer}
|
|
471
|
+
*/
|
|
472
|
+
function tokenizeWwwPrefix(effects, ok, nok) {
|
|
473
|
+
let size = 0;
|
|
474
|
+
return wwwPrefixInside;
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* In www prefix.
|
|
478
|
+
*
|
|
479
|
+
* ```markdown
|
|
480
|
+
* > | www.example.com
|
|
481
|
+
* ^^^^
|
|
482
|
+
* ```
|
|
483
|
+
*
|
|
484
|
+
* @type {State}
|
|
485
|
+
*/
|
|
486
|
+
function wwwPrefixInside(code) {
|
|
487
|
+
if ((code === 87 || code === 119) && size < 3) {
|
|
488
|
+
size++;
|
|
489
|
+
effects.consume(code);
|
|
490
|
+
return wwwPrefixInside;
|
|
491
|
+
}
|
|
492
|
+
if (code === 46 && size === 3) {
|
|
493
|
+
effects.consume(code);
|
|
494
|
+
return wwwPrefixAfter;
|
|
495
|
+
}
|
|
496
|
+
return nok(code);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* After www prefix.
|
|
501
|
+
*
|
|
502
|
+
* ```markdown
|
|
503
|
+
* > | www.example.com
|
|
504
|
+
* ^
|
|
505
|
+
* ```
|
|
506
|
+
*
|
|
507
|
+
* @type {State}
|
|
508
|
+
*/
|
|
509
|
+
function wwwPrefixAfter(code) {
|
|
510
|
+
// If there is *anything*, we can link.
|
|
511
|
+
return code === null ? nok(code) : ok(code);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Domain.
|
|
517
|
+
*
|
|
518
|
+
* ```markdown
|
|
519
|
+
* > | a https://example.org b
|
|
520
|
+
* ^^^^^^^^^^^
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* @this {TokenizeContext}
|
|
524
|
+
* @type {Tokenizer}
|
|
525
|
+
*/
|
|
526
|
+
function tokenizeDomain(effects, ok, nok) {
|
|
527
|
+
/** @type {boolean | undefined} */
|
|
528
|
+
let underscoreInLastSegment;
|
|
529
|
+
/** @type {boolean | undefined} */
|
|
530
|
+
let underscoreInLastLastSegment;
|
|
531
|
+
/** @type {boolean | undefined} */
|
|
532
|
+
let seen;
|
|
533
|
+
return domainInside;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* In domain.
|
|
537
|
+
*
|
|
538
|
+
* ```markdown
|
|
539
|
+
* > | https://example.com/a
|
|
540
|
+
* ^^^^^^^^^^^
|
|
541
|
+
* ```
|
|
542
|
+
*
|
|
543
|
+
* @type {State}
|
|
544
|
+
*/
|
|
545
|
+
function domainInside(code) {
|
|
546
|
+
// Check whether this marker, which is a trailing punctuation
|
|
547
|
+
// marker, optionally followed by more trailing markers, and then
|
|
548
|
+
// followed by an end.
|
|
549
|
+
if (code === 46 || code === 95) {
|
|
550
|
+
return effects.check(trail, domainAfter, domainAtPunctuation)(code);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// GH documents that only alphanumerics (other than `-`, `.`, and `_`) can
|
|
554
|
+
// occur, which sounds like ASCII only, but they also support `www.點看.com`,
|
|
555
|
+
// so that’s Unicode.
|
|
556
|
+
// Instead of some new production for Unicode alphanumerics, markdown
|
|
557
|
+
// already has that for Unicode punctuation and whitespace, so use those.
|
|
558
|
+
// Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L12>.
|
|
559
|
+
if (
|
|
560
|
+
code === null ||
|
|
561
|
+
markdownLineEndingOrSpace(code) ||
|
|
562
|
+
unicodeWhitespace(code) ||
|
|
563
|
+
(code !== 45 && unicodePunctuation(code))
|
|
564
|
+
) {
|
|
565
|
+
return domainAfter(code);
|
|
566
|
+
}
|
|
567
|
+
seen = true;
|
|
568
|
+
effects.consume(code);
|
|
569
|
+
return domainInside;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* In domain, at potential trailing punctuation, that was not trailing.
|
|
574
|
+
*
|
|
575
|
+
* ```markdown
|
|
576
|
+
* > | https://example.com
|
|
577
|
+
* ^
|
|
578
|
+
* ```
|
|
579
|
+
*
|
|
580
|
+
* @type {State}
|
|
581
|
+
*/
|
|
582
|
+
function domainAtPunctuation(code) {
|
|
583
|
+
// There is an underscore in the last segment of the domain
|
|
584
|
+
if (code === 95) {
|
|
585
|
+
underscoreInLastSegment = true;
|
|
586
|
+
}
|
|
587
|
+
// Otherwise, it’s a `.`: save the last segment underscore in the
|
|
588
|
+
// penultimate segment slot.
|
|
589
|
+
else {
|
|
590
|
+
underscoreInLastLastSegment = underscoreInLastSegment;
|
|
591
|
+
underscoreInLastSegment = undefined;
|
|
592
|
+
}
|
|
593
|
+
effects.consume(code);
|
|
594
|
+
return domainInside;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* After domain.
|
|
599
|
+
*
|
|
600
|
+
* ```markdown
|
|
601
|
+
* > | https://example.com/a
|
|
602
|
+
* ^
|
|
603
|
+
* ```
|
|
604
|
+
*
|
|
605
|
+
* @type {State} */
|
|
606
|
+
function domainAfter(code) {
|
|
607
|
+
// Note: that’s GH says a dot is needed, but it’s not true:
|
|
608
|
+
// <https://github.com/github/cmark-gfm/issues/279>
|
|
609
|
+
if (underscoreInLastLastSegment || underscoreInLastSegment || !seen) {
|
|
610
|
+
return nok(code);
|
|
611
|
+
}
|
|
612
|
+
return ok(code);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Path.
|
|
618
|
+
*
|
|
619
|
+
* ```markdown
|
|
620
|
+
* > | a https://example.org/stuff b
|
|
621
|
+
* ^^^^^^
|
|
622
|
+
* ```
|
|
623
|
+
*
|
|
624
|
+
* @this {TokenizeContext}
|
|
625
|
+
* @type {Tokenizer}
|
|
626
|
+
*/
|
|
627
|
+
function tokenizePath(effects, ok) {
|
|
628
|
+
let sizeOpen = 0;
|
|
629
|
+
let sizeClose = 0;
|
|
630
|
+
return pathInside;
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* In path.
|
|
634
|
+
*
|
|
635
|
+
* ```markdown
|
|
636
|
+
* > | https://example.com/a
|
|
637
|
+
* ^^
|
|
638
|
+
* ```
|
|
639
|
+
*
|
|
640
|
+
* @type {State}
|
|
641
|
+
*/
|
|
642
|
+
function pathInside(code) {
|
|
643
|
+
if (code === 40) {
|
|
644
|
+
sizeOpen++;
|
|
645
|
+
effects.consume(code);
|
|
646
|
+
return pathInside;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// To do: `markdown-rs` also needs this.
|
|
650
|
+
// If this is a paren, and there are less closings than openings,
|
|
651
|
+
// we don’t check for a trail.
|
|
652
|
+
if (code === 41 && sizeClose < sizeOpen) {
|
|
653
|
+
return pathAtPunctuation(code);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Check whether this trailing punctuation marker is optionally
|
|
657
|
+
// followed by more trailing markers, and then followed
|
|
658
|
+
// by an end.
|
|
659
|
+
if (
|
|
660
|
+
code === 33 ||
|
|
661
|
+
code === 34 ||
|
|
662
|
+
code === 38 ||
|
|
663
|
+
code === 39 ||
|
|
664
|
+
code === 41 ||
|
|
665
|
+
code === 42 ||
|
|
666
|
+
code === 44 ||
|
|
667
|
+
code === 46 ||
|
|
668
|
+
code === 58 ||
|
|
669
|
+
code === 59 ||
|
|
670
|
+
code === 60 ||
|
|
671
|
+
code === 63 ||
|
|
672
|
+
code === 93 ||
|
|
673
|
+
code === 95 ||
|
|
674
|
+
code === 126
|
|
675
|
+
) {
|
|
676
|
+
return effects.check(trail, ok, pathAtPunctuation)(code);
|
|
677
|
+
}
|
|
678
|
+
if (
|
|
679
|
+
code === null ||
|
|
680
|
+
markdownLineEndingOrSpace(code) ||
|
|
681
|
+
unicodeWhitespace(code)
|
|
682
|
+
) {
|
|
683
|
+
return ok(code);
|
|
684
|
+
}
|
|
685
|
+
effects.consume(code);
|
|
686
|
+
return pathInside;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* In path, at potential trailing punctuation, that was not trailing.
|
|
691
|
+
*
|
|
692
|
+
* ```markdown
|
|
693
|
+
* > | https://example.com/a"b
|
|
694
|
+
* ^
|
|
695
|
+
* ```
|
|
696
|
+
*
|
|
697
|
+
* @type {State}
|
|
698
|
+
*/
|
|
699
|
+
function pathAtPunctuation(code) {
|
|
700
|
+
// Count closing parens.
|
|
701
|
+
if (code === 41) {
|
|
702
|
+
sizeClose++;
|
|
703
|
+
}
|
|
704
|
+
effects.consume(code);
|
|
705
|
+
return pathInside;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Trail.
|
|
711
|
+
*
|
|
712
|
+
* This calls `ok` if this *is* the trail, followed by an end, which means
|
|
713
|
+
* the entire trail is not part of the link.
|
|
714
|
+
* It calls `nok` if this *is* part of the link.
|
|
715
|
+
*
|
|
716
|
+
* ```markdown
|
|
717
|
+
* > | https://example.com").
|
|
718
|
+
* ^^^
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* @this {TokenizeContext}
|
|
722
|
+
* @type {Tokenizer}
|
|
723
|
+
*/
|
|
724
|
+
function tokenizeTrail(effects, ok, nok) {
|
|
725
|
+
return trail;
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* In trail of domain or path.
|
|
729
|
+
*
|
|
730
|
+
* ```markdown
|
|
731
|
+
* > | https://example.com").
|
|
732
|
+
* ^
|
|
733
|
+
* ```
|
|
734
|
+
*
|
|
735
|
+
* @type {State}
|
|
736
|
+
*/
|
|
737
|
+
function trail(code) {
|
|
738
|
+
// Regular trailing punctuation.
|
|
739
|
+
if (
|
|
740
|
+
code === 33 ||
|
|
741
|
+
code === 34 ||
|
|
742
|
+
code === 39 ||
|
|
743
|
+
code === 41 ||
|
|
744
|
+
code === 42 ||
|
|
745
|
+
code === 44 ||
|
|
746
|
+
code === 46 ||
|
|
747
|
+
code === 58 ||
|
|
748
|
+
code === 59 ||
|
|
749
|
+
code === 63 ||
|
|
750
|
+
code === 95 ||
|
|
751
|
+
code === 126
|
|
752
|
+
) {
|
|
753
|
+
effects.consume(code);
|
|
754
|
+
return trail;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// `&` followed by one or more alphabeticals and then a `;`, is
|
|
758
|
+
// as a whole considered as trailing punctuation.
|
|
759
|
+
// In all other cases, it is considered as continuation of the URL.
|
|
760
|
+
if (code === 38) {
|
|
761
|
+
effects.consume(code);
|
|
762
|
+
return trailCharacterReferenceStart;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Needed because we allow literals after `[`, as we fix:
|
|
766
|
+
// <https://github.com/github/cmark-gfm/issues/278>.
|
|
767
|
+
// Check that it is not followed by `(` or `[`.
|
|
768
|
+
if (code === 93) {
|
|
769
|
+
effects.consume(code);
|
|
770
|
+
return trailBracketAfter;
|
|
771
|
+
}
|
|
772
|
+
if (
|
|
773
|
+
// `<` is an end.
|
|
774
|
+
code === 60 ||
|
|
775
|
+
// So is whitespace.
|
|
776
|
+
code === null ||
|
|
777
|
+
markdownLineEndingOrSpace(code) ||
|
|
778
|
+
unicodeWhitespace(code)
|
|
779
|
+
) {
|
|
780
|
+
return ok(code);
|
|
781
|
+
}
|
|
782
|
+
return nok(code);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* In trail, after `]`.
|
|
787
|
+
*
|
|
788
|
+
* > 👉 **Note**: this deviates from `cmark-gfm` to fix a bug.
|
|
789
|
+
* > See end of <https://github.com/github/cmark-gfm/issues/278> for more.
|
|
790
|
+
*
|
|
791
|
+
* ```markdown
|
|
792
|
+
* > | https://example.com](
|
|
793
|
+
* ^
|
|
794
|
+
* ```
|
|
795
|
+
*
|
|
796
|
+
* @type {State}
|
|
797
|
+
*/
|
|
798
|
+
function trailBracketAfter(code) {
|
|
799
|
+
// Whitespace or something that could start a resource or reference is the end.
|
|
800
|
+
// Switch back to trail otherwise.
|
|
801
|
+
if (
|
|
802
|
+
code === null ||
|
|
803
|
+
code === 40 ||
|
|
804
|
+
code === 91 ||
|
|
805
|
+
markdownLineEndingOrSpace(code) ||
|
|
806
|
+
unicodeWhitespace(code)
|
|
807
|
+
) {
|
|
808
|
+
return ok(code);
|
|
809
|
+
}
|
|
810
|
+
return trail(code);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* In character-reference like trail, after `&`.
|
|
815
|
+
*
|
|
816
|
+
* ```markdown
|
|
817
|
+
* > | https://example.com&).
|
|
818
|
+
* ^
|
|
819
|
+
* ```
|
|
820
|
+
*
|
|
821
|
+
* @type {State}
|
|
822
|
+
*/
|
|
823
|
+
function trailCharacterReferenceStart(code) {
|
|
824
|
+
// When non-alpha, it’s not a trail.
|
|
825
|
+
return asciiAlpha(code) ? trailCharacterReferenceInside(code) : nok(code);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* In character-reference like trail.
|
|
830
|
+
*
|
|
831
|
+
* ```markdown
|
|
832
|
+
* > | https://example.com&).
|
|
833
|
+
* ^
|
|
834
|
+
* ```
|
|
835
|
+
*
|
|
836
|
+
* @type {State}
|
|
837
|
+
*/
|
|
838
|
+
function trailCharacterReferenceInside(code) {
|
|
839
|
+
// Switch back to trail if this is well-formed.
|
|
840
|
+
if (code === 59) {
|
|
841
|
+
effects.consume(code);
|
|
842
|
+
return trail;
|
|
843
|
+
}
|
|
844
|
+
if (asciiAlpha(code)) {
|
|
845
|
+
effects.consume(code);
|
|
846
|
+
return trailCharacterReferenceInside;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// It’s not a trail.
|
|
850
|
+
return nok(code);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Dot in email domain trail.
|
|
856
|
+
*
|
|
857
|
+
* This calls `ok` if this *is* the trail, followed by an end, which means
|
|
858
|
+
* the trail is not part of the link.
|
|
859
|
+
* It calls `nok` if this *is* part of the link.
|
|
860
|
+
*
|
|
861
|
+
* ```markdown
|
|
862
|
+
* > | contact@example.org.
|
|
863
|
+
* ^
|
|
864
|
+
* ```
|
|
865
|
+
*
|
|
866
|
+
* @this {TokenizeContext}
|
|
867
|
+
* @type {Tokenizer}
|
|
868
|
+
*/
|
|
869
|
+
function tokenizeEmailDomainDotTrail(effects, ok, nok) {
|
|
870
|
+
return start;
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Dot.
|
|
874
|
+
*
|
|
875
|
+
* ```markdown
|
|
876
|
+
* > | contact@example.org.
|
|
877
|
+
* ^ ^
|
|
878
|
+
* ```
|
|
879
|
+
*
|
|
880
|
+
* @type {State}
|
|
881
|
+
*/
|
|
882
|
+
function start(code) {
|
|
883
|
+
// Must be dot.
|
|
884
|
+
effects.consume(code);
|
|
885
|
+
return after;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* After dot.
|
|
890
|
+
*
|
|
891
|
+
* ```markdown
|
|
892
|
+
* > | contact@example.org.
|
|
893
|
+
* ^ ^
|
|
894
|
+
* ```
|
|
895
|
+
*
|
|
896
|
+
* @type {State}
|
|
897
|
+
*/
|
|
898
|
+
function after(code) {
|
|
899
|
+
// Not a trail if alphanumeric.
|
|
900
|
+
return asciiAlphanumeric(code) ? nok(code) : ok(code);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* See:
|
|
906
|
+
* <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L156>.
|
|
907
|
+
*
|
|
908
|
+
* @type {Previous}
|
|
909
|
+
*/
|
|
910
|
+
function previousWww(code) {
|
|
911
|
+
return (
|
|
912
|
+
code === null ||
|
|
913
|
+
code === 40 ||
|
|
914
|
+
code === 42 ||
|
|
915
|
+
code === 95 ||
|
|
916
|
+
code === 91 ||
|
|
917
|
+
code === 93 ||
|
|
918
|
+
code === 126 ||
|
|
919
|
+
markdownLineEndingOrSpace(code)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* See:
|
|
925
|
+
* <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L214>.
|
|
926
|
+
*
|
|
927
|
+
* @type {Previous}
|
|
928
|
+
*/
|
|
929
|
+
function previousProtocol(code) {
|
|
930
|
+
return !asciiAlpha(code);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* @this {TokenizeContext}
|
|
935
|
+
* @type {Previous}
|
|
936
|
+
*/
|
|
937
|
+
function previousEmail(code) {
|
|
938
|
+
// Do not allow a slash “inside” atext.
|
|
939
|
+
// The reference code is a bit weird, but that’s what it results in.
|
|
940
|
+
// Source: <https://github.com/github/cmark-gfm/blob/ef1cfcb/extensions/autolink.c#L307>.
|
|
941
|
+
// Other than slash, every preceding character is allowed.
|
|
942
|
+
return !(code === 47 || gfmAtext(code));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* @param {Code} code
|
|
947
|
+
* @returns {boolean}
|
|
948
|
+
*/
|
|
949
|
+
function gfmAtext(code) {
|
|
950
|
+
return (
|
|
951
|
+
code === 43 ||
|
|
952
|
+
code === 45 ||
|
|
953
|
+
code === 46 ||
|
|
954
|
+
code === 95 ||
|
|
955
|
+
asciiAlphanumeric(code)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* @param {Array<Event>} events
|
|
961
|
+
* @returns {boolean}
|
|
962
|
+
*/
|
|
963
|
+
function previousUnbalanced(events) {
|
|
964
|
+
let index = events.length;
|
|
965
|
+
let result = false;
|
|
966
|
+
while (index--) {
|
|
967
|
+
const token = events[index][1];
|
|
968
|
+
if (
|
|
969
|
+
(token.type === "labelLink" || token.type === "labelImage") &&
|
|
970
|
+
!token._balanced
|
|
971
|
+
) {
|
|
972
|
+
result = true;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// If we’ve seen this token, and it was marked as not having any unbalanced
|
|
977
|
+
// bracket before it, we can exit.
|
|
978
|
+
if (token._gfmAutolinkLiteralWalkedInto) {
|
|
979
|
+
result = false;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// A run an earlier walk crossed without finding either of the above. Jump
|
|
984
|
+
// over it instead of re-reading it, so a document whose answer is “yes”
|
|
985
|
+
// stops costing one full walk per candidate character. The jump is only
|
|
986
|
+
// taken while the run’s far end still sits at the index it was recorded
|
|
987
|
+
// at and still holds the same token: a resolver that splices events can
|
|
988
|
+
// move a token out of the array, and the identity check is what notices.
|
|
989
|
+
// `skip.index < index` keeps the walk moving backwards whatever the array
|
|
990
|
+
// has done since.
|
|
991
|
+
const skip = token._gfmAutolinkLiteralSkipTo;
|
|
992
|
+
if (
|
|
993
|
+
skip !== undefined &&
|
|
994
|
+
skip.index < index &&
|
|
995
|
+
events[skip.index] !== undefined &&
|
|
996
|
+
events[skip.index][1] === skip.token
|
|
997
|
+
) {
|
|
998
|
+
index = skip.index + 1;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (events.length > 0) {
|
|
1002
|
+
if (!result) {
|
|
1003
|
+
// Mark the last token as “walked into” w/o finding
|
|
1004
|
+
// anything.
|
|
1005
|
+
events[events.length - 1][1]._gfmAutolinkLiteralWalkedInto = true;
|
|
1006
|
+
} else if (index >= 0) {
|
|
1007
|
+
// Record where the walk stopped, so the next one resumes there.
|
|
1008
|
+
events[events.length - 1][1]._gfmAutolinkLiteralSkipTo = {
|
|
1009
|
+
index: index,
|
|
1010
|
+
token: events[index][1],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
package/types/gfm.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unified plugin adding GFM support: autolink literals, footnotes,
|
|
3
|
+
* strikethrough, tables and task lists — the same set, in the same order, as
|
|
4
|
+
* `remark-gfm`.
|
|
5
|
+
* @this {any} unified processor
|
|
6
|
+
* @returns {undefined}
|
|
7
|
+
*/
|
|
8
|
+
export default function remarkGfmFixed(this: any): undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an extension for `micromark` to support GitHub autolink literal
|
|
3
|
+
* syntax.
|
|
4
|
+
*
|
|
5
|
+
* @returns {Extension}
|
|
6
|
+
* Extension for `micromark` that can be passed in `extensions` to enable GFM
|
|
7
|
+
* autolink literal syntax.
|
|
8
|
+
*/
|
|
9
|
+
export function gfmAutolinkLiteral(): Extension;
|
|
10
|
+
import type { Extension } from 'micromark-util-types';
|