securemark 0.266.0 → 0.268.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 (46) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/dist/index.js +75 -79
  4. package/index.d.ts +7 -8
  5. package/markdown.d.ts +26 -9
  6. package/package.json +7 -7
  7. package/src/combinator/control/constraint/block.ts +2 -2
  8. package/src/combinator/control/constraint/line.ts +2 -2
  9. package/src/combinator/control/manipulation/fence.ts +4 -4
  10. package/src/parser/api/bind.ts +1 -1
  11. package/src/parser/api/parse.test.ts +0 -2
  12. package/src/parser/api/parse.ts +1 -1
  13. package/src/parser/autolink.ts +15 -22
  14. package/src/parser/block/blockquote.ts +1 -1
  15. package/src/parser/block/codeblock.ts +2 -2
  16. package/src/parser/block/olist.test.ts +2 -2
  17. package/src/parser/block/olist.ts +2 -2
  18. package/src/parser/block/pagebreak.test.ts +31 -0
  19. package/src/parser/block/pagebreak.ts +7 -0
  20. package/src/parser/block/paragraph.test.ts +3 -0
  21. package/src/parser/block/reply/cite.ts +1 -2
  22. package/src/parser/block/reply/quote.test.ts +3 -0
  23. package/src/parser/block/reply/quote.ts +15 -9
  24. package/src/parser/block/sidefence.ts +1 -1
  25. package/src/parser/block/ulist.test.ts +2 -2
  26. package/src/parser/block/ulist.ts +2 -2
  27. package/src/parser/block.ts +3 -3
  28. package/src/parser/inline/autolink/account.ts +3 -5
  29. package/src/parser/inline/autolink/anchor.test.ts +2 -2
  30. package/src/parser/inline/autolink/anchor.ts +4 -3
  31. package/src/parser/inline/autolink/channel.test.ts +0 -2
  32. package/src/parser/inline/autolink/channel.ts +0 -1
  33. package/src/parser/inline/autolink/hashnum.test.ts +1 -1
  34. package/src/parser/inline/autolink/hashtag.test.ts +2 -6
  35. package/src/parser/inline/autolink/hashtag.ts +7 -20
  36. package/src/parser/inline/extension/index.ts +1 -1
  37. package/src/parser/inline/extension/indexee.ts +1 -0
  38. package/src/parser/inline/link.ts +1 -0
  39. package/src/parser/inline.test.ts +5 -2
  40. package/src/parser/source/line.ts +3 -3
  41. package/src/parser/source/str.ts +1 -1
  42. package/src/parser/util.ts +10 -0
  43. package/src/parser/visibility.ts +2 -1
  44. package/src/util/quote.ts +9 -2
  45. package/src/parser/block/horizontalrule.test.ts +0 -31
  46. package/src/parser/block/horizontalrule.ts +0 -7
@@ -1,5 +1,5 @@
1
1
  import { Parser, Ctx } from '../../data/parser';
2
- import { firstline, isEmpty } from '../constraint/line';
2
+ import { firstline, isBlank } from '../constraint/line';
3
3
  import { unshift } from 'spica/array';
4
4
 
5
5
  export function fence<C extends Ctx, D extends Parser<unknown, C>[]>(opener: RegExp, limit: number, separation = true): Parser<string, C, D> {
@@ -13,20 +13,20 @@ export function fence<C extends Ctx, D extends Parser<unknown, C>[]>(opener: Reg
13
13
  if (matches[0].indexOf(delim, delim.length) !== -1) return;
14
14
  let rest = source.slice(matches[0].length);
15
15
  // Prevent annoying parsing in editing.
16
- if (isEmpty(firstline(rest)) && firstline(rest.slice(firstline(rest).length)).trimEnd() !== delim) return;
16
+ if (isBlank(firstline(rest)) && firstline(rest.slice(firstline(rest).length)).trimEnd() !== delim) return;
17
17
  let block = '';
18
18
  let closer = '';
19
19
  let overflow = '';
20
20
  for (let count = 1; ; ++count) {
21
21
  if (rest === '') break;
22
22
  const line = firstline(rest);
23
- if ((closer || count > limit + 1) && isEmpty(line)) break;
23
+ if ((closer || count > limit + 1) && isBlank(line)) break;
24
24
  if(closer) {
25
25
  overflow += line;
26
26
  }
27
27
  if (!closer && count <= limit + 1 && line.slice(0, delim.length) === delim && line.trimEnd() === delim) {
28
28
  closer = line;
29
- if (isEmpty(firstline(rest.slice(line.length)))) {
29
+ if (isBlank(firstline(rest.slice(line.length)))) {
30
30
  rest = rest.slice(line.length);
31
31
  break;
32
32
  }
@@ -26,7 +26,7 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
26
26
  host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
27
27
  memo: new Memo({ targets: State.backtrackers }),
28
28
  };
29
- if (context.id?.includes(':')) throw new Error('ID must not contain ":"');
29
+ if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
30
30
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
31
31
  assert(!settings.id);
32
32
  type Block = readonly [segment: string, blocks: readonly HTMLElement[], url: string];
@@ -99,7 +99,6 @@ describe('Unit: parser/api/parse', () => {
99
99
  '@a#b',
100
100
  '@domain/a#b',
101
101
  '#a',
102
- '#domain/a',
103
102
  '[#a]',
104
103
  '$-a\n$$\n$$',
105
104
  '$-a',
@@ -122,7 +121,6 @@ describe('Unit: parser/api/parse', () => {
122
121
  '<p><a class="channel" href="https://source/@a?ch=b" target="_blank">@a#b</a></p>',
123
122
  '<p><a class="channel" href="https://domain/@a?ch=b" target="_blank">@domain/a#b</a></p>',
124
123
  '<p><a class="hashtag" href="https://source/hashtags/a" target="_blank">#a</a></p>',
125
- '<p><a class="hashtag" href="https://domain/hashtags/a" target="_blank">#domain/a</a></p>',
126
124
  '<p><a class="index" href="#index::a">a</a></p>',
127
125
  '<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>',
128
126
  '<p><a class="label" data-label="$-a" href="#label:$-a">(1)</a></p>',
@@ -19,7 +19,6 @@ interface Options extends ParserOptions {
19
19
 
20
20
  export function parse(source: string, opts: Options = {}, context?: MarkdownParser.Context): DocumentFragment {
21
21
  if (!validate(source, MAX_SEGMENT_SIZE)) throw new Error(`Too large input over ${MAX_SEGMENT_SIZE.toLocaleString('en')} bytes`);
22
- if (context?.id?.includes(':')) throw new Error('ID must not contain ":"');
23
22
  const url = headers(source).find(field => field.toLowerCase().startsWith('url:'))?.slice(4).trim() ?? '';
24
23
  source = !context ? normalize(source) : source;
25
24
  assert(!context?.delimiters);
@@ -33,6 +32,7 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
33
32
  },
34
33
  memo: new Memo({ targets: State.backtrackers }),
35
34
  };
35
+ if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
36
36
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
37
37
  const node = frag();
38
38
  let index = 0;
@@ -1,28 +1,21 @@
1
1
  import { MarkdownParser } from '../../markdown';
2
- import { union, lazy } from '../combinator';
2
+ import { union, tails, subsequence, some, line, focus, lazy } from '../combinator';
3
+ import { link } from './inline/link';
3
4
  import { autolink as autolink_ } from './inline/autolink';
4
- import { linebreak, unescsource } from './source';
5
+ import { linebreak, unescsource, str } from './source';
6
+ import { format } from './util';
5
7
 
6
8
  export import AutolinkParser = MarkdownParser.AutolinkParser;
7
9
 
8
- const delimiter = /[@#>0-9A-Za-z\n]|\S[#>]/;
10
+ export const autolink: AutolinkParser = lazy(() => some(line(subsequence([
11
+ lineurl,
12
+ some(union([
13
+ autolink_,
14
+ linebreak,
15
+ unescsource,
16
+ ])),
17
+ ]))));
9
18
 
10
- export const autolink: AutolinkParser = ({ source, context }) => {
11
- if (source === '') return;
12
- assert(source[0] !== '\x1B');
13
- const i = source.search(delimiter);
14
- switch (i) {
15
- case -1:
16
- return [[source], ''];
17
- case 0:
18
- return parser({ source, context });
19
- default:
20
- return [[source.slice(0, i)], source.slice(i)];
21
- }
22
- };
23
-
24
- const parser: AutolinkParser = lazy(() => union([
25
- autolink_,
26
- linebreak,
27
- unescsource
28
- ]));
19
+ export const lineurl: AutolinkParser.LineUrlParser = lazy(() => focus(
20
+ /^!?https?:\/\/\S+(?=[^\S\n]*(?:$|\n))/,
21
+ format(tails([str('!'), link]))));
@@ -25,7 +25,7 @@ const source: BlockquoteParser.SourceParser = lazy(() => fmap(
25
25
  convert(unindent, source)),
26
26
  rewrite(
27
27
  some(contentline, opener),
28
- convert(unindent, fmap(some(autolink), ns => [html('pre', defrag(ns))]))),
28
+ convert(unindent, fmap(autolink, ns => [html('pre', defrag(ns))]))),
29
29
  ]))),
30
30
  ns => [html('blockquote', ns)]));
31
31
 
@@ -1,6 +1,6 @@
1
1
  import { CodeBlockParser } from '../block';
2
2
  import { eval } from '../../combinator/data/parser';
3
- import { some, block, validate, fence, clear, fmap } from '../../combinator';
3
+ import { block, validate, fence, clear, fmap } from '../../combinator';
4
4
  import { autolink } from '../autolink';
5
5
  import { html, defrag } from 'typed-dom/dom';
6
6
 
@@ -69,6 +69,6 @@ export const codeblock: CodeBlockParser = block(validate('```', fmap(
69
69
  params.lang
70
70
  ? context.caches?.code?.get(`${params.lang ?? ''}\n${body.slice(0, -1)}`)?.cloneNode(true).childNodes ||
71
71
  body.slice(0, -1) || undefined
72
- : defrag(eval(some(autolink)({ source: body.slice(0, -1), context }), [])));
72
+ : defrag(eval(autolink({ source: body.slice(0, -1), context }), [])));
73
73
  return [el];
74
74
  })));
@@ -37,8 +37,8 @@ describe('Unit: parser/block/olist', () => {
37
37
  // pending
38
38
  assert.deepStrictEqual(inspect(parser('1. ')), [['<ol><li></li></ol>'], '']);
39
39
  // filled
40
- assert.deepStrictEqual(inspect(parser('1. \\')), [['<ol><li></li></ol>'], '']);
41
- assert.deepStrictEqual(inspect(parser('1. \\\n')), [['<ol><li></li></ol>'], '']);
40
+ assert.deepStrictEqual(inspect(parser('1. \\')), [['<ol><li id="index::\\">\\</li></ol>'], '']);
41
+ assert.deepStrictEqual(inspect(parser('1. \\\n')), [['<ol><li id="index::\\">\\</li></ol>'], '']);
42
42
  assert.deepStrictEqual(inspect(parser('1. -')), [['<ol><li id="index::-">-</li></ol>'], '']);
43
43
  assert.deepStrictEqual(inspect(parser('1. -\n')), [['<ol><li id="index::-">-</li></ol>'], '']);
44
44
  // pending
@@ -6,7 +6,7 @@ import { ilist_ } from './ilist';
6
6
  import { inline, indexee, indexer } from '../inline';
7
7
  import { contentline } from '../source';
8
8
  import { State } from '../context';
9
- import { trimBlank } from '../visibility';
9
+ import { visualize, trimBlank } from '../visibility';
10
10
  import { memoize } from 'spica/memoize';
11
11
  import { shift } from 'spica/array';
12
12
  import { html, define, defrag } from 'typed-dom/dom';
@@ -37,7 +37,7 @@ const list = (type: string, form: string): OListParser.ListParser => fmap(
37
37
  some(creation(1, false, union([
38
38
  indexee(fmap(fallback(
39
39
  inits([
40
- line(open(heads[form], subsequence([checkbox, trimBlank(some(union([indexer, inline])))]), true)),
40
+ line(open(heads[form], subsequence([checkbox, trimBlank(visualize(some(union([indexer, inline]))))]), true)),
41
41
  indent(union([ulist_, olist_, ilist_])),
42
42
  ]),
43
43
  invalid),
@@ -0,0 +1,31 @@
1
+ import { pagebreak } from './pagebreak';
2
+ import { some } from '../../combinator';
3
+ import { inspect } from '../../debug.test';
4
+
5
+ describe('Unit: parser/block/pagebreak', () => {
6
+ describe('pagebreak', () => {
7
+ const parser = (source: string) => some(pagebreak)({ source, context: {} });
8
+
9
+ it('invalid', () => {
10
+ assert.deepStrictEqual(inspect(parser('')), undefined);
11
+ assert.deepStrictEqual(inspect(parser('\n')), undefined);
12
+ assert.deepStrictEqual(inspect(parser('=')), undefined);
13
+ assert.deepStrictEqual(inspect(parser('==')), undefined);
14
+ assert.deepStrictEqual(inspect(parser('==\n=')), undefined);
15
+ assert.deepStrictEqual(inspect(parser('===a')), undefined);
16
+ assert.deepStrictEqual(inspect(parser('===\na')), undefined);
17
+ assert.deepStrictEqual(inspect(parser('= = =')), undefined);
18
+ assert.deepStrictEqual(inspect(parser(' ===')), undefined);
19
+ assert.deepStrictEqual(inspect(parser('---')), undefined);
20
+ });
21
+
22
+ it('valid', () => {
23
+ assert.deepStrictEqual(inspect(parser('===')), [['<hr>'], '']);
24
+ assert.deepStrictEqual(inspect(parser('=== ')), [['<hr>'], '']);
25
+ assert.deepStrictEqual(inspect(parser('===\n')), [['<hr>'], '']);
26
+ assert.deepStrictEqual(inspect(parser('====')), [['<hr>'], '']);
27
+ });
28
+
29
+ });
30
+
31
+ });
@@ -0,0 +1,7 @@
1
+ import { PagebreakParser } from '../block';
2
+ import { block, line, focus } from '../../combinator';
3
+ import { html } from 'typed-dom/dom';
4
+
5
+ export const pagebreak: PagebreakParser = block(line(focus(
6
+ /^={3,}[^\S\n]*(?:$|\n)/,
7
+ () => [[html('hr')], ''])));
@@ -30,6 +30,9 @@ describe('Unit: parser/block/paragraph', () => {
30
30
  assert.deepStrictEqual(inspect(parser('_a\n<wbr>_\nb')), [['<p>_a<br><wbr>_<br>b</p>'], '']);
31
31
  assert.deepStrictEqual(inspect(parser('*a\n<wbr>*\nb')), [['<p>*a<br><wbr>*<br>b</p>'], '']);
32
32
  assert.deepStrictEqual(inspect(parser('==a\n<wbr>==\nb')), [['<p>==a<br><wbr>==<br>b</p>'], '']);
33
+ assert.deepStrictEqual(inspect(parser('http://host#!')), [['<p><a class="url" href="http://host#!" target="_blank">http://host#!</a></p>'], '']);
34
+ assert.deepStrictEqual(inspect(parser('a\nhttp://host#\\ \nb')), [['<p>a<br><a class="url" href="http://host#\\" target="_blank">http://host#\\</a><br>b</p>'], '']);
35
+ assert.deepStrictEqual(inspect(parser('!http://host#!')), [['<p><a href="http://host#!" target="_blank"><img class="media" data-src="http://host#!" alt=""></a></p>'], '']);
33
36
  assert.deepStrictEqual(inspect(parser('\ta')), [['<p>\ta</p>'], '']);
34
37
  });
35
38
 
@@ -14,8 +14,7 @@ export const cite: ReplyParser.CiteParser = creation(1, false, line(fmap(validat
14
14
  // リンクの実装は後で検討
15
15
  focus(/^>>\.(?=\s*$)/, () => [[html('a', { class: 'anchor' }, '>>.')], '']),
16
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)], '']),
17
+ 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)], '']),
19
18
  ]),
20
19
  ]))),
21
20
  ([el, quotes = '']: [HTMLElement, string?]) => [
@@ -55,6 +55,9 @@ describe('Unit: parser/block/reply/quote', () => {
55
55
  assert.deepStrictEqual(inspect(parser('> $-a, $-b')), [['<span class="quote">&gt; $-a, $-b</span>', '<br>'], '']);
56
56
  assert.deepStrictEqual(inspect(parser('> $a=b$')), [['<span class="quote">&gt; <span class="math" translate="no" data-src="$a=b$">$a=b$</span></span>', '<br>'], '']);
57
57
  assert.deepStrictEqual(inspect(parser('> ${a}$')), [['<span class="quote">&gt; <span class="math" translate="no" data-src="${a}$">${a}$</span></span>', '<br>'], '']);
58
+ assert.deepStrictEqual(inspect(parser('> http://host#!')), [['<span class="quote">&gt; <a class="url" href="http://host#!" target="_blank">http://host#!</a></span>', '<br>'], '']);
59
+ assert.deepStrictEqual(inspect(parser('> a\n> http://host#\\ \n> b')), [['<span class="quote">&gt; a<br>&gt; <a class="url" href="http://host#\\" target="_blank">http://host#\\</a> <br>&gt; b</span>', '<br>'], '']);
60
+ assert.deepStrictEqual(inspect(parser('> !http://host#!')), [['<span class="quote">&gt; !<a class="url" href="http://host#!" target="_blank">http://host#!</a></span>', '<br>'], '']);
58
61
  });
59
62
 
60
63
  });
@@ -1,9 +1,10 @@
1
1
  import { ReplyParser } from '../../block';
2
2
  import { eval } from '../../../combinator/data/parser';
3
- import { union, some, creation, block, line, validate, rewrite, lazy, fmap } from '../../../combinator';
3
+ import { union, subsequence, some, creation, block, line, validate, rewrite, lazy, fmap } from '../../../combinator';
4
4
  import { math } from '../../inline/math';
5
- import { str, anyline } from '../../source';
6
- import { autolink } from '../../autolink';
5
+ import { autolink } from '../../inline/autolink';
6
+ import { linebreak, unescsource, str, anyline } from '../../source';
7
+ import { lineurl } from '../../autolink';
7
8
  import { html, defrag } from 'typed-dom/dom';
8
9
 
9
10
  export const syntax = /^>+(?=[^\S\n])|^>(?=[^\s>])|^>+(?=[^\s>])(?![0-9a-z]+(?:-[0-9a-z]+)*(?![0-9A-Za-z@#:]))/;
@@ -40,8 +41,8 @@ const qblock: ReplyParser.QuoteParser.BlockParser = ({ source, context }) => {
40
41
  const quotes = source.match(/^>+[^\S\n]/mg)!;
41
42
  assert(quotes);
42
43
  assert(quotes.length > 0);
43
- const content = lines.reduce((acc, line, row) => acc + line.slice(quotes[row].length), '');
44
- const nodes = eval(some(text)({ source: content, context }), []);
44
+ const content = lines.reduce((acc, line, i) => acc + line.slice(quotes[i].length), '');
45
+ const nodes = eval(text({ source: content, context }), []);
45
46
  nodes.unshift(quotes.shift()!);
46
47
  for (let i = 0; i < nodes.length; ++i) {
47
48
  const child = nodes[i] as string | Text | Element;
@@ -71,7 +72,12 @@ const qblock: ReplyParser.QuoteParser.BlockParser = ({ source, context }) => {
71
72
  return [nodes, ''];
72
73
  };
73
74
 
74
- const text: ReplyParser.QuoteParser.TextParser = union([
75
- math,
76
- autolink,
77
- ]);
75
+ const text: ReplyParser.QuoteParser.TextParser = some(line(subsequence([
76
+ lineurl,
77
+ some(union([
78
+ math, // quote補助関数が残した数式をパースする。他の構文で数式を残す場合はソーステキストを直接使用する。
79
+ autolink,
80
+ linebreak,
81
+ unescsource,
82
+ ])),
83
+ ])));
@@ -26,6 +26,6 @@ const source: SidefenceParser.SourceParser = lazy(() => fmap(
26
26
  convert(unindent, source)),
27
27
  rewrite(
28
28
  some(contentline, opener),
29
- convert(unindent, fmap(some(autolink), ns => [html('pre', defrag(ns))]))),
29
+ convert(unindent, fmap(autolink, ns => [html('pre', defrag(ns))]))),
30
30
  ]))),
31
31
  ns => [html('blockquote', ns)]));
@@ -26,8 +26,8 @@ describe('Unit: parser/block/ulist', () => {
26
26
  // pending
27
27
  assert.deepStrictEqual(inspect(parser('- ')), [['<ul><li></li></ul>'], '']);
28
28
  // filled
29
- assert.deepStrictEqual(inspect(parser('- \\')), [['<ul><li></li></ul>'], '']);
30
- assert.deepStrictEqual(inspect(parser('- \\\n')), [['<ul><li></li></ul>'], '']);
29
+ assert.deepStrictEqual(inspect(parser('- \\')), [['<ul><li id="index::\\">\\</li></ul>'], '']);
30
+ assert.deepStrictEqual(inspect(parser('- \\\n')), [['<ul><li id="index::\\">\\</li></ul>'], '']);
31
31
  assert.deepStrictEqual(inspect(parser('- -')), [['<ul><li id="index::-">-</li></ul>'], '']);
32
32
  assert.deepStrictEqual(inspect(parser('- -\n')), [['<ul><li id="index::-">-</li></ul>'], '']);
33
33
  });
@@ -4,7 +4,7 @@ import { olist_, invalid } from './olist';
4
4
  import { ilist_ } from './ilist';
5
5
  import { inline, indexer, indexee } from '../inline';
6
6
  import { State } from '../context';
7
- import { trimBlank } from '../visibility';
7
+ import { visualize, trimBlank } from '../visibility';
8
8
  import { unshift } from 'spica/array';
9
9
  import { html, defrag } from 'typed-dom/dom';
10
10
 
@@ -18,7 +18,7 @@ export const ulist_: UListParser = lazy(() => block(fmap(validate(
18
18
  some(creation(1, false, union([
19
19
  indexee(fmap(fallback(
20
20
  inits([
21
- line(open(/^-(?:$|\s)/, subsequence([checkbox, trimBlank(some(union([indexer, inline])))]), true)),
21
+ line(open(/^-(?:$|\s)/, subsequence([checkbox, trimBlank(visualize(some(union([indexer, inline]))))]), true)),
22
22
  indent(union([ulist_, olist_, ilist_])),
23
23
  ]),
24
24
  invalid),
@@ -1,7 +1,7 @@
1
1
  import { MarkdownParser } from '../../markdown';
2
2
  import { union, reset, creation, open, fallback, recover } from '../combinator';
3
3
  import { emptyline } from './source';
4
- import { horizontalrule } from './block/horizontalrule';
4
+ import { pagebreak } from './block/pagebreak';
5
5
  import { heading } from './block/heading';
6
6
  import { ulist } from './block/ulist';
7
7
  import { olist } from './block/olist';
@@ -19,7 +19,7 @@ import { rnd0Z } from 'spica/random';
19
19
  import { html } from 'typed-dom/dom';
20
20
 
21
21
  export import BlockParser = MarkdownParser.BlockParser;
22
- export import HorizontalRuleParser = BlockParser.HorizontalRuleParser;
22
+ export import PagebreakParser = BlockParser.PagebreakParser;
23
23
  export import HeadingParser = BlockParser.HeadingParser;
24
24
  export import UListParser = BlockParser.UListParser;
25
25
  export import OListParser = BlockParser.OListParser;
@@ -38,7 +38,7 @@ export const block: BlockParser = creation(1, false, error(
38
38
  reset({ resources: { clock: 50 * 1000, recursion: 20 } },
39
39
  union([
40
40
  emptyline,
41
- horizontalrule,
41
+ pagebreak,
42
42
  heading,
43
43
  ulist,
44
44
  olist,
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, constraint, tails, verify, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, constraint, tails, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { State } from '../../context';
@@ -12,10 +12,8 @@ export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
12
12
  open(
13
13
  '@',
14
14
  tails([
15
- verify(
16
- str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i),
17
- ([source]) => source.length <= 253 + 1),
18
- str(/^[a-z](?:-(?=[0-9a-z])|[0-9a-z]){0,63}/i),
15
+ str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i),
16
+ str(/^[a-z][0-9a-z]*(?:-[0-9a-z]+)*/i),
19
17
  ]))),
20
18
  convert(
21
19
  source =>
@@ -28,8 +28,8 @@ describe('Unit: parser/inline/autolink/anchor', () => {
28
28
  assert.deepStrictEqual(inspect(parser('>>0-a')), [['<a class="anchor" href="?at=0-a">&gt;&gt;0-a</a>'], '']);
29
29
  assert.deepStrictEqual(inspect(parser('>>0-A')), [['<a class="anchor" href="?at=0-A">&gt;&gt;0-A</a>'], '']);
30
30
  assert.deepStrictEqual(inspect(parser('>>0--a')), [['<a class="anchor" href="?at=0">&gt;&gt;0</a>'], '--a']);
31
- assert.deepStrictEqual(inspect(parser('>>2000-01-31-23-59-59-999-JST')), [['<a class="anchor" href="?at=2000-01-31-23-59-59-999-JST">&gt;&gt;2000-01-31-23-59-59-999-JST</a>'], '']);
32
- assert.deepStrictEqual(inspect(parser('>>A/2000-01-31-23-59-59-JST')), [['<a class="anchor" href="/@A/timeline/2000-01-31-23-59-59-JST">&gt;&gt;A/2000-01-31-23-59-59-JST</a>'], '']);
31
+ assert.deepStrictEqual(inspect(parser('>>2000-0131-2359-59999')), [['<a class="anchor" href="?at=2000-0131-2359-59999">&gt;&gt;2000-0131-2359-59999</a>'], '']);
32
+ assert.deepStrictEqual(inspect(parser('>>A/2000-0131-2359-59')), [['<a class="anchor" href="/@A/timeline?at=2000-0131-2359-59">&gt;&gt;A/2000-0131-2359-59</a>'], '']);
33
33
  });
34
34
 
35
35
  });
@@ -7,8 +7,9 @@ import { define } from 'typed-dom/dom';
7
7
  // Timeline(pseudonym): user/tid
8
8
  // Thread(anonymous): cid
9
9
 
10
- // tid: YYYY-MM-DD-HH-MM-SS-TMZ
11
- // cid: YYYY-MM-DD-HH-MM-SS-mmm-TMZ
10
+ // UTC
11
+ // tid: YYYY-MMDD-HHMM-SS
12
+ // cid: YYYY-MMDD-HHMM-SSmmm
12
13
 
13
14
  // 内部表現はUnixTimeに統一する(時系列順)
14
15
  // 外部表現は投稿ごとに投稿者の投稿時のタイムゾーンに統一する(非時系列順)
@@ -21,7 +22,7 @@ export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>', fma
21
22
  source =>
22
23
  `[${source}]{ ${
23
24
  source.includes('/')
24
- ? `/@${source.slice(2).replace('/', '/timeline/')}`
25
+ ? `/@${source.slice(2).replace('/', '/timeline?at=')}`
25
26
  : `?at=${source.slice(2)}`
26
27
  } }`,
27
28
  union([unsafelink])))),
@@ -16,8 +16,6 @@ describe('Unit: parser/inline/autolink/channel', () => {
16
16
  assert.deepStrictEqual(inspect(parser('@a#1@b')), [['@a#1@b'], '']);
17
17
  assert.deepStrictEqual(inspect(parser('@a#b#')), [['@a#b#'], '']);
18
18
  assert.deepStrictEqual(inspect(parser('@a#b#1')), [['@a#b#1'], '']);
19
- assert.deepStrictEqual(inspect(parser('@a#domain/b')), [['@a#domain/b'], '']);
20
- assert.deepStrictEqual(inspect(parser('@domain/a#domain/b')), [['@domain/a#domain/b'], '']);
21
19
  assert.deepStrictEqual(inspect(parser(' @a#b')), undefined);
22
20
  });
23
21
 
@@ -14,7 +14,6 @@ export const channel: AutolinkParser.ChannelParser = validate('@', bind(
14
14
  ]),
15
15
  (es, rest) => {
16
16
  const source = stringify(es);
17
- if (source.includes('/', source.indexOf('#'))) return;
18
17
  const el = es[0];
19
18
  const url = `${el.getAttribute('href')}?ch=${source.slice(source.indexOf('#') + 1).replace(/#/g, '+')}`;
20
19
  return [[define(el, { class: 'channel', href: url }, source)], rest];
@@ -33,7 +33,7 @@ describe('Unit: parser/inline/autolink/hashnum', () => {
33
33
  assert.deepStrictEqual(inspect(parser('あ#1')), [['あ#1'], '']);
34
34
  assert.deepStrictEqual(inspect(parser(' #1')), undefined);
35
35
  assert.deepStrictEqual(inspect(parser('#12345678901234567')), [['#12345678901234567'], '']);
36
- assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(128)}a`)), [[`#${'1'.repeat(128)}a`], '']);
36
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(16)}a`)), [[`#${'1'.repeat(16)}a`], '']);
37
37
  });
38
38
 
39
39
  it('valid', () => {
@@ -34,9 +34,6 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
34
34
  assert.deepStrictEqual(inspect(parser('a##1')), [['a##1'], '']);
35
35
  assert.deepStrictEqual(inspect(parser('a##b')), [['a##b'], '']);
36
36
  assert.deepStrictEqual(inspect(parser('あ#b')), [['あ#b'], '']);
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`]);
40
37
  assert.deepStrictEqual(inspect(parser(' #a')), undefined);
41
38
  });
42
39
 
@@ -51,15 +48,13 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
51
48
  assert.deepStrictEqual(inspect(parser('#a(b')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '(b']);
52
49
  assert.deepStrictEqual(inspect(parser('#a(b)')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '(b)']);
53
50
  assert.deepStrictEqual(inspect(parser('#a_')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '_']);
54
- assert.deepStrictEqual(inspect(parser('#a__b')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '__b']);
55
51
  assert.deepStrictEqual(inspect(parser('#a_b')), [['<a class="hashtag" href="/hashtags/a_b">#a_b</a>'], '']);
52
+ assert.deepStrictEqual(inspect(parser('#a__b')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '__b']);
56
53
  assert.deepStrictEqual(inspect(parser('#あ')), [['<a class="hashtag" href="/hashtags/あ">#あ</a>'], '']);
57
54
  assert.deepStrictEqual(inspect(parser('#👩')), [['<a class="hashtag" href="/hashtags/👩">#👩</a>'], '']);
58
55
  assert.deepStrictEqual(inspect(parser('#1a')), [['<a class="hashtag" href="/hashtags/1a">#1a</a>'], '']);
59
56
  assert.deepStrictEqual(inspect(parser('#1あ')), [['<a class="hashtag" href="/hashtags/1あ">#1あ</a>'], '']);
60
57
  assert.deepStrictEqual(inspect(parser('#1👩')), [['<a class="hashtag" href="/hashtags/1👩">#1👩</a>'], '']);
61
- assert.deepStrictEqual(inspect(parser('#domain/a')), [['<a class="hashtag" href="https://domain/hashtags/a" target="_blank">#domain/a</a>'], '']);
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>'], '']);
63
58
  assert.deepStrictEqual(inspect(parser(`#'0`)), [[`<a class="hashtag" href="/hashtags/'0">#'0</a>`], '']);
64
59
  assert.deepStrictEqual(inspect(parser(`#'00`)), [[`<a class="hashtag" href="/hashtags/'00">#'00</a>`], '']);
65
60
  assert.deepStrictEqual(inspect(parser(`#1'`)), [[`<a class="hashtag" href="/hashtags/1'">#1'</a>`], '']);
@@ -69,6 +64,7 @@ describe('Unit: parser/inline/autolink/hashtag', () => {
69
64
  assert.deepStrictEqual(inspect(parser(`#a'b`)), [[`<a class="hashtag" href="/hashtags/a'b">#a'b</a>`], '']);
70
65
  assert.deepStrictEqual(inspect(parser(`#a'_b`)), [[`<a class="hashtag" href="/hashtags/a'_b">#a'_b</a>`], '']);
71
66
  assert.deepStrictEqual(inspect(parser(`#a_'b`)), [[`<a class="hashtag" href="/hashtags/a_'b">#a_'b</a>`], '']);
67
+ assert.deepStrictEqual(inspect(parser(`#${'1'.repeat(15)}a`)), [[`<a class="hashtag" href="/hashtags/${'1'.repeat(15)}a">#${'1'.repeat(15)}a</a>`], '']);
72
68
  });
73
69
 
74
70
  });
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, tails, constraint, verify, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { State } from '../../context';
@@ -14,24 +14,11 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
14
14
  constraint(State.shortcut, false,
15
15
  open(
16
16
  '#',
17
- tails([
18
- verify(
19
- str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=\w)){0,61}[0-9a-z])?)*\//i),
20
- ([source]) => source.length <= 253 + 1),
21
- verify(
22
- str(new RegExp([
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
- ].join('').replace(/emoji/g, emoji), 'u')),
27
- ([source]) => source.length <= 128),
28
- ]))),
17
+ str(new RegExp([
18
+ /^(?=(?:[0-9]{1,15})?(?:[^\d\p{C}\p{S}\p{P}\s]|emoji|'))/u.source,
19
+ /(?:[^\p{C}\p{S}\p{P}\s]|emoji|(?<!')'|_(?=[^\p{C}\p{S}\p{P}\s]|emoji|'))+/u.source,
20
+ ].join('').replace(/emoji/g, emoji), 'u')))),
29
21
  convert(
30
- source =>
31
- `[${source}]{ ${
32
- source.includes('/')
33
- ? `https://${source.slice(1).replace('/', '/hashtags/')}`
34
- : `/hashtags/${source.slice(1)}`
35
- } }`,
22
+ source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`,
36
23
  union([unsafelink]))),
37
- ([el]) => [define(el, { class: 'hashtag' }, el.innerText)]));
24
+ ([el]) => [define(el, { class: 'hashtag' })]));
@@ -14,7 +14,7 @@ export const index: IndexParser = lazy(() => validate('[#', fmap(indexee(surroun
14
14
  constraint(State.index, false,
15
15
  syntax(Syntax.index, 2, 1, State.linkers | State.media,
16
16
  startTight(
17
- open(stropt(/^\|?/), trimBlankEnd(some(union([
17
+ open(stropt('|'), trimBlankEnd(some(union([
18
18
  signature,
19
19
  inline,
20
20
  ]), ']', [[/^\\?\n/, 9], [']', 2]])), true)))),
@@ -9,6 +9,7 @@ export function indexee(parser: Parser<HTMLElement, MarkdownParser.Context>, opt
9
9
  }
10
10
 
11
11
  export function identity(id: string | undefined, text: string, name: 'index' | 'mark' = 'index'): string | undefined {
12
+ assert(!id?.match(/[^0-9a-z/-]/i));
12
13
  assert(!text.includes('\n'));
13
14
  if (id === '') return undefined;
14
15
  text &&= text.trim().replace(/\s+/g, '_');
@@ -180,6 +180,7 @@ export function resolve(uri: string, host: URL | Location, source: URL | Locatio
180
180
  case uri.slice(0, 2) === '^/':
181
181
  const last = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
182
182
  return last.includes('.') // isFile
183
+ // Exclude ISO 6709.
183
184
  && /^[0-9]*[a-z][0-9a-z]*$/i.test(last.slice(last.lastIndexOf('.') + 1))
184
185
  ? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}`
185
186
  : `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
@@ -122,6 +122,7 @@ describe('Unit: parser/inline', () => {
122
122
  assert.deepStrictEqual(inspect(parser('a@b')), [['<a class="email" href="mailto:a@b">a@b</a>'], '']);
123
123
  assert.deepStrictEqual(inspect(parser('_a@b')), [['_', '<a class="email" href="mailto:a@b">a@b</a>'], '']);
124
124
  assert.deepStrictEqual(inspect(parser('_a@b_')), [['<em><a class="email" href="mailto:a@b">a@b</a></em>'], '']);
125
+ assert.deepStrictEqual(inspect(parser('_a_b@c_')), [['<em><a class="email" href="mailto:a_b@c">a_b@c</a></em>'], '']);
125
126
  assert.deepStrictEqual(inspect(parser('*a@b*')), [['<strong><a class="email" href="mailto:a@b">a@b</a></strong>'], '']);
126
127
  assert.deepStrictEqual(inspect(parser('(a@b)')), [['<span class="paren">(<a class="email" href="mailto:a@b">a@b</a>)</span>'], '']);
127
128
  assert.deepStrictEqual(inspect(parser(' a@b')), [[' ', '<a class="email" href="mailto:a@b">a@b</a>'], '']);
@@ -130,9 +131,7 @@ describe('Unit: parser/inline', () => {
130
131
 
131
132
  it('channel', () => {
132
133
  assert.deepStrictEqual(inspect(parser('@a#b')), [['<a class="channel" href="/@a?ch=b">@a#b</a>'], '']);
133
- assert.deepStrictEqual(inspect(parser('@a#domain/b')), [['@a#domain/b'], '']);
134
134
  assert.deepStrictEqual(inspect(parser('@domain/a#b')), [['<a class="channel" href="https://domain/@a?ch=b" target="_blank">@domain/a#b</a>'], '']);
135
- assert.deepStrictEqual(inspect(parser('@domain/a#domain/b')), [['@domain/a#domain/b'], '']);
136
135
  assert.deepStrictEqual(inspect(parser('_@a#b')), [['_', '<a class="channel" href="/@a?ch=b">@a#b</a>'], '']);
137
136
  assert.deepStrictEqual(inspect(parser(' @a#b')), [[' ', '<a class="channel" href="/@a?ch=b">@a#b</a>'], '']);
138
137
  });
@@ -152,6 +151,8 @@ describe('Unit: parser/inline', () => {
152
151
  assert.deepStrictEqual(inspect(parser('#a')), [['<a class="hashtag" href="/hashtags/a">#a</a>'], '']);
153
152
  assert.deepStrictEqual(inspect(parser('#a\nb\n#c\n[#d]')), [['<a class="hashtag" href="/hashtags/a">#a</a>', '<br>', 'b', '<br>', '<a class="hashtag" href="/hashtags/c">#c</a>', '<br>', '<a class="index" href="#index::d">d</a>'], '']);
154
153
  assert.deepStrictEqual(inspect(parser('##a')), [['##a'], '']);
154
+ assert.deepStrictEqual(inspect(parser('_#a')), [['_', '<a class="hashtag" href="/hashtags/a">#a</a>'], '']);
155
+ assert.deepStrictEqual(inspect(parser('_#a_')), [['<em><a class="hashtag" href="/hashtags/a">#a</a></em>'], '']);
155
156
  assert.deepStrictEqual(inspect(parser('a#b')), [['a#b'], '']);
156
157
  assert.deepStrictEqual(inspect(parser('0a#b')), [['0a#b'], '']);
157
158
  assert.deepStrictEqual(inspect(parser('あ#b')), [['あ#b'], '']);
@@ -173,6 +174,8 @@ describe('Unit: parser/inline', () => {
173
174
  it('hashnum', () => {
174
175
  assert.deepStrictEqual(inspect(parser('#1')), [['<a class="hashnum">#1</a>'], '']);
175
176
  assert.deepStrictEqual(inspect(parser('#12345678901234567@a')), [['#12345678901234567@a'], '']);
177
+ assert.deepStrictEqual(inspect(parser('_#1_')), [['<em><a class="hashnum">#1</a></em>'], '']);
178
+ assert.deepStrictEqual(inspect(parser('_#1_0')), [['<em><a class="hashnum">#1</a></em>', '0'], '']);
176
179
  assert.deepStrictEqual(inspect(parser('「#1」')), [['「', '<a class="hashnum">#1</a>', '」'], '']);
177
180
  });
178
181
 
@@ -1,6 +1,6 @@
1
1
  import { AnyLineParser, EmptyLineParser, ContentLineParser } from '../source';
2
- import { line, isEmpty } from '../../combinator';
2
+ import { line, isBlank } from '../../combinator';
3
3
 
4
4
  export const anyline: AnyLineParser = line(() => [[], '']);
5
- export const emptyline: EmptyLineParser = line(i => isEmpty(i.source) ? [[], ''] : undefined);
6
- export const contentline: ContentLineParser = line(i => !isEmpty(i.source) ? [[], ''] : undefined);
5
+ export const emptyline: EmptyLineParser = line(i => isBlank(i.source) ? [[], ''] : undefined);
6
+ export const contentline: ContentLineParser = line(i => !isBlank(i.source) ? [[], ''] : undefined);