agent-sanitizer 2.53.0 → 2.54.1

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,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.53.0",
3
+ "version": "2.54.1",
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 "remark-gfm";
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&amp;).
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&amp;).
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
+ }
@@ -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';