securemark 0.260.4 → 0.261.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +12 -12
  3. package/design.md +0 -4
  4. package/dist/index.js +268 -243
  5. package/markdown.d.ts +7 -23
  6. package/package.json +6 -6
  7. package/src/combinator/data/parser/context/memo.ts +4 -0
  8. package/src/combinator/data/parser/context.ts +25 -16
  9. package/src/combinator/data/parser.ts +0 -1
  10. package/src/parser/api/bind.ts +2 -1
  11. package/src/parser/api/parse.test.ts +1 -1
  12. package/src/parser/api/parse.ts +2 -1
  13. package/src/parser/block/blockquote.test.ts +2 -2
  14. package/src/parser/block/dlist.test.ts +1 -1
  15. package/src/parser/block/extension/example.test.ts +1 -1
  16. package/src/parser/block/extension/fig.test.ts +1 -1
  17. package/src/parser/block/heading.test.ts +2 -2
  18. package/src/parser/block/paragraph.test.ts +1 -4
  19. package/src/parser/block/reply/cite.test.ts +5 -0
  20. package/src/parser/block/reply/cite.ts +5 -4
  21. package/src/parser/context.ts +8 -7
  22. package/src/parser/inline/autolink/anchor.test.ts +1 -0
  23. package/src/parser/inline/autolink/email.test.ts +3 -0
  24. package/src/parser/inline/autolink/email.ts +1 -1
  25. package/src/parser/inline/autolink/hashnum.test.ts +1 -2
  26. package/src/parser/inline/autolink/hashnum.ts +1 -1
  27. package/src/parser/inline/autolink/hashtag.test.ts +15 -12
  28. package/src/parser/inline/autolink/hashtag.ts +3 -3
  29. package/src/parser/inline/autolink/url.test.ts +1 -1
  30. package/src/parser/inline/autolink/url.ts +1 -1
  31. package/src/parser/inline/autolink.ts +14 -5
  32. package/src/parser/inline/deletion.test.ts +2 -2
  33. package/src/parser/inline/emphasis.test.ts +26 -35
  34. package/src/parser/inline/emphasis.ts +5 -12
  35. package/src/parser/inline/escape.ts +1 -10
  36. package/src/parser/inline/extension/index.test.ts +2 -2
  37. package/src/parser/inline/extension/index.ts +1 -1
  38. package/src/parser/inline/insertion.test.ts +2 -2
  39. package/src/parser/inline/link.test.ts +1 -1
  40. package/src/parser/inline/link.ts +2 -2
  41. package/src/parser/inline/mark.test.ts +1 -1
  42. package/src/parser/inline/media.ts +2 -2
  43. package/src/parser/inline/ruby.ts +10 -6
  44. package/src/parser/inline/strong.test.ts +25 -32
  45. package/src/parser/inline/strong.ts +4 -8
  46. package/src/parser/inline/template.ts +1 -1
  47. package/src/parser/inline.test.ts +18 -91
  48. package/src/parser/inline.ts +0 -3
  49. package/src/parser/locale/ja.ts +1 -9
  50. package/src/parser/locale.test.ts +1 -1
  51. package/src/parser/source/text.test.ts +9 -4
  52. package/src/parser/source/text.ts +9 -16
  53. package/src/parser/inline/emstrong.ts +0 -62
package/markdown.d.ts CHANGED
@@ -635,7 +635,6 @@ export namespace MarkdownParser {
635
635
  InlineParser.InsertionParser,
636
636
  InlineParser.DeletionParser,
637
637
  InlineParser.MarkParser,
638
- InlineParser.EmStrongParser,
639
638
  InlineParser.StrongParser,
640
639
  InlineParser.EmphasisParser,
641
640
  InlineParser.CodeParser,
@@ -829,8 +828,8 @@ export namespace MarkdownParser {
829
828
  // [AB](a b)
830
829
  Inline<'ruby'>,
831
830
  Parser<HTMLElement, Context, [
832
- RubyParser.TextParser,
833
- RubyParser.TextParser,
831
+ SourceParser.StrParser,
832
+ SourceParser.StrParser,
834
833
  ]> {
835
834
  }
836
835
  export namespace RubyParser {
@@ -1032,36 +1031,20 @@ export namespace MarkdownParser {
1032
1031
  MarkParser,
1033
1032
  ]> {
1034
1033
  }
1035
- export interface EmStrongParser extends
1036
- // *abc*
1037
- Inline<'emstrong'>,
1038
- Parser<HTMLElement | string, Context, [
1039
- InlineParser,
1040
- InlineParser,
1041
- ]> {
1042
- }
1043
1034
  export interface StrongParser extends
1044
- // **abc**
1035
+ // *abc*
1045
1036
  Inline<'strong'>,
1046
1037
  Parser<HTMLElement | string, Context, [
1047
1038
  InlineParser,
1048
- Parser<HTMLElement | string, Context, [
1049
- EmStrongParser,
1050
- StrongParser,
1051
- ]>,
1039
+ StrongParser,
1052
1040
  ]> {
1053
1041
  }
1054
1042
  export interface EmphasisParser extends
1055
- // *abc*
1043
+ // _abc_
1056
1044
  Inline<'emphasis'>,
1057
1045
  Parser<HTMLElement | string, Context, [
1058
- StrongParser,
1059
1046
  InlineParser,
1060
- Parser<HTMLElement | string, Context, [
1061
- EmStrongParser,
1062
- StrongParser,
1063
- EmphasisParser,
1064
- ]>,
1047
+ EmphasisParser,
1065
1048
  ]> {
1066
1049
  }
1067
1050
  export interface CodeParser extends
@@ -1103,6 +1086,7 @@ export namespace MarkdownParser {
1103
1086
  AutolinkParser.HashtagParser,
1104
1087
  AutolinkParser.HashnumParser,
1105
1088
  SourceParser.StrParser,
1089
+ SourceParser.StrParser,
1106
1090
  AutolinkParser.AnchorParser,
1107
1091
  ]> {
1108
1092
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.260.4",
3
+ "version": "0.261.0",
4
4
  "description": "Secure markdown renderer working on browsers for user input data.",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/falsandtru/securemark",
@@ -34,11 +34,11 @@
34
34
  "@types/mocha": "9.1.1",
35
35
  "@types/power-assert": "1.5.8",
36
36
  "@types/prismjs": "1.26.0",
37
- "@typescript-eslint/parser": "^5.30.7",
37
+ "@typescript-eslint/parser": "^5.33.0",
38
38
  "babel-loader": "^8.2.5",
39
39
  "babel-plugin-unassert": "^3.2.0",
40
40
  "concurrently": "^7.3.0",
41
- "eslint": "^8.20.0",
41
+ "eslint": "^8.21.0",
42
42
  "eslint-plugin-redos": "^4.4.1",
43
43
  "eslint-webpack-plugin": "^3.2.0",
44
44
  "glob": "^8.0.3",
@@ -49,13 +49,13 @@
49
49
  "karma-mocha": "^2.0.1",
50
50
  "karma-power-assert": "^1.0.0",
51
51
  "mocha": "^10.0.0",
52
- "npm-check-updates": "^15.3.4",
52
+ "npm-check-updates": "^16.0.5",
53
53
  "semver": "^7.3.7",
54
- "spica": "0.0.573",
54
+ "spica": "0.0.591",
55
55
  "ts-loader": "^9.3.1",
56
56
  "typed-dom": "^0.0.301",
57
57
  "typescript": "4.7.4",
58
- "webpack": "^5.73.0",
58
+ "webpack": "^5.74.0",
59
59
  "webpack-cli": "^4.10.0",
60
60
  "webpack-merge": "^5.8.0"
61
61
  },
@@ -1,4 +1,8 @@
1
1
  export class Memo {
2
+ constructor({ targets = ~0 } = {}) {
3
+ this.targets = targets;
4
+ }
5
+ public readonly targets: number;
2
6
  private readonly memory: Record<string, readonly [any[], number] | readonly []>[/* pos */] = [];
3
7
  public get length(): number {
4
8
  return this.memory.length;
@@ -5,12 +5,15 @@ import { Memo } from './context/memo';
5
5
 
6
6
  export function reset<P extends Parser<unknown>>(base: Context<P>, parser: P): P;
7
7
  export function reset<T>(base: Ctx, parser: Parser<T>): Parser<T> {
8
+ if (!('memo' in base)) {
9
+ base.memo = undefined;
10
+ }
8
11
  assert(Object.getPrototypeOf(base) === Object.prototype);
9
12
  assert(Object.freeze(base));
10
13
  const changes = Object.entries(base);
11
14
  const values = Array(changes.length);
12
15
  return ({ source, context }) =>
13
- apply(parser, source, ObjectCreate(context), changes, values);
16
+ apply(parser, source, ObjectCreate(context), changes, values, true);
14
17
  }
15
18
 
16
19
  export function context<P extends Parser<unknown>>(base: Context<P>, parser: P): P;
@@ -23,18 +26,22 @@ export function context<T>(base: Ctx, parser: Parser<T>): Parser<T> {
23
26
  apply(parser, source, context, changes, values);
24
27
  }
25
28
 
26
- function apply<P extends Parser<unknown>>(parser: P, source: string, context: Context<P>, changes: [string, any][], values: any[]): Result<Tree<P>>;
27
- function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: [string, any][], values: any[]): Result<T> {
29
+ function apply<P extends Parser<unknown>>(parser: P, source: string, context: Context<P>, changes: [string, any][], values: any[], reset?: boolean): Result<Tree<P>>;
30
+ function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: [string, any][], values: any[], reset = false): Result<T> {
28
31
  if (context) for (let i = 0; i < changes.length; ++i) {
29
32
  const change = changes[i];
30
33
  const prop = change[0];
31
34
  switch (prop) {
32
35
  case 'resources':
36
+ if (!reset) break;
33
37
  assert(typeof change[1] === 'object');
34
38
  assert(context[prop] || !(prop in context));
35
39
  if (prop in context && !hasOwnProperty(context, prop)) break;
36
- // @ts-expect-error
37
- context[prop] = ObjectCreate(change[1]);
40
+ context[prop as string] = ObjectCreate(change[1]);
41
+ break;
42
+ case 'memo':
43
+ if (!reset) break;
44
+ context.memo = new Memo({ targets: context.memo?.targets });
38
45
  break;
39
46
  default:
40
47
  values[i] = context[prop];
@@ -47,7 +54,10 @@ function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: [str
47
54
  const prop = change[0];
48
55
  switch (prop) {
49
56
  case 'resources':
50
- break;
57
+ // @ts-expect-error
58
+ case 'memo':
59
+ if (!reset) break;
60
+ // fallthrough
51
61
  default:
52
62
  context[prop] = values[i];
53
63
  values[i] = undefined;
@@ -61,26 +71,25 @@ export function syntax<T>(syntax: number, prec: number, cost: number, state: num
61
71
  return creation(cost, precedence(prec, ({ source, context }) => {
62
72
  if (source === '') return;
63
73
  const memo = context.memo ??= new Memo();
64
- context.memorable ??= ~0;
65
74
  context.offset ??= 0;
66
75
  const position = source.length + context.offset!;
67
- const st0 = context.state ?? 0;
68
- const st1 = context.state = st0 | state;
69
- const cache = syntax && memo.get(position, syntax, st1);
76
+ const stateOuter = context.state ?? 0;
77
+ const stateInner = context.state = stateOuter | state;
78
+ const cache = syntax && stateInner & memo.targets && memo.get(position, syntax, stateInner);
70
79
  const result: Result<T> = cache
71
80
  ? cache.length === 0
72
81
  ? undefined
73
82
  : [cache[0], source.slice(cache[1])]
74
83
  : parser!({ source, context });
75
- if (syntax && st0 & context.memorable!) {
76
- cache ?? memo.set(position, syntax, st1, eval(result), source.length - exec(result, '').length);
77
- assert.deepStrictEqual(cache && cache, cache && memo.get(position, syntax, st1));
84
+ if (syntax && stateOuter & memo.targets) {
85
+ cache ?? memo.set(position, syntax, stateInner, eval(result), source.length - exec(result, '').length);
86
+ assert.deepStrictEqual(cache && cache, cache && memo.get(position, syntax, stateInner));
78
87
  }
79
- if (result && !st0 && memo.length! >= position + 2) {
80
- assert(!(st0 & context.memorable!));
88
+ if (result && !stateOuter && memo.length! >= position + 2) {
89
+ assert(!(stateOuter & memo.targets));
81
90
  memo.clear(position + 2);
82
91
  }
83
- context.state = st0;
92
+ context.state = stateOuter;
84
93
  return result;
85
94
  }));
86
95
  }
@@ -20,7 +20,6 @@ export interface Ctx {
20
20
  precedence?: number;
21
21
  delimiters?: Delimiters;
22
22
  state?: number;
23
- memorable?: number;
24
23
  memo?: Memo;
25
24
  }
26
25
  export type Tree<P extends Parser<unknown>> = P extends Parser<infer T> ? T : never;
@@ -2,6 +2,7 @@ import { undefined, location } from 'spica/global';
2
2
  import { ParserSettings, Progress } from '../../..';
3
3
  import { MarkdownParser } from '../../../markdown';
4
4
  import { eval } from '../../combinator/data/parser';
5
+ import { Memo } from '../../combinator/data/parser/context/memo';
5
6
  import { segment, validate, MAX_INPUT_SIZE } from '../segment';
6
7
  import { header } from '../header';
7
8
  import { block } from '../block';
@@ -24,7 +25,7 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
24
25
  let context: MarkdownParser.Context = {
25
26
  ...settings,
26
27
  host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
27
- memorable: State.backtrackable,
28
+ memo: new Memo({ targets: State.backtrackers }),
28
29
  };
29
30
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
30
31
  assert(!settings.id);
@@ -206,7 +206,7 @@ describe('Unit: parser/api/parse', () => {
206
206
  it('footnote', () => {
207
207
  const footnotes = { references: html('ol') };
208
208
  assert.deepStrictEqual(
209
- [...parse('$-a\n$$\n$$\n\n(($-a[[b]][[c*d*]]))', { footnotes }).children].map(el => el.outerHTML),
209
+ [...parse('$-a\n$$\n$$\n\n(($-a[[b]][[c_d_]]))', { footnotes }).children].map(el => el.outerHTML),
210
210
  [
211
211
  '<figure data-type="math" data-label="$-a" data-group="$" data-number="1" id="label:$-a"><figcaption><span class="figindex">(1)</span><span class="figtext"></span></figcaption><div><div class="math" translate="no">$$\n$$</div></div></figure>',
212
212
  '<p><sup class="annotation" id="annotation:ref:1" title="(1)[1][2]"><span hidden=""><a class="label" data-label="$-a" href="#label:$-a">(1)</a><sup class="reference" id="reference:ref:1" title="b"><span hidden="">b</span><a href="#reference:def:1">[1]</a></sup><sup class="reference" id="reference:ref:2" title="cd"><span hidden="">c<em>d</em></span><a href="#reference:def:2">[2]</a></sup></span><a href="#annotation:def:1">*1</a></sup></p>',
@@ -2,6 +2,7 @@ import { location } from 'spica/global';
2
2
  import { ParserOptions } from '../../..';
3
3
  import { MarkdownParser } from '../../../markdown';
4
4
  import { eval } from '../../combinator/data/parser';
5
+ import { Memo } from '../../combinator/data/parser/context/memo';
5
6
  import { segment, validate, MAX_SEGMENT_SIZE } from '../segment';
6
7
  import { header } from '../header';
7
8
  import { block } from '../block';
@@ -30,7 +31,7 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
30
31
  ...context?.resources && {
31
32
  resources: context.resources,
32
33
  },
33
- memorable: State.backtrackable,
34
+ memo: new Memo({ targets: State.backtrackers }),
34
35
  };
35
36
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
36
37
  const node = frag();
@@ -70,8 +70,8 @@ describe('Unit: parser/block/blockquote', () => {
70
70
  assert.deepStrictEqual(inspect(parser('!> a')), [['<blockquote><section><p>a</p><ol class="references"></ol></section></blockquote>'], '']);
71
71
  assert.deepStrictEqual(inspect(parser('!> a\n')), [['<blockquote><section><p>a</p><ol class="references"></ol></section></blockquote>'], '']);
72
72
  assert.deepStrictEqual(inspect(parser('!> a\\\nb')), [['<blockquote><section><p>a<span class="linebreak"> </span>b</p><ol class="references"></ol></section></blockquote>'], '']);
73
- assert.deepStrictEqual(inspect(parser('!> *a\nb*')), [['<blockquote><section><p><em>a<br>b</em></p><ol class="references"></ol></section></blockquote>'], '']);
74
- assert.deepStrictEqual(inspect(parser('!> *a\n> b*')), [['<blockquote><section><p><em>a<br>b</em></p><ol class="references"></ol></section></blockquote>'], '']);
73
+ assert.deepStrictEqual(inspect(parser('!> _a\nb_')), [['<blockquote><section><p><em>a<br>b</em></p><ol class="references"></ol></section></blockquote>'], '']);
74
+ assert.deepStrictEqual(inspect(parser('!> _a\n> b_')), [['<blockquote><section><p><em>a<br>b</em></p><ol class="references"></ol></section></blockquote>'], '']);
75
75
  assert.deepStrictEqual(inspect(parser('!> a \n b c ')), [['<blockquote><section><p> a<br> b c</p><ol class="references"></ol></section></blockquote>'], '']);
76
76
  assert.deepStrictEqual(inspect(parser('!>> a')), [['<blockquote><blockquote><section><p>a</p><ol class="references"></ol></section></blockquote></blockquote>'], '']);
77
77
  assert.deepStrictEqual(inspect(parser('!>> a\n> b')), [['<blockquote><blockquote><section><p>a</p><ol class="references"></ol></section></blockquote><section><p>b</p><ol class="references"></ol></section></blockquote>'], '']);
@@ -68,7 +68,7 @@ describe('Unit: parser/block/dlist', () => {
68
68
  assert.deepStrictEqual(inspect(parser('~ a [#b]')), [['<dl><dt id="index:b">a<span class="indexer" data-index="b"></span></dt><dd></dd></dl>'], '']);
69
69
  assert.deepStrictEqual(inspect(parser('~ a [#b]\\')), [['<dl><dt id="index:a_[#b]">a [<a class="hashtag" href="/hashtags/b">#b</a>]</dt><dd></dd></dl>'], '']);
70
70
  assert.deepStrictEqual(inspect(parser('~ A')), [['<dl><dt id="index:A">A</dt><dd></dd></dl>'], '']);
71
- assert.deepStrictEqual(inspect(parser('~ *A*')), [['<dl><dt id="index:A"><em>A</em></dt><dd></dd></dl>'], '']);
71
+ assert.deepStrictEqual(inspect(parser('~ _A_')), [['<dl><dt id="index:A"><em>A</em></dt><dd></dd></dl>'], '']);
72
72
  assert.deepStrictEqual(inspect(parser('~ `A`')), [['<dl><dt id="index:`A`"><code data-src="`A`">A</code></dt><dd></dd></dl>'], '']);
73
73
  assert.deepStrictEqual(inspect(parser('~ ${A}$')), [['<dl><dt id="index:${A}$"><span class="math" translate="no" data-src="${A}$">${A}$</span></dt><dd></dd></dl>'], '']);
74
74
  });
@@ -18,7 +18,7 @@ describe('Unit: parser/block/extension/example', () => {
18
18
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no"></pre><hr><section><ol class="references"></ol></section></aside>'], '']);
19
19
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\n\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no"></pre><hr><section><ol class="references"></ol></section></aside>'], '']);
20
20
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\na\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">a</pre><hr><section><p>a</p><ol class="references"></ol></section></aside>'], '']);
21
- assert.deepStrictEqual(inspect(parser('~~~example/markdown\n*a\nb*\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">*a\nb*</pre><hr><section><p><em>a<br>b</em></p><ol class="references"></ol></section></aside>'], '']);
21
+ assert.deepStrictEqual(inspect(parser('~~~example/markdown\n_a\nb_\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">_a\nb_</pre><hr><section><p><em>a<br>b</em></p><ol class="references"></ol></section></aside>'], '']);
22
22
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\n$fig-a\n!https://host\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">$fig-a\n!https://host</pre><hr><section><figure data-type="media" data-label="fig-a" data-group="fig" data-number="1"><figcaption><span class="figindex">Fig. 1. </span><span class="figtext"></span></figcaption><div><a href="https://host" target="_blank"><img class="media" data-src="https://host" alt=""></a></div></figure><ol class="references"></ol></section></aside>'], '']);
23
23
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\n[$fig-a]\n!https://host\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">[$fig-a]\n!https://host</pre><hr><section><figure data-type="media" data-label="fig-a" data-group="fig" data-number="1"><figcaption><span class="figindex">Fig. 1. </span><span class="figtext"></span></figcaption><div><a href="https://host" target="_blank"><img class="media" data-src="https://host" alt=""></a></div></figure><ol class="references"></ol></section></aside>'], '']);
24
24
  assert.deepStrictEqual(inspect(parser('~~~example/markdown\n## a\n~~~')), [['<aside class="example" data-type="markdown"><pre translate="no">## a</pre><hr><section><h2>a</h2><ol class="references"></ol></section></aside>'], '']);
@@ -40,7 +40,7 @@ describe('Unit: parser/block/extension/fig', () => {
40
40
  assert.deepStrictEqual(inspect(parser('[$group-name]\n> ')), [['<figure data-type="quote" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><blockquote></blockquote></div></figure>'], '']);
41
41
  assert.deepStrictEqual(inspect(parser('[$group-name]\n> \n')), [['<figure data-type="quote" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><blockquote></blockquote></div></figure>'], '']);
42
42
  assert.deepStrictEqual(inspect(parser('[$group-name]\n>\n~~~')), [['<figure data-type="quote" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><blockquote><pre><br>~~~</pre></blockquote></div></figure>'], '']);
43
- assert.deepStrictEqual(inspect(parser('[$group-name]\n!> *a*')), [['<figure data-type="quote" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><blockquote><section><p><em>a</em></p><ol class="references"></ol></section></blockquote></div></figure>'], '']);
43
+ assert.deepStrictEqual(inspect(parser('[$group-name]\n!> _a_')), [['<figure data-type="quote" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><blockquote><section><p><em>a</em></p><ol class="references"></ol></section></blockquote></div></figure>'], '']);
44
44
  assert.deepStrictEqual(inspect(parser('[$group-name]\n![]{https://host}')), [['<figure data-type="media" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><a href="https://host" target="_blank"><img class="media" data-src="https://host" alt=""></a></div></figure>'], '']);
45
45
  assert.deepStrictEqual(inspect(parser('[$group-name]\n![]{https://host}\n')), [['<figure data-type="media" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><a href="https://host" target="_blank"><img class="media" data-src="https://host" alt=""></a></div></figure>'], '']);
46
46
  assert.deepStrictEqual(inspect(parser('$group-name\n!https://host')), [['<figure data-type="media" data-label="group-name" data-group="group"><figcaption><span class="figindex"></span><span class="figtext"></span></figcaption><div><a href="https://host" target="_blank"><img class="media" data-src="https://host" alt=""></a></div></figure>'], '']);
@@ -36,7 +36,7 @@ describe('Unit: parser/block/heading', () => {
36
36
  assert.deepStrictEqual(inspect(parser('# a ')), [['<h1 id="index:a">a</h1>'], '']);
37
37
  assert.deepStrictEqual(inspect(parser('# a b c \n')), [['<h1 id="index:a_b_c">a b c</h1>'], '']);
38
38
  assert.deepStrictEqual(inspect(parser('# a\n')), [['<h1 id="index:a">a</h1>'], '']);
39
- assert.deepStrictEqual(inspect(parser('# *a*`b`${c}$')), [['<h1 id="index:a`b`${c}$"><em>a</em><code data-src="`b`">b</code><span class="math" translate="no" data-src="${c}$">${c}$</span></h1>'], '']);
39
+ assert.deepStrictEqual(inspect(parser('# _a_`b`${c}$')), [['<h1 id="index:a`b`${c}$"><em>a</em><code data-src="`b`">b</code><span class="math" translate="no" data-src="${c}$">${c}$</span></h1>'], '']);
40
40
  assert.deepStrictEqual(inspect(parser('# a\\')), [['<h1 id="index:a">a</h1>'], '']);
41
41
  assert.deepStrictEqual(inspect(parser('# a\\\n')), [['<h1 id="index:a">a</h1>'], '']);
42
42
  assert.deepStrictEqual(inspect(parser('# \\')), [['<h1 id="index:\\">\\</h1>'], '']);
@@ -69,7 +69,7 @@ describe('Unit: parser/block/heading', () => {
69
69
  assert.deepStrictEqual(inspect(parser('# a [#b ]')), [['<h1 id="index:b">a<span class="indexer" data-index="b"></span></h1>'], '']);
70
70
  assert.deepStrictEqual(inspect(parser('# a [#b ]')), [['<h1 id="index:b">a<span class="indexer" data-index="b"></span></h1>'], '']);
71
71
  assert.deepStrictEqual(inspect(parser('# a [#b c]')), [['<h1 id="index:b_c">a<span class="indexer" data-index="b_c"></span></h1>'], '']);
72
- assert.deepStrictEqual(inspect(parser('# a [#*b*`c`${d}$]')), [['<h1 id="index:b`c`${d}$">a<span class="indexer" data-index="b`c`${d}$"></span></h1>'], '']);
72
+ assert.deepStrictEqual(inspect(parser('# a [#_b_`c`${d}$]')), [['<h1 id="index:b`c`${d}$">a<span class="indexer" data-index="b`c`${d}$"></span></h1>'], '']);
73
73
  assert.deepStrictEqual(inspect(parser('# a [#@a]')), [['<h1 id="index:@a">a<span class="indexer" data-index="@a"></span></h1>'], '']);
74
74
  assert.deepStrictEqual(inspect(parser('# a [#http://host]')), [['<h1 id="index:http://host">a<span class="indexer" data-index="http://host"></span></h1>'], '']);
75
75
  assert.deepStrictEqual(inspect(parser('# a [#!http://host]')), [['<h1 id="index:!http://host">a<span class="indexer" data-index="!http://host"></span></h1>'], '']);
@@ -27,11 +27,8 @@ describe('Unit: parser/block/paragraph', () => {
27
27
  assert.deepStrictEqual(inspect(parser('<wbr>\na')), [['<p>&lt;wbr&gt;<br>a</p>'], '']);
28
28
  assert.deepStrictEqual(inspect(parser('a\n<wbr>\n')), [['<p>a<br>&lt;wbr&gt;</p>'], '']);
29
29
  assert.deepStrictEqual(inspect(parser('a\n<wbr>\nb')), [['<p>a<br>&lt;wbr&gt;<br>b</p>'], '']);
30
+ assert.deepStrictEqual(inspect(parser('_a\n<wbr>_\nb')), [['<p>_a<br><wbr>_<br>b</p>'], '']);
30
31
  assert.deepStrictEqual(inspect(parser('*a\n<wbr>*\nb')), [['<p>*a<br><wbr>*<br>b</p>'], '']);
31
- assert.deepStrictEqual(inspect(parser('**a\n<wbr>**\nb')), [['<p>**a<br><wbr>**<br>b</p>'], '']);
32
- assert.deepStrictEqual(inspect(parser('***a\n<wbr>***\nb')), [['<p>***a<br><wbr>***<br>b</p>'], '']);
33
- assert.deepStrictEqual(inspect(parser('***a*b\n<wbr>**\nc')), [['<p>**<em>a</em>b<br><wbr>**<br>c</p>'], '']);
34
- assert.deepStrictEqual(inspect(parser('***a**b\n<wbr>*\nc')), [['<p>*<strong>a</strong>b<br><wbr>*<br>c</p>'], '']);
35
32
  assert.deepStrictEqual(inspect(parser('==a\n<wbr>==\nb')), [['<p>==a<br><wbr>==<br>b</p>'], '']);
36
33
  assert.deepStrictEqual(inspect(parser('\ta')), [['<p>\ta</p>'], '']);
37
34
  });
@@ -39,11 +39,16 @@ describe('Unit: parser/block/reply/cite', () => {
39
39
  assert.deepStrictEqual(inspect(parser('>>>0\n>>')), [['<span class="cite">&gt;&gt;<a class="anchor" href="?at=0" data-depth="2">&gt;0</a></span>', '<br>'], '>>']);
40
40
  assert.deepStrictEqual(inspect(parser('>>>0\n>>1')), [['<span class="cite">&gt;&gt;<a class="anchor" href="?at=0" data-depth="2">&gt;0</a></span>', '<br>', '<span class="cite">&gt;<a class="anchor" href="?at=1" data-depth="1">&gt;1</a></span>', '<br>'], '']);
41
41
  assert.deepStrictEqual(inspect(parser('>>.')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;.</a></span>', '<br>'], '']);
42
+ assert.deepStrictEqual(inspect(parser('>>. ')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;.</a></span>', '<br>'], '']);
43
+ assert.deepStrictEqual(inspect(parser('>>.\n')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;.</a></span>', '<br>'], '']);
42
44
  assert.deepStrictEqual(inspect(parser('>>#')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;#</a></span>', '<br>'], '']);
45
+ assert.deepStrictEqual(inspect(parser('>>#\n')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;#</a></span>', '<br>'], '']);
43
46
  assert.deepStrictEqual(inspect(parser('>>#a')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;#a</a></span>', '<br>'], '']);
44
47
  assert.deepStrictEqual(inspect(parser('>>#index:a')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;#index:a</a></span>', '<br>'], '']);
45
48
  assert.deepStrictEqual(inspect(parser('>>#:~:text=a')), [['<span class="cite">&gt;<a class="anchor" data-depth="1">&gt;#:~:text=a</a></span>', '<br>'], '']);
46
49
  assert.deepStrictEqual(inspect(parser('>>http://host')), [['<span class="cite">&gt;<a class="anchor" href="http://host" target="_blank" data-depth="1">&gt;http://host</a></span>', '<br>'], '']);
50
+ assert.deepStrictEqual(inspect(parser('>>http://host ')), [['<span class="cite">&gt;<a class="anchor" href="http://host" target="_blank" data-depth="1">&gt;http://host</a></span>', '<br>'], '']);
51
+ assert.deepStrictEqual(inspect(parser('>>http://host\n')), [['<span class="cite">&gt;<a class="anchor" href="http://host" target="_blank" data-depth="1">&gt;http://host</a></span>', '<br>'], '']);
47
52
  assert.deepStrictEqual(inspect(parser('>>https://host')), [['<span class="cite">&gt;<a class="anchor" href="https://host" target="_blank" data-depth="1">&gt;https://host</a></span>', '<br>'], '']);
48
53
  });
49
54
 
@@ -7,14 +7,15 @@ import { html, define, defrag } from 'typed-dom/dom';
7
7
  export const cite: ReplyParser.CiteParser = creation(1, false, line(fmap(validate(
8
8
  '>>',
9
9
  reverse(tails([
10
- str(/^>*(?=>>[^>\s]+[^\S\n]*(?:$|\n))/),
10
+ str(/^>*(?=>>[^>\s]+\s*$)/),
11
11
  union([
12
12
  anchor,
13
13
  // Subject page representation.
14
14
  // リンクの実装は後で検討
15
- focus(/^>>\.[^\S\n]*(?:$|\n)/, () => [[html('a', { class: 'anchor' }, '>>.')], '']),
16
- focus(/^>>#\S*[^\S\n]*(?:$|\n)/, ({ source }) => [[html('a', { class: 'anchor' }, source)], '']),
17
- focus(/^>>https?:\/\/\w\S*[^\S\n]*(?:$|\n)/, ({ source }) => [[html('a', { class: 'anchor', href: source.slice(2).trimEnd(), target: '_blank' }, source)], '']),
15
+ focus(/^>>\.(?=\s*$)/, () => [[html('a', { class: 'anchor' }, '>>.')], '']),
16
+ focus(/^>>#\S*(?=\s*$)/, ({ source }) => [[html('a', { class: 'anchor' }, source)], '']),
17
+ // Support all domains, but don't support IP(v6) addresses.
18
+ focus(/^>>https?:\/\/[^\p{C}\p{S}\p{P}\s]\S*(?=\s*$)/u, ({ source }) => [[html('a', { class: 'anchor', href: source.slice(2).trimEnd(), target: '_blank' }, source)], '']),
18
19
  ]),
19
20
  ]))),
20
21
  ([el, quotes = '']: [HTMLElement, string?]) => [
@@ -1,8 +1,9 @@
1
1
  export const enum Syntax {
2
- reference = 1 << 12,
3
- comment = 1 << 11,
4
- index = 1 << 10,
5
- placeholder = 1 << 9,
2
+ reference = 1 << 13,
3
+ comment = 1 << 12,
4
+ index = 1 << 11,
5
+ placeholder = 1 << 10,
6
+ ruby = 1 << 9,
6
7
  link = 1 << 8,
7
8
  bracket = 1 << 7,
8
9
  media = 1 << 6,
@@ -25,15 +26,15 @@ export const enum State {
25
26
  autolink = 1 << 1,
26
27
  shortcut = 1 << 0,
27
28
  none = 0,
28
- linkable = 0
29
+ all = ~0,
30
+ linkers = 0
29
31
  | State.annotation
30
32
  | State.reference
31
33
  | State.index
32
34
  | State.label
33
35
  | State.link
34
- | State.media
35
36
  | State.autolink,
36
- backtrackable = 0
37
+ backtrackers = 0
37
38
  | State.annotation
38
39
  | State.reference
39
40
  | State.index
@@ -16,6 +16,7 @@ describe('Unit: parser/inline/autolink/anchor', () => {
16
16
  assert.deepStrictEqual(inspect(parser('>>https://host')), undefined);
17
17
  assert.deepStrictEqual(inspect(parser('>>tel:1234567890')), undefined);
18
18
  assert.deepStrictEqual(inspect(parser('>>>')), undefined);
19
+ assert.deepStrictEqual(inspect(parser('a>>0')), [['a>>0'], '']);
19
20
  assert.deepStrictEqual(inspect(parser(' >>0')), undefined);
20
21
  });
21
22
 
@@ -8,6 +8,9 @@ describe('Unit: parser/inline/autolink/email', () => {
8
8
 
9
9
  it('invalid', () => {
10
10
  assert.deepStrictEqual(inspect(parser('')), undefined);
11
+ assert.deepStrictEqual(inspect(parser('a_b')), [['a'], '_b']);
12
+ assert.deepStrictEqual(inspect(parser('a_b_c')), [['a'], '_b_c']);
13
+ assert.deepStrictEqual(inspect(parser(`a_${'b'.repeat(255 - 2)}@`)), [[`a_${'b'.repeat(255 - 2)}@`], '']);
11
14
  assert.deepStrictEqual(inspect(parser('a@')), [['a@'], '']);
12
15
  assert.deepStrictEqual(inspect(parser('a@+')), [['a@'], '+']);
13
16
  assert.deepStrictEqual(inspect(parser('a@_')), [['a@'], '_']);
@@ -6,6 +6,6 @@ import { html } from 'typed-dom/dom';
6
6
  // https://html.spec.whatwg.org/multipage/input.html
7
7
 
8
8
  export const email: AutolinkParser.EmailParser = creation(rewrite(verify(
9
- str(/^[0-9a-z](?:[.+_-](?=[^\W_])|[0-9a-z]){0,255}@[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*(?![0-9a-z])/i),
9
+ str(/^[0-9a-z](?:[_.+-](?=[0-9a-z])|[0-9a-z]){0,255}@[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*(?![0-9a-z])/i),
10
10
  ([source]) => source.length <= 255),
11
11
  ({ source }) => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], '']));
@@ -20,8 +20,6 @@ describe('Unit: parser/inline/autolink/hashnum', () => {
20
20
  assert.deepStrictEqual(inspect(parser('##')), [['##'], '']);
21
21
  assert.deepStrictEqual(inspect(parser('##1')), [['##1'], '']);
22
22
  assert.deepStrictEqual(inspect(parser('###1')), [['###1'], '']);
23
- assert.deepStrictEqual(inspect(parser(`#1'`)), [[`#1'`], '']);
24
- assert.deepStrictEqual(inspect(parser(`#1''`)), [[`#1''`], '']);
25
23
  assert.deepStrictEqual(inspect(parser('#{}')), [['#'], '{}']);
26
24
  assert.deepStrictEqual(inspect(parser('#{{}')), [['#'], '{{}']);
27
25
  assert.deepStrictEqual(inspect(parser('#{}}')), [['#'], '{}}']);
@@ -40,6 +38,7 @@ describe('Unit: parser/inline/autolink/hashnum', () => {
40
38
 
41
39
  it('valid', () => {
42
40
  assert.deepStrictEqual(inspect(parser('#1')), [['<a class="hashnum">#1</a>'], '']);
41
+ assert.deepStrictEqual(inspect(parser('#1_')), [['<a class="hashnum">#1</a>'], '_']);
43
42
  assert.deepStrictEqual(inspect(parser('#1 ')), [['<a class="hashnum">#1</a>'], ' ']);
44
43
  assert.deepStrictEqual(inspect(parser('#1\n')), [['<a class="hashnum">#1</a>'], '\n']);
45
44
  assert.deepStrictEqual(inspect(parser('#1\\')), [['<a class="hashnum">#1</a>'], '\\']);
@@ -8,7 +8,7 @@ import { define } from 'typed-dom/dom';
8
8
 
9
9
  export const hashnum: AutolinkParser.HashnumParser = lazy(() => fmap(rewrite(
10
10
  constraint(State.shortcut, false,
11
- open('#', str(new RegExp(/^[0-9]{1,16}(?![^\p{C}\p{S}\p{P}\s]|emoji|['_])/u.source.replace(/emoji/, emoji), 'u')))),
11
+ open('#', str(new RegExp(/^[0-9]{1,16}(?![^\p{C}\p{S}\p{P}\s]|emoji|')/u.source.replace(/emoji/, emoji), 'u')))),
12
12
  convert(
13
13
  source => `[${source}]{ ${source.slice(1)} }`,
14
14
  union([unsafelink]))),
@@ -20,9 +20,8 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
20
20
  assert.deepStrictEqual(inspect(parser('##')), [['##'], '']);
21
21
  assert.deepStrictEqual(inspect(parser('##a')), [['##a'], '']);
22
22
  assert.deepStrictEqual(inspect(parser('###a')), [['###a'], '']);
23
- assert.deepStrictEqual(inspect(parser(`#'`)), [[`#'`], '']);
24
- assert.deepStrictEqual(inspect(parser(`#a''`)), [[`#a''`], '']);
25
- assert.deepStrictEqual(inspect(parser('#_')), [['#_'], '']);
23
+ assert.deepStrictEqual(inspect(parser('#_')), [['#'], '_']);
24
+ assert.deepStrictEqual(inspect(parser('#_a')), [['#'], '_a']);
26
25
  assert.deepStrictEqual(inspect(parser('#(a)')), [['#'], '(a)']);
27
26
  assert.deepStrictEqual(inspect(parser('#{}')), [['#'], '{}']);
28
27
  assert.deepStrictEqual(inspect(parser('#{{}')), [['#'], '{{}']);
@@ -35,9 +34,9 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
35
34
  assert.deepStrictEqual(inspect(parser('a##1')), [['a##1'], '']);
36
35
  assert.deepStrictEqual(inspect(parser('a##b')), [['a##b'], '']);
37
36
  assert.deepStrictEqual(inspect(parser('あ#b')), [['あ#b'], '']);
38
- assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_`)), [[`#${'1'.repeat(127)}_`], '']);
39
- assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_a`)), [[`#${'1'.repeat(127)}_a`], '']);
40
- assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_'a`)), [[`#${'1'.repeat(127)}_'a`], '']);
37
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_`)), [[`#${'1'.repeat(127)}`], '_']);
38
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_a`)), [[`#${'1'.repeat(127)}`], '_a']);
39
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(127)}_'a`)), [[`#${'1'.repeat(127)}`], `_'a`]);
41
40
  assert.deepStrictEqual(inspect(parser(' #a')), undefined);
42
41
  });
43
42
 
@@ -52,9 +51,8 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
52
51
  assert.deepStrictEqual(inspect(parser('#a(b')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '(b']);
53
52
  assert.deepStrictEqual(inspect(parser('#a(b)')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '(b)']);
54
53
  assert.deepStrictEqual(inspect(parser('#a_')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '_']);
55
- assert.deepStrictEqual(inspect(parser('#a_b')), [['<a class="hashtag" href="/hashtags/a_b">#a_b</a>'], '']);
56
- assert.deepStrictEqual(inspect(parser(`#a_'b`)), [['<a class="hashtag" href="/hashtags/a">#a</a>'], `_'b`]);
57
54
  assert.deepStrictEqual(inspect(parser('#a__b')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '__b']);
55
+ assert.deepStrictEqual(inspect(parser('#a_b')), [['<a class="hashtag" href="/hashtags/a_b">#a_b</a>'], '']);
58
56
  assert.deepStrictEqual(inspect(parser('#あ')), [['<a class="hashtag" href="/hashtags/あ">#あ</a>'], '']);
59
57
  assert.deepStrictEqual(inspect(parser('#👩')), [['<a class="hashtag" href="/hashtags/👩">#👩</a>'], '']);
60
58
  assert.deepStrictEqual(inspect(parser('#1a')), [['<a class="hashtag" href="/hashtags/1a">#1a</a>'], '']);
@@ -62,10 +60,15 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
62
60
  assert.deepStrictEqual(inspect(parser('#1👩')), [['<a class="hashtag" href="/hashtags/1👩">#1👩</a>'], '']);
63
61
  assert.deepStrictEqual(inspect(parser('#domain/a')), [['<a class="hashtag" href="https://domain/hashtags/a" target="_blank">#domain/a</a>'], '']);
64
62
  assert.deepStrictEqual(inspect(parser('#domain.co.jp/a')), [['<a class="hashtag" href="https://domain.co.jp/hashtags/a" target="_blank">#domain.co.jp/a</a>'], '']);
65
- // Reserved
66
- assert.deepStrictEqual(inspect(parser(`#a'`)), [[`#a'`], '']);
67
- assert.deepStrictEqual(inspect(parser(`#a'b`)), [[`#a'b`], '']);
68
- assert.deepStrictEqual(inspect(parser(`#a'_b`)), [[`#a'_b`], '']);
63
+ assert.deepStrictEqual(inspect(parser(`#'0`)), [[`<a class="hashtag" href="/hashtags/'0">#'0</a>`], '']);
64
+ assert.deepStrictEqual(inspect(parser(`#'00`)), [[`<a class="hashtag" href="/hashtags/'00">#'00</a>`], '']);
65
+ assert.deepStrictEqual(inspect(parser(`#1'`)), [[`<a class="hashtag" href="/hashtags/1'">#1'</a>`], '']);
66
+ assert.deepStrictEqual(inspect(parser(`#1''`)), [[`<a class="hashtag" href="/hashtags/1'">#1'</a>`], `'`]);
67
+ assert.deepStrictEqual(inspect(parser(`#a'`)), [[`<a class="hashtag" href="/hashtags/a'">#a'</a>`], '']);
68
+ assert.deepStrictEqual(inspect(parser(`#a''`)), [[`<a class="hashtag" href="/hashtags/a'">#a'</a>`], `'`]);
69
+ assert.deepStrictEqual(inspect(parser(`#a'b`)), [[`<a class="hashtag" href="/hashtags/a'b">#a'b</a>`], '']);
70
+ assert.deepStrictEqual(inspect(parser(`#a'_b`)), [[`<a class="hashtag" href="/hashtags/a'_b">#a'_b</a>`], '']);
71
+ assert.deepStrictEqual(inspect(parser(`#a_'b`)), [[`<a class="hashtag" href="/hashtags/a_'b">#a_'b</a>`], '']);
69
72
  });
70
73
 
71
74
  });
@@ -20,9 +20,9 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
20
20
  ([source]) => source.length <= 253 + 1),
21
21
  verify(
22
22
  str(new RegExp([
23
- /^(?=[0-9]{0,127}_?(?:[^\d\p{C}\p{S}\p{P}\s]|emoji))/u.source,
24
- /(?:[^\p{C}\p{S}\p{P}\s]|emoji|_(?=[^\p{C}\p{S}\p{P}\s]|emoji)){1,128}/u.source,
25
- /(?!_?(?:[^\p{C}\p{S}\p{P}\s]|emoji)|')/u.source,
23
+ /^(?=(?:[0-9]{1,127}_?)?(?:[^\d\p{C}\p{S}\p{P}\s]|emoji|'))/u.source,
24
+ /(?:[^\p{C}\p{S}\p{P}\s]|emoji|(?<!')'|_(?=[^\p{C}\p{S}\p{P}\s]|emoji|')){1,128}/u.source,
25
+ /(?!_?(?:[^\p{C}\p{S}\p{P}\s]|emoji|(?<!')'))/u.source,
26
26
  ].join('').replace(/emoji/g, emoji), 'u')),
27
27
  ([source]) => source.length <= 128),
28
28
  ]))),
@@ -58,7 +58,7 @@ describe('Unit: parser/inline/autolink/url', () => {
58
58
  assert.deepStrictEqual(inspect(parser('http://host`')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '`']);
59
59
  assert.deepStrictEqual(inspect(parser('http://host|')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '|']);
60
60
  assert.deepStrictEqual(inspect(parser('http://host&')), [['<a class="url" href="http://host&amp;" target="_blank">http://host&amp;</a>'], '']);
61
- assert.deepStrictEqual(inspect(parser('http://host_')), [['<a class="url" href="http://host_" target="_blank">http://host_</a>'], '']);
61
+ assert.deepStrictEqual(inspect(parser('http://host_')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '_']);
62
62
  assert.deepStrictEqual(inspect(parser('http://host$')), [['<a class="url" href="http://host$" target="_blank">http://host$</a>'], '']);
63
63
  assert.deepStrictEqual(inspect(parser('http://user@host')), [['<a class="url" href="http://user@host" target="_blank">http://user@host</a>'], '']);
64
64
  assert.deepStrictEqual(inspect(parser('http://host#@')), [['<a class="url" href="http://host#@" target="_blank">http://host#@</a>'], '']);
@@ -3,7 +3,7 @@ import { union, some, creation, precedence, validate, focus, rewrite, convert, s
3
3
  import { unsafelink } from '../link';
4
4
  import { unescsource } from '../../source';
5
5
 
6
- const closer = /^[-+*=~^,.;:!?]*(?=[\\"`|\[\](){}<>]|$)/;
6
+ const closer = /^[-+*=~^_,.;:!?]*(?=[\\"`|\[\](){}<>]|$)/;
7
7
 
8
8
  export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'https://'], rewrite(
9
9
  open(