securemark 0.230.0 → 0.231.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/securemark.js +113 -85
  3. package/gulpfile.js +21 -1
  4. package/markdown.d.ts +0 -2
  5. package/package-lock.json +952 -225
  6. package/package.json +9 -7
  7. package/src/debug.test.ts +1 -0
  8. package/src/parser/api/header.ts +1 -1
  9. package/src/parser/autolink.test.ts +3 -3
  10. package/src/parser/block/codeblock.ts +1 -1
  11. package/src/parser/block/extension/aside.ts +1 -1
  12. package/src/parser/block/extension/example.ts +1 -1
  13. package/src/parser/block/extension/message.ts +1 -1
  14. package/src/parser/block/extension/placeholder.ts +1 -1
  15. package/src/parser/block/extension/table.ts +1 -1
  16. package/src/parser/block/mathblock.ts +1 -1
  17. package/src/parser/block/olist.test.ts +4 -0
  18. package/src/parser/block/olist.ts +6 -6
  19. package/src/parser/block/paragraph/mention/quote.ts +1 -1
  20. package/src/parser/block/ulist.ts +1 -1
  21. package/src/parser/inline/autolink/account.ts +1 -1
  22. package/src/parser/inline/autolink/email.test.ts +2 -1
  23. package/src/parser/inline/autolink/email.ts +1 -1
  24. package/src/parser/inline/autolink/hashnum.test.ts +1 -0
  25. package/src/parser/inline/autolink/hashnum.ts +2 -1
  26. package/src/parser/inline/autolink/hashtag.test.ts +13 -12
  27. package/src/parser/inline/autolink/hashtag.ts +9 -2
  28. package/src/parser/inline/autolink.ts +5 -8
  29. package/src/parser/inline/bracket.ts +1 -4
  30. package/src/parser/inline/comment.test.ts +2 -0
  31. package/src/parser/inline/comment.ts +2 -2
  32. package/src/parser/inline/html.ts +1 -1
  33. package/src/parser/inline/link.ts +2 -2
  34. package/src/parser/inline/ruby.ts +2 -2
  35. package/src/parser/inline.test.ts +5 -3
  36. package/src/parser/source/text.ts +3 -3
  37. package/src/parser/source/unescapable.ts +1 -1
  38. package/src/parser/util.ts +2 -2
  39. package/src/renderer/render/media/youtube.ts +4 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.230.0",
3
+ "version": "0.231.1",
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",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/dompurify": "2.3.3",
34
- "@types/jquery": "3.5.13",
34
+ "@types/jquery": "3.5.14",
35
35
  "@types/mathjax": "0.0.37",
36
36
  "@types/mocha": "9.1.0",
37
37
  "@types/power-assert": "1.5.8",
@@ -40,15 +40,17 @@
40
40
  "browserify-shim": "^3.8.14",
41
41
  "concurrently": "^7.0.0",
42
42
  "del": "^6.0.0",
43
+ "eslint-plugin-redos": "^4.3.0",
43
44
  "gulp": "^4.0.2",
44
45
  "gulp-derequire": "^3.0.0",
46
+ "gulp-eslint": "^6.0.0",
45
47
  "gulp-footer": "^2.1.0",
46
48
  "gulp-header": "^2.0.9",
47
49
  "gulp-load-plugins": "^2.0.7",
48
50
  "gulp-mocha": "^8.0.0",
49
51
  "gulp-rename": "^2.0.0",
50
52
  "gulp-unassert": "^2.0.0",
51
- "karma": "^6.3.16",
53
+ "karma": "^6.3.17",
52
54
  "karma-chrome-launcher": "^3.1.0",
53
55
  "karma-coverage-istanbul-instrumenter": "^1.0.4",
54
56
  "karma-coverage-istanbul-reporter": "^3.0.3",
@@ -56,13 +58,13 @@
56
58
  "karma-firefox-launcher": "^2.1.2",
57
59
  "karma-mocha": "^2.0.1",
58
60
  "mocha": "^9.2.1",
59
- "npm-check-updates": "^12.4.0",
61
+ "npm-check-updates": "^12.5.2",
60
62
  "power-assert": "^1.6.1",
61
63
  "semver": "^7.3.5",
62
- "spica": "0.0.510",
64
+ "spica": "0.0.511",
63
65
  "tsify": "^5.0.4",
64
- "typed-dom": "0.0.248",
65
- "typescript": "4.5.5",
66
+ "typed-dom": "0.0.249",
67
+ "typescript": "4.6.2",
66
68
  "vinyl-buffer": "^1.0.1",
67
69
  "vinyl-source-stream": "^2.0.0"
68
70
  },
package/src/debug.test.ts CHANGED
@@ -21,6 +21,7 @@ export function inspect(result: Result<HTMLElement | string>, until: number | st
21
21
  el.innerHTML = node.outerHTML.slice(0, until);
22
22
  if (node.outerHTML.length <= until) {
23
23
  assert(node.outerHTML === el.innerHTML);
24
+ // eslint-disable-next-line redos/no-vulnerable
24
25
  assert(node.childNodes.length === el.firstChild?.childNodes.length || />[^<]{65537}/.test(node.outerHTML));
25
26
  }
26
27
  else {
@@ -8,7 +8,7 @@ export function header(source: string): string {
8
8
 
9
9
  export function headers(source: string): string[] {
10
10
  const [el] = parse(source);
11
- return el?.textContent!.trimEnd().slice(el.firstChild!.textContent!.length).split(/[^\S\n]*\n/) ?? [];
11
+ return el?.textContent!.trimEnd().slice(el.firstChild!.textContent!.length).split('\n') ?? [];
12
12
  }
13
13
 
14
14
  function parse(source: string): [HTMLDetailsElement, string] | [] {
@@ -13,11 +13,11 @@ describe('Unit: parser/autolink', () => {
13
13
  assert.deepStrictEqual(inspect(parser('@a#b')), [['<a href="/@a?ch=b" class="channel">@a#b</a>'], '']);
14
14
  assert.deepStrictEqual(inspect(parser('\\\n')), [['\\', '<br>'], '']);
15
15
  assert.deepStrictEqual(inspect(parser('a#b')), [['a#b'], '']);
16
- assert.deepStrictEqual(inspect(parser('0a#b')), [['0a#b'], '']);
16
+ assert.deepStrictEqual(inspect(parser('0a#b')), [['0', 'a#b'], '']);
17
17
  assert.deepStrictEqual(inspect(parser('あ#b')), [['あ#b'], '']);
18
- assert.deepStrictEqual(inspect(parser('あい#b')), [['あい#b'], '']);
18
+ assert.deepStrictEqual(inspect(parser('あい#b')), [['あ', 'い#b'], '']);
19
19
  assert.deepStrictEqual(inspect(parser('0aあ#b')), [['0a', 'あ#b'], '']);
20
- assert.deepStrictEqual(inspect(parser('0aあい#b')), [['0a', 'あい#b'], '']);
20
+ assert.deepStrictEqual(inspect(parser('0aあい#b')), [['0a', 'あ', 'い#b'], '']);
21
21
  assert.deepStrictEqual(inspect(parser('a\n#b')), [['a', '<br>', '<a href="/hashtags/b" class="hashtag">#b</a>'], '']);
22
22
  assert.deepStrictEqual(inspect(parser('a\\\n#b')), [['a', '\\', '<br>', '<a href="/hashtags/b" class="hashtag">#b</a>'], '']);
23
23
  });
@@ -30,7 +30,7 @@ export const codeblock: CodeBlockParser = block(validate('```', fmap(
30
30
  translate: 'no',
31
31
  'data-invalid-syntax': 'codeblock',
32
32
  'data-invalid-type': !closer ? 'closer' : 'argument',
33
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
33
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
34
34
  }, `${opener}${body}${closer}`)];
35
35
  const file = path.split('/').pop() ?? '';
36
36
  const ext = file && file.includes('.', 1)
@@ -13,7 +13,7 @@ export const aside: ExtensionParser.AsideParser = creator(100, block(validate('~
13
13
  translate: 'no',
14
14
  'data-invalid-syntax': 'aside',
15
15
  'data-invalid-type': !closer ? 'closer' : 'argument',
16
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
16
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
17
17
  }, `${opener}${body}${closer}`)];
18
18
  const annotations = html('ol', { class: 'annotations' });
19
19
  const references = html('ol', { class: 'references' });
@@ -16,7 +16,7 @@ export const example: ExtensionParser.ExampleParser = creator(100, block(validat
16
16
  translate: 'no',
17
17
  'data-invalid-syntax': 'example',
18
18
  'data-invalid-type': !closer ? 'closer' : 'argument',
19
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
19
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
20
20
  }, `${opener}${body}${closer}`)];
21
21
  switch (type) {
22
22
  case 'markdown': {
@@ -25,7 +25,7 @@ export const message: MessageParser = block(validate('~~~', fmap(
25
25
  translate: 'no',
26
26
  'data-invalid-syntax': 'message',
27
27
  'data-invalid-type': !closer ? 'closer' : 'argument',
28
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
28
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
29
29
  }, `${opener}${body}${closer}`)];
30
30
  switch (type) {
31
31
  case 'note':
@@ -18,6 +18,6 @@ export const placeholder: ExtensionParser.PlaceholderParser = block(validate('~~
18
18
  translate: 'no',
19
19
  'data-invalid-syntax': 'extension',
20
20
  'data-invalid-type': !closer ? 'closer' : 'syntax',
21
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid syntax.',
21
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid syntax.',
22
22
  }, `${opener}${body}${closer}`)
23
23
  ])));
@@ -32,7 +32,7 @@ export const table: TableParser = block(validate('~~~', recover(bind(
32
32
  translate: 'no',
33
33
  'data-invalid-syntax': 'table',
34
34
  'data-invalid-type': !closer ? 'closer' : 'argument',
35
- 'data-invalid-description': !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
35
+ 'data-invalid-description': !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
36
36
  }, `${opener}${body}${closer}`)], ''];
37
37
  return parser(body, context) ?? [[html('table')], ''];
38
38
  }),
@@ -23,6 +23,6 @@ export const mathblock: MathBlockParser = block(validate('$$', fmap(
23
23
  translate: 'no',
24
24
  'data-invalid-syntax': 'mathblock',
25
25
  'data-invalid-type': delim.length > 2 ? 'syntax' : !closer ? 'closer' : 'argument',
26
- 'data-invalid-description': delim.length > 2 ? 'Invalid syntax' : !closer ? `Missing the closing delimiter ${delim}.` : 'Invalid argument.',
26
+ 'data-invalid-description': delim.length > 2 ? 'Invalid syntax' : !closer ? `Missing the closing delimiter "${delim}".` : 'Invalid argument.',
27
27
  }, `${opener}${body}${closer}`),
28
28
  ])));
@@ -50,6 +50,7 @@ describe('Unit: parser/block/olist', () => {
50
50
  it('multiple', () => {
51
51
  // pending
52
52
  assert.deepStrictEqual(inspect(parser('0.\n0')), [['<ol><li></li><li></li></ol>'], '']);
53
+ assert.deepStrictEqual(inspect(parser('0.\n0\n')), [['<ol><li></li><li></li></ol>'], '']);
53
54
  assert.deepStrictEqual(inspect(parser('0.\n0.')), [['<ol><li></li><li></li></ol>'], '']);
54
55
  assert.deepStrictEqual(inspect(parser('0.\n0. ')), [['<ol><li></li><li></li></ol>'], '']);
55
56
  assert.deepStrictEqual(inspect(parser('0.\n0.\n')), [['<ol><li></li><li></li></ol>'], '']);
@@ -58,8 +59,11 @@ describe('Unit: parser/block/olist', () => {
58
59
  assert.deepStrictEqual(inspect(parser('0. 1\n0. 2\n0. 3')), [['<ol><li>1</li><li>2</li><li>3</li></ol>'], '']);
59
60
  // pending
60
61
  assert.deepStrictEqual(inspect(parser('(1) \n(')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
62
+ assert.deepStrictEqual(inspect(parser('(1) \n(\n')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
61
63
  assert.deepStrictEqual(inspect(parser('(1) \n(1')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
64
+ assert.deepStrictEqual(inspect(parser('(1) \n(1\n')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
62
65
  assert.deepStrictEqual(inspect(parser('(1) \n(1)')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
66
+ assert.deepStrictEqual(inspect(parser('(1) \n(1)\n')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
63
67
  // filled
64
68
  assert.deepStrictEqual(inspect(parser('(1) \n(1) ')), [['<ol data-format="paren"><li></li><li></li></ol>'], '']);
65
69
  // invalid
@@ -16,8 +16,8 @@ const openers = {
16
16
 
17
17
  export const olist: OListParser = lazy(() => block(validate(
18
18
  [
19
- /^(?=([0-9]+|[a-z]+|[A-Z]+)(?:-[0-9]+)*\.(?=[^\S\n]|\n[^\S\n]*\S))/,
20
- /^(?=\(([0-9]+|[a-z]+)\)(?:-[0-9]+)*(?=[^\S\n]|\n[^\S\n]*\S))/,
19
+ /^([0-9]+|[a-z]+|[A-Z]+)(?:-[0-9]+)*\.(?=[^\S\n]|\n[^\S\n]*\S)/,
20
+ /^\(([0-9]+|[a-z]+)\)(?:-[0-9]+)*(?=[^\S\n]|\n[^\S\n]*\S)/,
21
21
  ],
22
22
  context({ syntax: { inline: { media: false } } },
23
23
  olist_))));
@@ -35,7 +35,7 @@ const list = (type: string, delim: string): OListParser.ListParser => fmap(
35
35
  some(creator(union([
36
36
  fmap(fallback(
37
37
  inits([
38
- line(open(items[delim], trim(subsequence([checkbox, trimStart(some(inline))])), true)),
38
+ line(open(heads[delim], trim(subsequence([checkbox, trimStart(some(inline))])), true)),
39
39
  indent(union([ulist_, olist_, ilist_])),
40
40
  ]),
41
41
  iitem),
@@ -43,13 +43,13 @@ const list = (type: string, delim: string): OListParser.ListParser => fmap(
43
43
  ]))),
44
44
  es => [format(html('ol', es), type, delim)]);
45
45
 
46
- const items = {
46
+ const heads = {
47
47
  '.': focus(
48
48
  openers['.'],
49
49
  (source: string) => [[`${source.split('.', 1)[0]}.`], '']),
50
50
  '(': focus(
51
51
  openers['('],
52
- (source: string) => [[source.trimEnd().replace(/^\($/, '(1)').replace(/^\((\w+)\)?$/, '($1)')], '']),
52
+ (source: string) => [[source.replace(/^\($/, '(1)').replace(/^\((\w+)$/, '($1)')], '']),
53
53
  } as const;
54
54
 
55
55
  const iitem = rewrite(contentline, source => [[
@@ -58,7 +58,7 @@ const iitem = rewrite(contentline, source => [[
58
58
  class: 'invalid',
59
59
  'data-invalid-syntax': 'listitem',
60
60
  'data-invalid-type': 'syntax',
61
- 'data-invalid-description': 'Fix the indent or the head of list items.',
61
+ 'data-invalid-description': 'Fix the indent or the head of the list item.',
62
62
  }, source.replace('\n', ''))
63
63
  ], '']);
64
64
 
@@ -26,7 +26,7 @@ export const quote: ParagraphParser.MentionParser.QuoteParser = lazy(() => creat
26
26
  class: 'quote invalid',
27
27
  'data-invalid-syntax': 'quote',
28
28
  'data-invalid-type': 'syntax',
29
- 'data-invalid-description': `Missing the whitespace after ${ns[0].split(/[^>]/, 1)[0]}.`,
29
+ 'data-invalid-description': `Missing the whitespace after "${ns[0].split(/[^>]/, 1)[0]}".`,
30
30
  },
31
31
  ns),
32
32
  html('br'),
@@ -36,7 +36,7 @@ const iitem = rewrite(contentline, source => [[
36
36
  class: 'invalid',
37
37
  'data-invalid-syntax': 'listitem',
38
38
  'data-invalid-type': 'syntax',
39
- 'data-invalid-description': 'Fix the indent or the head of list items.',
39
+ 'data-invalid-description': 'Fix the indent or the head of the list item.',
40
40
  }, source.replace('\n', ''))
41
41
  ], '']);
42
42
 
@@ -11,7 +11,7 @@ export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
11
11
  '@',
12
12
  tails([
13
13
  verify(
14
- str(/^[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?)*\//),
14
+ str(/^[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//),
15
15
  ([source]) => source.length <= 253 + 1),
16
16
  verify(
17
17
  str(/^[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/),
@@ -23,6 +23,7 @@ describe('Unit: parser/inline/autolink/email', () => {
23
23
  assert.deepStrictEqual(inspect(parser('a+@b')), undefined);
24
24
  assert.deepStrictEqual(inspect(parser('a..b@c')), undefined);
25
25
  assert.deepStrictEqual(inspect(parser('a++b@c')), undefined);
26
+ assert.deepStrictEqual(inspect(parser(`a@${'b'.repeat(64)}`)), [[`a@${'b'.repeat(64)}`], '']);
26
27
  assert.deepStrictEqual(inspect(parser(' a@b')), undefined);
27
28
  });
28
29
 
@@ -36,7 +37,7 @@ describe('Unit: parser/inline/autolink/email', () => {
36
37
  assert.deepStrictEqual(inspect(parser('a@b_c')), [['<a class="email" href="mailto:a@b">a@b</a>'], '_c']);
37
38
  assert.deepStrictEqual(inspect(parser('a@b-')), [['<a class="email" href="mailto:a@b">a@b</a>'], '-']);
38
39
  assert.deepStrictEqual(inspect(parser('a@b-c')), [['<a class="email" href="mailto:a@b-c">a@b-c</a>'], '']);
39
- assert.deepStrictEqual(inspect(parser('a@b--c')), [['<a class="email" href="mailto:a@b--c">a@b--c</a>'], '']);
40
+ assert.deepStrictEqual(inspect(parser('a@b--c')), [['<a class="email" href="mailto:a@b">a@b</a>'], '--c']);
40
41
  assert.deepStrictEqual(inspect(parser('a@b.')), [['<a class="email" href="mailto:a@b">a@b</a>'], '.']);
41
42
  assert.deepStrictEqual(inspect(parser('a@b.c')), [['<a class="email" href="mailto:a@b.c">a@b.c</a>'], '']);
42
43
  assert.deepStrictEqual(inspect(parser('a@b..c')), [['<a class="email" href="mailto:a@b">a@b</a>'], '..c']);
@@ -6,6 +6,6 @@ import { html } from 'typed-dom';
6
6
  // https://html.spec.whatwg.org/multipage/input.html
7
7
 
8
8
  export const email: AutolinkParser.EmailParser = creator(rewrite(verify(
9
- str(/^[0-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*@[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?)*/),
9
+ str(/^[0-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*@[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*(?![0-9A-Za-z])/),
10
10
  ([source]) => source.indexOf('@') <= 64 && source.length <= 255),
11
11
  source => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], '']));
@@ -35,6 +35,7 @@ describe('Unit: parser/inline/autolink/hashnum', () => {
35
35
  assert.deepStrictEqual(inspect(parser('あ#1')), [['あ#1'], '']);
36
36
  assert.deepStrictEqual(inspect(parser(' #1')), undefined);
37
37
  assert.deepStrictEqual(inspect(parser('#12345678901234567')), [['#12345678901234567'], '']);
38
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(128)}a`)), [[`#${'1'.repeat(128)}a`], '']);
38
39
  });
39
40
 
40
41
  it('valid', () => {
@@ -1,11 +1,12 @@
1
1
  import { AutolinkParser } from '../../inline';
2
2
  import { union, rewrite, context, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { link } from '../link';
4
+ import { emoji } from './hashtag';
4
5
  import { str } from '../../source';
5
6
  import { define } from 'typed-dom';
6
7
 
7
8
  export const hashnum: AutolinkParser.HashnumParser = lazy(() => fmap(rewrite(
8
- open('#', str(/^[0-9]{1,16}(?![0-9A-Za-z'_]|[^\x00-\x7F\s])/)),
9
+ open('#', str(new RegExp(String.raw`^[0-9]{1,16}(?![^\p{C}\p{S}\p{P}\s]|${emoji}|['_])`, 'u'))),
9
10
  context({ syntax: { inline: {
10
11
  link: true,
11
12
  autolink: false,
@@ -23,9 +23,6 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
23
23
  assert.deepStrictEqual(inspect(parser(`#'`)), [[`#'`], '']);
24
24
  assert.deepStrictEqual(inspect(parser(`#a''`)), [[`#a''`], '']);
25
25
  assert.deepStrictEqual(inspect(parser('#_')), [['#_'], '']);
26
- assert.deepStrictEqual(inspect(parser('#a_')), [['#a_'], '']);
27
- assert.deepStrictEqual(inspect(parser('#a__b')), [['#a__b'], '']);
28
- assert.deepStrictEqual(inspect(parser(`#a_'b`)), [[`#a_'b`], '']);
29
26
  assert.deepStrictEqual(inspect(parser('#(a)')), [['#'], '(a)']);
30
27
  assert.deepStrictEqual(inspect(parser('#{}')), [['#'], '{}']);
31
28
  assert.deepStrictEqual(inspect(parser('#{{}')), [['#'], '{{}']);
@@ -38,6 +35,9 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
38
35
  assert.deepStrictEqual(inspect(parser('a##1')), [['a##1'], '']);
39
36
  assert.deepStrictEqual(inspect(parser('a##b')), [['a##b'], '']);
40
37
  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`], '']);
41
41
  assert.deepStrictEqual(inspect(parser(' #a')), undefined);
42
42
  });
43
43
 
@@ -50,21 +50,22 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
50
50
  assert.deepStrictEqual(inspect(parser('#a\\\n')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '\\\n']);
51
51
  assert.deepStrictEqual(inspect(parser('#a)')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], ')']);
52
52
  assert.deepStrictEqual(inspect(parser('#a(b')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '(b']);
53
- assert.deepStrictEqual(inspect(parser('#a(b)')), [['<a href="/hashtags/a(b)" class="hashtag">#a(b)</a>'], '']);
54
- assert.deepStrictEqual(inspect(parser('#a((b))')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '((b))']);
55
- assert.deepStrictEqual(inspect(parser(`#a'`)), [[`<a href="/hashtags/a'" class="hashtag">#a'</a>`], '']);
56
- assert.deepStrictEqual(inspect(parser(`#a(b')`)), [[`<a href="/hashtags/a(b')" class="hashtag">#a(b')</a>`], '']);
57
- assert.deepStrictEqual(inspect(parser(`#a(b'')`)), [[`<a href="/hashtags/a" class="hashtag">#a</a>`], `(b'')`]);
58
- assert.deepStrictEqual(inspect(parser(`#a('b)`)), [['<a href="/hashtags/a" class="hashtag">#a</a>'], `('b)`]);
53
+ assert.deepStrictEqual(inspect(parser('#a(b)')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '(b)']);
54
+ assert.deepStrictEqual(inspect(parser('#a_')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '_']);
59
55
  assert.deepStrictEqual(inspect(parser('#a_b')), [['<a href="/hashtags/a_b" class="hashtag">#a_b</a>'], '']);
60
- assert.deepStrictEqual(inspect(parser(`#a'_b`)), [[`<a href="/hashtags/a'_b" class="hashtag">#a'_b</a>`], '']);
61
- assert.deepStrictEqual(inspect(parser('#a(b_c)')), [['<a href="/hashtags/a(b_c)" class="hashtag">#a(b_c)</a>'], '']);
62
- assert.deepStrictEqual(inspect(parser('#a(b__c)')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '(b__c)']);
56
+ assert.deepStrictEqual(inspect(parser(`#a_'b`)), [['<a href="/hashtags/a" class="hashtag">#a</a>'], `_'b`]);
57
+ assert.deepStrictEqual(inspect(parser('#a__b')), [['<a href="/hashtags/a" class="hashtag">#a</a>'], '__b']);
63
58
  assert.deepStrictEqual(inspect(parser('#あ')), [['<a href="/hashtags/あ" class="hashtag">#あ</a>'], '']);
59
+ assert.deepStrictEqual(inspect(parser('#👩')), [['<a href="/hashtags/👩" class="hashtag">#👩</a>'], '']);
64
60
  assert.deepStrictEqual(inspect(parser('#1a')), [['<a href="/hashtags/1a" class="hashtag">#1a</a>'], '']);
65
61
  assert.deepStrictEqual(inspect(parser('#1あ')), [['<a href="/hashtags/1あ" class="hashtag">#1あ</a>'], '']);
62
+ assert.deepStrictEqual(inspect(parser('#1👩')), [['<a href="/hashtags/1👩" class="hashtag">#1👩</a>'], '']);
66
63
  assert.deepStrictEqual(inspect(parser('#domain/a')), [['<a href="https://domain/hashtags/a" target="_blank" class="hashtag">#domain/a</a>'], '']);
67
64
  assert.deepStrictEqual(inspect(parser('#domain.co.jp/a')), [['<a href="https://domain.co.jp/hashtags/a" target="_blank" class="hashtag">#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`], '']);
68
69
  });
69
70
 
70
71
  });
@@ -6,15 +6,22 @@ import { define } from 'typed-dom';
6
6
 
7
7
  // https://example/hashtags/a must be a hashtag page or a redirect page going there.
8
8
 
9
+ // https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
10
+ export const emoji = String.raw`\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F`;
11
+
9
12
  export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
10
13
  open(
11
14
  '#',
12
15
  tails([
13
16
  verify(
14
- str(/^[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:[0-9A-Za-z-]{0,61}[0-9A-Za-z])?)*\//),
17
+ str(/^[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-(?=\w)){0,61}[0-9A-Za-z])?)*\//),
15
18
  ([source]) => source.length <= 253 + 1),
16
19
  verify(
17
- str(/^(?=(?:[0-9]{1,127}_?)?(?:[A-Za-z]|[^\x00-\x7F\s]))(?:[0-9A-Za-z]|[^\x00-\x7F\s]|'(?!')|_(?=[0-9A-Za-z]|[^\x00-\x7F\s])){1,128}(?:_?\((?=(?:[0-9]{1,127}_?)?(?:[A-Za-z]|[^\x00-\x7F\s]))(?:[0-9A-Za-z]|[^\x00-\x7F\s]|'(?!')|_(?=[0-9A-Za-z]|[^\x00-\x7F\s])){1,125}\))?(?![0-9A-Za-z'_]|[^\x00-\x7F\s])/),
20
+ str(new RegExp(['^',
21
+ String.raw`(?=[0-9]{0,127}_?(?:[^\d\p{C}\p{S}\p{P}\s]|${emoji}))`,
22
+ String.raw`(?:[^\p{C}\p{S}\p{P}\s]|${emoji}|_(?=[^\p{C}\p{S}\p{P}\s]|${emoji})){1,128}`,
23
+ String.raw`(?!_?(?:[^\p{C}\p{S}\p{P}\s]|${emoji})|')`,
24
+ ].join(''), 'u')),
18
25
  ([source]) => source.length <= 128),
19
26
  ])),
20
27
  context({ syntax: { inline: {
@@ -4,33 +4,30 @@ import { url } from './autolink/url';
4
4
  import { email } from './autolink/email';
5
5
  import { channel } from './autolink/channel';
6
6
  import { account } from './autolink/account';
7
- import { hashtag } from './autolink/hashtag';
7
+ import { hashtag, emoji } from './autolink/hashtag';
8
8
  import { hashnum } from './autolink/hashnum';
9
9
  import { anchor } from './autolink/anchor';
10
10
  import { str } from '../source';
11
11
  import { stringify } from '../util';
12
12
 
13
13
  export const autolink: AutolinkParser = fmap(
14
- validate(/^(?:[@#>0-9A-Za-z]|[^\x00-\x7F\s])/,
14
+ validate(/^(?:[@#>0-9A-Za-z]|\S#)/,
15
15
  guard(context => context.syntax?.inline?.autolink ?? true,
16
16
  some(union([
17
17
  url,
18
18
  email,
19
19
  // Escape unmatched email-like strings.
20
20
  str(/^[0-9A-Za-z]+(?:[.+_-][0-9A-Za-z]+)*(?:@(?:[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?)+/),
21
- // Escape repeated symbols.
22
- str(/^@+(?![0-9A-Za-z]|[^\x00-\x7F\s])/),
23
- str(/^#+(?![0-9A-Za-z'_]|[^\x00-\x7F\s])/),
24
21
  channel,
25
22
  account,
26
23
  // Escape unmatched account-like strings.
27
- str(/^@[0-9A-Za-z]+(?:-[0-9A-Za-z]+)*/),
24
+ str(/^@+[0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/),
28
25
  // Escape invalid leading characters.
29
- str(/^[0-9A-Za-z]+(?=#)|^[^\x00-\x7F\s]+(?=#)/),
26
+ str(new RegExp(String.raw`^(?:[^\p{C}\p{S}\p{P}\s]|${emoji}|['_])(?=#)`, 'u')),
30
27
  hashtag,
31
28
  hashnum,
32
29
  // Escape unmatched hashtag-like strings.
33
- str(/^#(?:[0-9A-Za-z'_]|[^\x00-\x7F\s])+/),
30
+ str(new RegExp(String.raw`^#+(?:[^\p{C}\p{S}\p{P}\s]|${emoji}|['_])*`, 'u')),
34
31
  anchor,
35
32
  ])))),
36
33
  ns => ns.length === 1 ? ns : [stringify(ns)]);
@@ -6,10 +6,7 @@ import { str } from '../source';
6
6
  import { html, defrag } from 'typed-dom';
7
7
  import { unshift, push } from 'spica/array';
8
8
 
9
- const index = new RegExp(`^(?:${[
10
- /(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))+/,
11
- /[0-9]{1,4}|[A-Za-z]/,
12
- ].map(r => r.source).join('|')})`);
9
+ const index = /^(?:[0-9]+(?:\.[0-9]+)*|[A-Za-z])/;
13
10
  const indexFW = new RegExp(index.source.replace(/[019AZaz](?!,)/g, c => String.fromCharCode(c.charCodeAt(0) + 0xfee0)));
14
11
 
15
12
  export const bracket: BracketParser = lazy(() => union([
@@ -17,8 +17,10 @@ describe('Unit: parser/inline/comment', () => {
17
17
  assert.deepStrictEqual(inspect(parser('[# #] #]')), undefined);
18
18
  assert.deepStrictEqual(inspect(parser('[# #] #]')), undefined);
19
19
  assert.deepStrictEqual(inspect(parser('[# [#')), undefined);
20
+ assert.deepStrictEqual(inspect(parser('[#[#')), undefined);
20
21
  assert.deepStrictEqual(inspect(parser('[# [# ')), undefined);
21
22
  assert.deepStrictEqual(inspect(parser('[# [# a')), undefined);
23
+ assert.deepStrictEqual(inspect(parser('[# a[#')), [['<sup class="comment invalid">[# a</sup>'], '[#']);
22
24
  assert.deepStrictEqual(inspect(parser('[# a [#')), [['<sup class="comment invalid">[# a </sup>'], '[#']);
23
25
  assert.deepStrictEqual(inspect(parser('[# a [# ')), [['<sup class="comment invalid">[# a </sup>'], '[# ']);
24
26
  assert.deepStrictEqual(inspect(parser('[# a [#\n')), [['<sup class="comment invalid">[# a </sup>'], '[#\n']);
@@ -6,9 +6,9 @@ import { unescsource } from '../source';
6
6
  import { html } from 'typed-dom';
7
7
 
8
8
  export const comment: CommentParser = creator(validate('[#', match(
9
- /^\[(#+)\s+(?!\s|\1\]|\[\1\s)((?:\S+\s+)+?)(\1\]|(?=\[\1(?:$|\s)))/,
9
+ /^\[(#+)(?!\S|\s+\1\]|\s*\[\1(?:$|\s))((?:\s+\S+)+?)(?:\s+(\1\])|\s*(?=\[\1(?:$|\s)))/,
10
10
  ([whole, , body, closer]) => (rest, context) => {
11
- [whole, body] = `${whole}\0${body.trimEnd()}`.replace(/\x1B/g, '').split('\0', 2);
11
+ [whole, body] = `${whole}\0${body.trimStart()}`.replace(/\x1B/g, '').split('\0', 2);
12
12
  if (!closer) return [[html('sup', {
13
13
  class: 'comment invalid',
14
14
  'data-invalid-syntax': 'comment',
@@ -88,7 +88,7 @@ export const html: HTMLParser = lazy(() => creator(validate('<', validate(/^<[a-
88
88
  ])))));
89
89
 
90
90
  export const attribute: HTMLParser.TagParser.AttributeParser = union([
91
- str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\n"])*")?(?=[^\S\n]|>)/),
91
+ str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|>)/),
92
92
  ]);
93
93
 
94
94
  function elem(tag: string, as: (HTMLElement | string)[], bs: (HTMLElement | string)[], cs: (HTMLElement | string)[], context: MarkdownParser.Context): HTMLElement {
@@ -71,7 +71,7 @@ export const uri: LinkParser.ParameterParser.UriParser = union([
71
71
 
72
72
  export const option: LinkParser.ParameterParser.OptionParser = union([
73
73
  fmap(str(/^[^\S\n]+nofollow(?=[^\S\n]|})/), () => [` rel="nofollow"`]),
74
- str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\n"])*")?(?=[^\S\n]|})/),
74
+ str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|})/),
75
75
  fmap(str(/^[^\S\n]+(?=})/), () => []),
76
76
  fmap(str(/^[^\S\n]+[^\n{}]+/), opt => [` \\${opt.slice(1)}`]),
77
77
  ]);
@@ -83,7 +83,7 @@ export function resolve(uri: string, host: URL | Location, source: URL | Locatio
83
83
  case uri.slice(0, 2) === '^/':
84
84
  const last = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
85
85
  return last.includes('.') // isFile
86
- && /^[0-9]*[a-z][0-9a-z]*$/i.test(last.slice(last.lastIndexOf('.') + 1))
86
+ && /^[0-9]*[A-Za-z][0-9A-Za-z]*$/.test(last.slice(last.lastIndexOf('.') + 1))
87
87
  ? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}`
88
88
  : `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
89
89
  case host.origin === source.origin
@@ -11,8 +11,8 @@ import { unshift, push, join } from 'spica/array';
11
11
  export const ruby: RubyParser = lazy(() => creator(bind(verify(
12
12
  validate('[', ')', '\n',
13
13
  sequence([
14
- surround('[', focus(/^(?:\\[^\n]|[^\[\]\n])+(?=]\()/, text), ']'),
15
- surround('(', focus(/^(?:\\[^\n]|[^\(\)\n])+(?=\))/, text), ')'),
14
+ surround('[', focus(/^(?:\\[^\n]|[^\\\[\]\n])+(?=]\()/, text), ']'),
15
+ surround('(', focus(/^(?:\\[^\n]|[^\\\(\)\n])+(?=\))/, text), ')'),
16
16
  ])),
17
17
  ([texts]) => isStartTightNodes(texts)),
18
18
  ([texts, rubies], rest) => {
@@ -168,11 +168,12 @@ describe('Unit: parser/inline', () => {
168
168
  assert.deepStrictEqual(inspect(parser('#a\nb\n#c\n[#d]')), [['<a href="/hashtags/a" class="hashtag">#a</a>', '<br>', 'b', '<br>', '<a href="/hashtags/c" class="hashtag">#c</a>', '<br>', '<a class="index" href="#index:d">d</a>'], '']);
169
169
  assert.deepStrictEqual(inspect(parser('##a')), [['##a'], '']);
170
170
  assert.deepStrictEqual(inspect(parser('a#b')), [['a#b'], '']);
171
- assert.deepStrictEqual(inspect(parser('0a#b')), [['0a#b'], '']);
171
+ assert.deepStrictEqual(inspect(parser('0a#b')), [['0', 'a#b'], '']);
172
172
  assert.deepStrictEqual(inspect(parser('あ#b')), [['あ#b'], '']);
173
- assert.deepStrictEqual(inspect(parser('あい#b')), [['あい#b'], '']);
173
+ assert.deepStrictEqual(inspect(parser('あい#b')), [['あ', 'い#b'], '']);
174
174
  assert.deepStrictEqual(inspect(parser('0aあ#b')), [['0a', 'あ#b'], '']);
175
- assert.deepStrictEqual(inspect(parser('0aあい#b')), [['0a', 'あい#b'], '']);
175
+ assert.deepStrictEqual(inspect(parser('0aあい#b')), [['0a', 'あ', 'い#b'], '']);
176
+ assert.deepStrictEqual(inspect(parser('「#あ」')), [['「', '<a href="/hashtags/あ" class="hashtag">#あ</a>', '」'], '']);
176
177
  assert.deepStrictEqual(inspect(parser('a\n#b')), [['a', '<br>', '<a href="/hashtags/b" class="hashtag">#b</a>'], '']);
177
178
  assert.deepStrictEqual(inspect(parser('a\\\n#b')), [['a', '<span class="linebreak"> </span>', '<a href="/hashtags/b" class="hashtag">#b</a>'], '']);
178
179
  assert.deepStrictEqual(inspect(parser('*a*#b')), [['<em>a</em>', '<a href="/hashtags/b" class="hashtag">#b</a>'], '']);
@@ -186,6 +187,7 @@ describe('Unit: parser/inline', () => {
186
187
  it('hashnum', () => {
187
188
  assert.deepStrictEqual(inspect(parser('#1')), [['<a class="hashnum">#1</a>'], '']);
188
189
  assert.deepStrictEqual(inspect(parser('#12345678901234567@a')), [['#12345678901234567@a'], '']);
190
+ assert.deepStrictEqual(inspect(parser('「#1」')), [['「', '<a class="hashnum">#1</a>', '」'], '']);
189
191
  });
190
192
 
191
193
  });
@@ -4,9 +4,9 @@ import { union, focus, creator } from '../../combinator';
4
4
  import { str } from './str';
5
5
  import { html } from 'typed-dom';
6
6
 
7
- export const separator = /[\s\x00-\x7F]|[、。!?][^\S\n]*(?=\\\n)/;
7
+ export const separator = /[\s\x00-\x7F]|\S#|[、。!?][^\S\n]*(?=\\\n)/;
8
8
  export const nonWhitespace = /[\S\n]|$/;
9
- export const nonAlphanumeric = /[^0-9A-Za-z]|$/;
9
+ export const nonAlphanumeric = /[^0-9A-Za-z]|\S#|$/;
10
10
  const repeat = str(/^(.)\1*/);
11
11
 
12
12
  export const text: TextParser = creator((source, context) => {
@@ -59,7 +59,7 @@ export const text: TextParser = creator((source, context) => {
59
59
  assert(source[0] !== '\n');
60
60
  const b = source[0].trimStart() === '';
61
61
  const i = b || isAlphanumeric(source[0])
62
- ? source.search(b ? nonWhitespace : nonAlphanumeric)
62
+ ? source.search(b ? nonWhitespace : nonAlphanumeric) || 1
63
63
  : 1;
64
64
  assert(i > 0);
65
65
  assert(!['\\', '\n'].includes(source[0]));
@@ -12,7 +12,7 @@ export const unescsource: UnescapableSourceParser = creator(source => {
12
12
  case 0: {
13
13
  const b = source[0] !== '\n' && source[0].trimStart() === '';
14
14
  const i = b || isAlphanumeric(source[0])
15
- ? source.search(b ? nonWhitespace : nonAlphanumeric)
15
+ ? source.search(b ? nonWhitespace : nonAlphanumeric) || 1
16
16
  : 1;
17
17
  assert(i > 0);
18
18
  return [[source.slice(0, i)], source.slice(i)];
@@ -42,13 +42,13 @@ const invisibleHTMLEntityNames = [
42
42
  'InvisibleComma',
43
43
  'ic',
44
44
  ];
45
- const blankline = new RegExp(String.raw`^(?!$|\n)(?:\\?\s|&(?:${invisibleHTMLEntityNames.join('|')});|<wbr>|\[(#+)\s+(?!\s|\1\]|\[\1\s)(?:\S+\s+)+?(?:\1\]|(?=\[\1(?:$|\s))))*\\?(?:$|\n)`, 'gm');
45
+ const blankline = new RegExp(String.raw`^(?!$|\n)(?:\\?[^\S\n]|&(?:${invisibleHTMLEntityNames.join('|')});|<wbr>|\[(#+)(?!\S|\s+\1\]|\s*\[\1(?:$|\s))((?:\s+\S+)+?)(?:\s+(\1\])|\s*(?=\[\1(?:$|\s))))*(?:\\?(?:$|\n)|(\S))`, 'gm');
46
46
 
47
47
  export function visualize<P extends Parser<HTMLElement | string>>(parser: P): P;
48
48
  export function visualize<T extends HTMLElement | string>(parser: Parser<T>): Parser<T> {
49
49
  return union([
50
50
  convert(
51
- source => source.replace(blankline, line => line.replace(/[\\&<\[]/g, '\x1B$&')),
51
+ source => source.replace(blankline, (line, ...$) => !$[3] ? line.replace(/[\\&<\[]/g, '\x1B$&') : line),
52
52
  verify(parser, (ns, rest, context) => !rest && hasVisible(ns, context))),
53
53
  some(union([linebreak, unescsource])),
54
54
  ]);
@@ -15,12 +15,12 @@ export function youtube(source: HTMLImageElement, url: URL): HTMLElement | undef
15
15
  function resolve(url: URL): string | undefined {
16
16
  switch (url.origin) {
17
17
  case 'https://www.youtube.com':
18
- return url.pathname === '/watch/'
19
- ? url.href.replace(/.+?=/, '').replace('&', '?')
18
+ return url.pathname.match(/^\/watch\/?$/)
19
+ ? url.searchParams.get('v')?.concat(url.search.replace(/([?&])v=[^&#]*&?/g, '$1'), url.hash)
20
20
  : undefined;
21
21
  case 'https://youtu.be':
22
- return url.pathname.match(/^\/[\w-]+$/)
23
- ? url.href.slice(url.href.indexOf('/', 9) + 1)
22
+ return url.pathname.match(/^\/[\w-]+\/?$/)
23
+ ? url.href.slice(url.origin.length)
24
24
  : undefined;
25
25
  default:
26
26
  return;