securemark 0.290.2 → 0.291.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 (51) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/design.md +48 -2
  3. package/dist/index.js +215 -121
  4. package/markdown.d.ts +6 -17
  5. package/package.json +1 -1
  6. package/src/combinator/control/manipulation/surround.ts +17 -13
  7. package/src/combinator/data/parser/context/delimiter.ts +2 -2
  8. package/src/combinator/data/parser/context.ts +4 -3
  9. package/src/combinator/data/parser/some.ts +3 -3
  10. package/src/combinator/data/parser.ts +5 -3
  11. package/src/parser/api/parse.test.ts +34 -30
  12. package/src/parser/block/dlist.ts +1 -1
  13. package/src/parser/block/heading.ts +2 -2
  14. package/src/parser/block/mediablock.ts +2 -2
  15. package/src/parser/block/pagebreak.ts +1 -1
  16. package/src/parser/block/reply.ts +1 -1
  17. package/src/parser/block.ts +3 -1
  18. package/src/parser/header.ts +1 -1
  19. package/src/parser/inline/annotation.ts +4 -5
  20. package/src/parser/inline/autolink/account.ts +1 -1
  21. package/src/parser/inline/autolink/anchor.ts +1 -1
  22. package/src/parser/inline/autolink/channel.ts +1 -1
  23. package/src/parser/inline/autolink/email.ts +1 -1
  24. package/src/parser/inline/autolink/hashnum.ts +1 -1
  25. package/src/parser/inline/autolink/hashtag.ts +1 -1
  26. package/src/parser/inline/autolink/url.test.ts +8 -2
  27. package/src/parser/inline/autolink/url.ts +5 -6
  28. package/src/parser/inline/bracket.ts +25 -3
  29. package/src/parser/inline/code.ts +2 -2
  30. package/src/parser/inline/extension/index.ts +42 -26
  31. package/src/parser/inline/extension/indexee.ts +6 -3
  32. package/src/parser/inline/extension/label.ts +1 -1
  33. package/src/parser/inline/html.test.ts +22 -19
  34. package/src/parser/inline/html.ts +82 -78
  35. package/src/parser/inline/link.ts +12 -9
  36. package/src/parser/inline/mark.ts +1 -1
  37. package/src/parser/inline/math.test.ts +1 -0
  38. package/src/parser/inline/math.ts +18 -9
  39. package/src/parser/inline/media.test.ts +3 -3
  40. package/src/parser/inline/media.ts +14 -6
  41. package/src/parser/inline/reference.ts +67 -10
  42. package/src/parser/inline/ruby.ts +6 -5
  43. package/src/parser/inline/shortmedia.ts +1 -1
  44. package/src/parser/inline.ts +4 -19
  45. package/src/parser/segment.test.ts +3 -2
  46. package/src/parser/segment.ts +2 -2
  47. package/src/parser/source/escapable.ts +1 -1
  48. package/src/parser/source/text.ts +2 -2
  49. package/src/parser/source/unescapable.ts +1 -1
  50. package/src/parser/util.ts +14 -4
  51. package/src/parser/visibility.ts +1 -0
package/markdown.d.ts CHANGED
@@ -723,7 +723,7 @@ export namespace MarkdownParser {
723
723
  // [#index]
724
724
  // [#index|signature]
725
725
  Inline<'extension/index'>,
726
- Parser<HTMLAnchorElement, Context, [
726
+ Parser<string | HTMLElement, Context, [
727
727
  InlineParser,
728
728
  IndexParser.SignatureParser,
729
729
  ]> {
@@ -736,21 +736,10 @@ export namespace MarkdownParser {
736
736
  ]> {
737
737
  }
738
738
  export namespace SignatureParser {
739
- export interface BracketParser extends
740
- Inline<'extension/index/signature/bracket'>,
739
+ export interface InternalParser extends
740
+ Inline<'extension/index/signature/internal'>,
741
741
  Parser<string, Context, [
742
- Parser<string, Context, [
743
- BracketParser,
744
- SourceParser.TxtParser,
745
- ]>,
746
- Parser<string, Context, [
747
- BracketParser,
748
- SourceParser.TxtParser,
749
- ]>,
750
- Parser<string, Context, [
751
- BracketParser,
752
- SourceParser.TxtParser,
753
- ]>,
742
+ UnsafeHTMLEntityParser,
754
743
  SourceParser.TxtParser,
755
744
  ]> {
756
745
  }
@@ -933,7 +922,6 @@ export namespace MarkdownParser {
933
922
  export interface OptionParser extends
934
923
  Inline<'media/parameter/option'>,
935
924
  Parser<string, Context, [
936
- SourceParser.StrParser,
937
925
  SourceParser.StrParser,
938
926
  LinkParser.ParameterParser.OptionParser,
939
927
  ]> {
@@ -982,6 +970,7 @@ export namespace MarkdownParser {
982
970
  Inline<'html/attribute'>,
983
971
  Parser<string, Context, [
984
972
  SourceParser.StrParser,
973
+ SourceParser.StrParser,
985
974
  ]> {
986
975
  }
987
976
  }
@@ -1057,7 +1046,7 @@ export namespace MarkdownParser {
1057
1046
  MathParser.BracketParser,
1058
1047
  Parser<string, Context, [
1059
1048
  MathParser.BracketParser,
1060
- SourceParser.UnescapableSourceParser,
1049
+ SourceParser.EscapableSourceParser,
1061
1050
  ]>,
1062
1051
  ]> {
1063
1052
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.290.2",
3
+ "version": "0.291.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",
@@ -1,4 +1,5 @@
1
1
  import { Parser, Input, Result, Ctx, Node, Context, SubParsers, SubNode, IntermediateParser, eval, exec, check } from '../../data/parser';
2
+ import { consume } from '../../../combinator';
2
3
  import { unshift, push } from 'spica/array';
3
4
 
4
5
  export function surround<P extends Parser<unknown>, S = string>(
@@ -50,21 +51,21 @@ export function surround<N>(
50
51
  const sme_ = source;
51
52
  if (sme_ === '') return;
52
53
  const { linebreak } = context;
53
- context.linebreak = undefined;
54
+ context.linebreak = 0;
54
55
  const resultS = opener({ source: sme_, context });
55
56
  assert(check(sme_, resultS, false));
56
57
  if (resultS === undefined) return void revert(context, linebreak);
57
58
  const nodesS = eval(resultS);
58
59
  const me_ = exec(resultS);
59
- if (getBacktrack(context, backtracks, sme_, sme_.length - me_.length)) return void revert(context, linebreak);
60
+ if (isBacktrack(context, backtracks, sme_, sme_.length - me_.length)) return void revert(context, linebreak);
60
61
  const resultM = me_ !== '' ? parser({ source: me_, context }) : undefined;
61
62
  assert(check(me_, resultM));
62
63
  const nodesM = eval(resultM);
63
- const e_ = exec(resultM, me_);
64
+ const e_ = exec(resultM) ?? me_;
64
65
  const resultE = nodesM || optional ? closer({ source: e_, context }) : undefined;
65
66
  assert(check(e_, resultE, false));
66
67
  const nodesE = eval(resultE);
67
- const rest = exec(resultE, e_);
68
+ const rest = exec(resultE) ?? e_;
68
69
  nodesE || setBacktrack(context, backtracks, sme_.length);
69
70
  if (!nodesM && !optional) return void revert(context, linebreak);
70
71
  if (rest.length === sme_.length) return void revert(context, linebreak);
@@ -81,7 +82,7 @@ export function surround<N>(
81
82
  ? g([nodesS, nodesM!, me_], rest, context)
82
83
  : undefined;
83
84
  if (result) {
84
- context.linebreak ??= linebreak;
85
+ context.linebreak ||= linebreak;
85
86
  }
86
87
  else {
87
88
  revert(context, linebreak);
@@ -119,16 +120,18 @@ export function close<N>(
119
120
  }
120
121
 
121
122
  const statesize = 2;
122
- export function getBacktrack(
123
+ export function isBacktrack(
123
124
  context: Ctx,
124
125
  backtracks: readonly number[],
125
126
  source: string,
126
127
  length: number = 1,
127
128
  ): boolean {
128
- if (length > 0) for (const backtrack of backtracks) {
129
+ if (length === 0 || source.length === 0) return false;
130
+ for (const backtrack of backtracks) {
129
131
  if (backtrack & 1) {
130
132
  const { backtracks = {}, offset = 0 } = context;
131
133
  for (let i = 0; i < length; ++i) {
134
+ assert(i < source.length);
132
135
  if (source[i] !== source[0]) break;
133
136
  const pos = source.length - i + offset - 1;
134
137
  assert(pos >= 0);
@@ -145,8 +148,9 @@ export function setBacktrack(
145
148
  position: number,
146
149
  length: number = 1,
147
150
  ): void {
148
- if (length > 0) for (const backtrack of backtracks) {
149
- if (backtrack & 2) {
151
+ if (length === 0 || position === 0) return;
152
+ for (const backtrack of backtracks) {
153
+ if (backtrack & 2 && position !== 0) {
150
154
  const { backtracks = {}, offset = 0 } = context;
151
155
  for (let i = 0; i < length; ++i) {
152
156
  const pos = position - i + offset - 1;
@@ -162,11 +166,11 @@ function match(pattern: string | RegExp): (input: Input) => [never[], string] |
162
166
  case 'string':
163
167
  return ({ source }) => source.slice(0, pattern.length) === pattern ? [[], source.slice(pattern.length)] : undefined;
164
168
  case 'object':
165
- return ({ source }) => {
169
+ return ({ source, context }) => {
166
170
  const m = source.match(pattern);
167
- return m
168
- ? [[], source.slice(m[0].length)]
169
- : undefined;
171
+ if (m === null) return;
172
+ consume(m[0].length, context);
173
+ return [[], source.slice(m[0].length)];
170
174
  };
171
175
  }
172
176
  }
@@ -38,11 +38,11 @@ export class Delimiters {
38
38
  return source => pattern.test(source) || undefined;
39
39
  }
40
40
  }
41
- private readonly heap: Record<number, Delimiter[]> = {};
41
+ private readonly tree: Record<number, Delimiter[]> = {};
42
42
  private readonly map: Map<string, Delimiter[]> = new Map();
43
43
  private registry(signature: number | string): Delimiter[] {
44
44
  if (typeof signature === 'number') {
45
- return this.heap[signature] ??= [];
45
+ return this.tree[signature] ??= [];
46
46
  }
47
47
  else {
48
48
  const ds = this.map.get(signature);
@@ -50,7 +50,8 @@ function apply<N>(parser: Parser<N>, source: string, context: Ctx, changes: read
50
50
  switch (prop) {
51
51
  case 'resources':
52
52
  assert(reset);
53
- break;
53
+ // プロトタイプに戻ることで戻す
54
+ continue;
54
55
  }
55
56
  context[prop] = values[i];
56
57
  values[i] = undefined;
@@ -135,11 +136,11 @@ export function state<N>(state: number, positive: boolean | Parser<N>, parser?:
135
136
  }
136
137
 
137
138
  export function constraint<P extends Parser<unknown>>(state: number, parser: P): P;
138
- export function constraint<P extends Parser<unknown>>(state: number, positive: boolean, parser: P): P;
139
+ //export function constraint<P extends Parser<unknown>>(state: number, positive: boolean, parser: P): P;
139
140
  export function constraint<N>(state: number, positive: boolean | Parser<N>, parser?: Parser<N>): Parser<N> {
140
141
  if (typeof positive === 'function') {
141
142
  parser = positive;
142
- positive = true;
143
+ positive = false;
143
144
  }
144
145
  assert(state);
145
146
  assert(parser = parser!);
@@ -6,7 +6,7 @@ type DelimiterOption = readonly [delimiter: string | RegExp, precedence: number,
6
6
 
7
7
  export function some<P extends Parser<unknown>>(parser: P, limit?: number): P;
8
8
  export function some<P extends Parser<unknown>>(parser: P, end?: string | RegExp, delimiters?: readonly DelimiterOption[], limit?: number): P;
9
- export function some<N>(parser: Parser<N>, end?: string | RegExp | number, delimiters: readonly DelimiterOption[] = [], limit = -1): Parser<N> {
9
+ export function some<N>(parser: Parser<N>, end?: string | RegExp | number, delimiters: readonly DelimiterOption[] = [], limit = 0): Parser<N> {
10
10
  if (typeof end === 'number') return some(parser, undefined, delimiters, end);
11
11
  assert(parser);
12
12
  assert([end].concat(delimiters.map(o => o[0])).every(d => d instanceof RegExp ? !d.flags.match(/[gmy]/) && d.source.startsWith('^') : true));
@@ -31,7 +31,7 @@ export function some<N>(parser: Parser<N>, end?: string | RegExp | number, delim
31
31
  if (match(rest)) break;
32
32
  if (context.delimiters?.match(rest, context)) break;
33
33
  const result = parser({ source: rest, context });
34
- assert.doesNotThrow(() => limit < 0 && check(rest, result));
34
+ assert.doesNotThrow(() => limit === 0 && check(rest, result));
35
35
  if (result === undefined) break;
36
36
  nodes = nodes
37
37
  ? nodes.length < eval(result).length / 8
@@ -39,7 +39,7 @@ export function some<N>(parser: Parser<N>, end?: string | RegExp | number, delim
39
39
  : push(nodes, eval(result))
40
40
  : eval(result);
41
41
  rest = exec(result);
42
- if (limit >= 0 && source.length - rest.length > limit) break;
42
+ if (limit > 0 && source.length - rest.length > limit) break;
43
43
  }
44
44
  if (delims.length > 0) {
45
45
  context.delimiters!.pop(delims.length);
@@ -19,7 +19,9 @@ export interface Ctx {
19
19
  precedence?: number;
20
20
  delimiters?: Delimiters;
21
21
  state?: number;
22
- depth?: number;
22
+ // Objectの内部実装を利用する。
23
+ // 探索木を直接使用する場合は探索速度が重要で挿入は相対的に少なく削除は不要かつ不確実であるため
24
+ // AVL木が適当と思われる。
23
25
  backtracks?: Record<number, number>;
24
26
  linebreak?: number;
25
27
  recent?: string[];
@@ -42,8 +44,8 @@ function eval_<N>(result: Result<N>, default_?: N[]): N[] | undefined {
42
44
  : default_;
43
45
  }
44
46
 
45
- export function exec(result: NonNullable<Result<unknown>>, default_?: string): string;
46
- export function exec(result: Result<unknown>, default_: string): string
47
+ export function exec(result: NonNullable<Result<unknown>>, default_?: ''): string;
48
+ export function exec(result: Result<unknown>, default_: ''): string
47
49
  export function exec(result: Result<unknown>, default_?: undefined): string | undefined;
48
50
  export function exec(result: Result<unknown>, default_?: string): string | undefined {
49
51
  return result
@@ -288,44 +288,47 @@ describe('Unit: parser/api/parse', () => {
288
288
 
289
289
  it('recursion', () => {
290
290
  assert.deepStrictEqual(
291
- [...parse(`${'{'.repeat(20)}a`).children].map(el => el.outerHTML),
292
- [`<p>${'{'.repeat(20)}a</p>`]);
291
+ [...parse(`${'{'.repeat(20)}0`).children].map(el => el.outerHTML),
292
+ [`<p>${'{'.repeat(20)}0</p>`]);
293
293
  assert.deepStrictEqual(
294
- [...parse(`${'{'.repeat(21)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
294
+ [...parse(`${'{'.repeat(21)}0`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
295
295
  [
296
296
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
297
- `<pre class="error" translate="no">${'{'.repeat(21)}a</pre>`,
297
+ `<pre class="error" translate="no">${'{'.repeat(21)}0</pre>`,
298
298
  ]);
299
299
  assert.deepStrictEqual(
300
- [...parse(`${'('.repeat(20)}a`).children].map(el => el.outerHTML),
301
- [`<p>${'('.repeat(20)}a</p>`]);
300
+ [...parse(`${'('.repeat(20)}0`).children].map(el => el.outerHTML),
301
+ [`<p>${'('.repeat(20)}0</p>`]);
302
302
  assert.deepStrictEqual(
303
- [...parse(`${'('.repeat(21)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
303
+ [...parse(`${'('.repeat(21)}0`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
304
304
  [
305
305
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
306
- `<pre class="error" translate="no">${'('.repeat(21)}a</pre>`,
306
+ `<pre class="error" translate="no">${'('.repeat(21)}0</pre>`,
307
307
  ]);
308
308
  assert.deepStrictEqual(
309
- [...parse(`${'['.repeat(20)}a`).children].map(el => el.outerHTML),
310
- [`<p>${'['.repeat(20)}a</p>`]);
309
+ [...parse(`${'['.repeat(20)}0`).children].map(el => el.outerHTML),
310
+ [`<p>${'['.repeat(20)}0</p>`]);
311
311
  assert.deepStrictEqual(
312
- [...parse(`${'['.repeat(21)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
312
+ [...parse(`${'['.repeat(21)}0`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
313
313
  [
314
314
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
315
- `<pre class="error" translate="no">${'['.repeat(21)}a</pre>`,
315
+ `<pre class="error" translate="no">${'['.repeat(21)}0</pre>`,
316
316
  ]);
317
- assert.deepStrictEqual(
318
- [...parse(`${'['.repeat(20)}\na`).children].map(el => el.outerHTML),
319
- [`<p>${'['.repeat(20)}<br>a</p>`]);
320
317
  });
321
318
 
322
319
  it('recovery', () => {
323
320
  assert.deepStrictEqual(
324
- [...parse(`${'{'.repeat(21)}\n\na`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
321
+ [...parse(`${'['.repeat(20)}0\n\n[a]`).children].map(el => el.outerHTML),
325
322
  [
326
- `<h1 id="error:rnd" class="error">Error: Too much recursion</h1>`,
327
- `<pre class="error" translate="no">${'{'.repeat(21)}\n</pre>`,
328
- '<p>a</p>',
323
+ `<p>${'['.repeat(20)}0</p>`,
324
+ '<p>[a]</p>',
325
+ ]);
326
+ assert.deepStrictEqual(
327
+ [...parse(`${'['.repeat(21)}0\n\n[a]`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
328
+ [
329
+ '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
330
+ `<pre class="error" translate="no">${'['.repeat(21)}0\n</pre>`,
331
+ '<p>[a]</p>',
329
332
  ]);
330
333
  });
331
334
 
@@ -350,22 +353,23 @@ describe('Unit: parser/api/parse', () => {
350
353
 
351
354
  it('backtrack', function () {
352
355
  this.timeout(5000);
353
- // 8n = template + link + annotation/reference + link + code + url + ruby + text
354
- const source = `${'.'.repeat(0 + 0)}{{{((([[[\`http://[${'.'.repeat(12493)}`;
355
- assert.deepStrictEqual(
356
- [...parse(source).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
357
- [`<p>${source}</p>`]);
356
+ // 最悪計算量での実行速度はCommonMarkの公式JS実装の32nに対して5倍遅い程度。
357
+ // 11n = link + link + template + annotation/reference + link +
358
+ // code + signature * 2 + url/math + ruby + text
359
+ const source = `${'.'.repeat(6 + 0)}!{ !{!{{{((([[[\`[#.|$[${'.'.repeat(9082)}]]]]`;
360
+ assert.deepStrictEqual(
361
+ [...parse(source, {}, { resources: { clock: 100000, recursions: [100] } }).children]
362
+ .map(el => el.tagName),
363
+ ['P', 'OL']);
358
364
  });
359
365
 
360
366
  it('backtrack error', function () {
361
367
  this.timeout(5000);
362
- const source = `${'.'.repeat(0 + 1)}{{{((([[[\`http://[${'.'.repeat(12493)}`;
368
+ const source = `${'.'.repeat(6 + 1)}!{ !{!{{{((([[[\`[#.|$[${'.'.repeat(9082)}]]]]`;
363
369
  assert.deepStrictEqual(
364
- [...parse(source).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
365
- [
366
- '<h1 id="error:rnd" class="error">Error: Too many creations</h1>',
367
- `<pre class="error" translate="no">${source.slice(0, 1000 - 3)}...</pre>`,
368
- ]);
370
+ [...parse(source, {}, { resources: { clock: 100000, recursions: [100] } }).children]
371
+ .map(el => el.tagName),
372
+ ['H1', 'PRE']);
369
373
  });
370
374
 
371
375
  });
@@ -11,7 +11,7 @@ import { html, defrag } from 'typed-dom/dom';
11
11
  export const dlist: DListParser = lazy(() => block(fmap(validate(
12
12
  /^~[^\S\n]+(?=\S)/,
13
13
  some(inits([
14
- state(State.annotation | State.reference | State.index | State.label | State.link | State.media,
14
+ state(State.annotation | State.reference | State.index | State.label | State.link,
15
15
  some(term)),
16
16
  some(desc),
17
17
  ]))),
@@ -9,11 +9,11 @@ import { html, defrag } from 'typed-dom/dom';
9
9
 
10
10
  export const segment: HeadingParser.SegmentParser = block(validate('#', focus(
11
11
  /^#+[^\S\n]+\S[^\n]*(?:\n#+(?!\S)[^\n]*)*(?:$|\n)/,
12
- some(line(({ source }) => [[source], ''])), false)));
12
+ some(line(({ source }) => [[source], ''])))));
13
13
 
14
14
  export const heading: HeadingParser = block(rewrite(segment,
15
15
  // その他の表示制御は各所のCSSで行う。
16
- state(State.annotation | State.reference | State.index | State.label | State.link | State.media,
16
+ state(State.annotation | State.reference | State.index | State.label | State.link,
17
17
  line(indexee(fmap(union([
18
18
  open(
19
19
  str(/^##+/),
@@ -15,9 +15,9 @@ export const mediablock: MediaBlockParser = block(validate(['[!', '!'], fmap(
15
15
  medialink,
16
16
  media,
17
17
  shortmedia,
18
- ]), ({ source }) => [[html('div', [html('span', {
18
+ ]), ({ source }) => [[html('span', {
19
19
  class: 'invalid',
20
20
  ...invalid('mediablock', 'syntax', 'Not media syntax'),
21
- }, source.replace('\n', ''))])], '']))),
21
+ }, source.replace('\n', ''))], '']))),
22
22
  ]),
23
23
  ns => [html('div', ns)])));
@@ -4,4 +4,4 @@ import { html } from 'typed-dom/dom';
4
4
 
5
5
  export const pagebreak: PagebreakParser = block(line(focus(
6
6
  /^={3,}[^\S\n]*(?:$|\n)/,
7
- () => [[html('hr')], ''], false)));
7
+ () => [[html('hr')], ''])));
@@ -16,6 +16,6 @@ export const reply: ReplyParser = block(validate(csyntax, fmap(
16
16
  quote,
17
17
  rewrite(
18
18
  some(anyline, delimiter),
19
- visualize(lineable(some(inline), true))),
19
+ visualize(lineable(some(inline), 1))),
20
20
  ])),
21
21
  ns => [html('p', trimBlankNodeEnd(defrag(ns)))])));
@@ -1,6 +1,7 @@
1
1
  import { MarkdownParser } from '../../markdown';
2
2
  import { Recursion, Command } from './context';
3
3
  import { union, reset, open, fallback, recover } from '../combinator';
4
+ import { MAX_SEGMENT_SIZE } from './segment';
4
5
  import { emptyline } from './source';
5
6
  import { pagebreak } from './block/pagebreak';
6
7
  import { heading } from './block/heading';
@@ -40,7 +41,8 @@ export import ParagraphParser = BlockParser.ParagraphParser;
40
41
  export const block: BlockParser = reset(
41
42
  {
42
43
  resources: {
43
- clock: 100000,
44
+ // バックトラックのせいで文字数制限を受けないようにする。
45
+ clock: MAX_SEGMENT_SIZE * 11 + 1,
44
46
  recursions: [
45
47
  10 || Recursion.block,
46
48
  20 || Recursion.blockquote,
@@ -27,7 +27,7 @@ export const header: MarkdownParser.HeaderParser = lazy(() => validate(
27
27
  ...es,
28
28
  ])),
29
29
  ]),
30
- ]), false), false)),
30
+ ]), false))),
31
31
  ({ source }) => [[
32
32
  html('pre', {
33
33
  class: 'invalid',
@@ -5,16 +5,15 @@ import { inline } from '../inline';
5
5
  import { trimBlankStart, trimBlankNodeEnd } from '../visibility';
6
6
  import { html, defrag } from 'typed-dom/dom';
7
7
 
8
- export const annotation: AnnotationParser = lazy(() => constraint(State.annotation, false, surround(
8
+ export const annotation: AnnotationParser = lazy(() => constraint(State.annotation, surround(
9
9
  '((',
10
- precedence(1, state(State.annotation | State.media,
10
+ precedence(1, state(State.annotation,
11
11
  trimBlankStart(some(union([inline]), ')', [[')', 1]])))),
12
12
  '))',
13
13
  false,
14
14
  ([, ns], rest, context) =>
15
- context.linebreak === undefined &&
16
- trimBlankNodeEnd(ns).length > 0
17
- ? [[html('sup', { class: 'annotation' }, [html('span', defrag(ns))])], rest]
15
+ context.linebreak === 0
16
+ ? [[html('sup', { class: 'annotation' }, [html('span', defrag(trimBlankNodeEnd(ns)))])], rest]
18
17
  : undefined,
19
18
  undefined,
20
19
  [3 | Backtrack.doublebracket, 1 | Backtrack.bracket])));
@@ -15,7 +15,7 @@ export const account: AutolinkParser.AccountParser = lazy(() => rewrite(
15
15
  str(/^[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*/i),
16
16
  ])),
17
17
  union([
18
- constraint(State.autolink, false, state(State.autolink, fmap(convert(
18
+ constraint(State.autolink, state(State.autolink, fmap(convert(
19
19
  source =>
20
20
  `[${source}]{ ${source.includes('/')
21
21
  ? `https://${source.slice(1).replace('/', '/@')}`
@@ -18,7 +18,7 @@ export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>',
18
18
  focus(
19
19
  /^>>(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?![0-9a-z@#:])/i,
20
20
  union([
21
- constraint(State.autolink, false, state(State.autolink, fmap(convert(
21
+ constraint(State.autolink, state(State.autolink, fmap(convert(
22
22
  source =>
23
23
  `[${source}]{ ${source.includes('/')
24
24
  ? `/@${source.slice(2).replace('/', '/timeline?at=')}`
@@ -9,7 +9,7 @@ import { define } from 'typed-dom/dom';
9
9
  // https://example/@user?ch=a+b must be a user channel page or a redirect page going there.
10
10
 
11
11
  export const channel: AutolinkParser.ChannelParser = validate('@',
12
- constraint(State.autolink, false, bind(
12
+ constraint(State.autolink, bind(
13
13
  sequence([
14
14
  account,
15
15
  some(hashtag),
@@ -16,7 +16,7 @@ export const email: AutolinkParser.EmailParser = rewrite(
16
16
  false, undefined, undefined,
17
17
  [3 | Backtrack.autolink]),
18
18
  union([
19
- constraint(State.autolink, false, state(State.autolink,
19
+ constraint(State.autolink, state(State.autolink,
20
20
  ({ source }) => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], ''])),
21
21
  ({ source }) => [[source], ''],
22
22
  ]));
@@ -13,7 +13,7 @@ export const hashnum: AutolinkParser.HashnumParser = lazy(() => rewrite(
13
13
  /^[0-9]{1,9}(?![^\p{C}\p{S}\p{P}\s]|emoji)/u.source,
14
14
  ].join('').replace(/emoji/, emoji), 'u'))),
15
15
  union([
16
- constraint(State.autolink, false, state(State.autolink, fmap(convert(
16
+ constraint(State.autolink, state(State.autolink, fmap(convert(
17
17
  source => `[${source}]{ ${source.slice(1)} }`,
18
18
  unsafelink,
19
19
  false),
@@ -20,7 +20,7 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => rewrite(
20
20
  false,
21
21
  [3 | Backtrack.autolink]),
22
22
  union([
23
- constraint(State.autolink, false, state(State.autolink, fmap(convert(
23
+ constraint(State.autolink, state(State.autolink, fmap(convert(
24
24
  source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`,
25
25
  unsafelink,
26
26
  false),
@@ -21,6 +21,7 @@ describe('Unit: parser/inline/autolink/url', () => {
21
21
 
22
22
  it('basic', () => {
23
23
  assert.deepStrictEqual(inspect(parser('http://a')), [['<a class="url" href="http://a" target="_blank">http://a</a>'], '']);
24
+ assert.deepStrictEqual(inspect(parser('http://a/')), [['<a class="url" href="http://a/" target="_blank">http://a/</a>'], '']);
24
25
  assert.deepStrictEqual(inspect(parser('http://a:80')), [['<a class="url" href="http://a:80" target="_blank">http://a:80</a>'], '']);
25
26
  assert.deepStrictEqual(inspect(parser('http://a.b')), [['<a class="url" href="http://a.b" target="_blank">http://a.b</a>'], '']);
26
27
  assert.deepStrictEqual(inspect(parser(`http://a?#${encodeURIComponent(':/[]()<>?#=& ')}`)), [['<a class="url" href="http://a?#%3A%2F%5B%5D()%3C%3E%3F%23%3D%26%20" target="_blank">http://a?#%3A%2F[]()&lt;&gt;%3F%23%3D%26%20</a>'], '']);
@@ -55,13 +56,18 @@ describe('Unit: parser/inline/autolink/url', () => {
55
56
  assert.deepStrictEqual(inspect(parser('http://host=')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '=']);
56
57
  assert.deepStrictEqual(inspect(parser('http://host~')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '~']);
57
58
  assert.deepStrictEqual(inspect(parser('http://host^')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '^']);
59
+ assert.deepStrictEqual(inspect(parser('http://host_')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '_']);
60
+ 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>'], '//']);
58
62
  assert.deepStrictEqual(inspect(parser(`http://host'`)), [['<a class="url" href="http://host\'" target="_blank">http://host\'</a>'], '']);
59
63
  assert.deepStrictEqual(inspect(parser('http://host"')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '"']);
60
64
  assert.deepStrictEqual(inspect(parser('http://host`')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '`']);
61
65
  assert.deepStrictEqual(inspect(parser('http://host|')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '|']);
62
66
  assert.deepStrictEqual(inspect(parser('http://host&')), [['<a class="url" href="http://host&amp;" target="_blank">http://host&amp;</a>'], '']);
63
- assert.deepStrictEqual(inspect(parser('http://host_')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '_']);
64
- assert.deepStrictEqual(inspect(parser('http://host$')), [['<a class="url" href="http://host$" target="_blank">http://host$</a>'], '']);
67
+ assert.deepStrictEqual(inspect(parser('http://host$')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '$']);
68
+ assert.deepStrictEqual(inspect(parser('http://host#"$"')), [['<a class="url" href="http://host#" target="_blank">http://host#</a>'], '"$"']);
69
+ assert.deepStrictEqual(inspect(parser('http://host#($)')), [['<a class="url" href="http://host#" target="_blank">http://host#</a>'], '($)']);
70
+ assert.deepStrictEqual(inspect(parser('http://host#(($))')), [['<a class="url" href="http://host#" target="_blank">http://host#</a>'], '(($))']);
65
71
  assert.deepStrictEqual(inspect(parser('http://user@host')), [['<a class="url" href="http://user@host" target="_blank">http://user@host</a>'], '']);
66
72
  assert.deepStrictEqual(inspect(parser('http://host#@')), [['<a class="url" href="http://host#@" target="_blank">http://host#@</a>'], '']);
67
73
  assert.deepStrictEqual(inspect(parser('http://host[')), [['<a class="url" href="http://host" target="_blank">http://host</a>'], '[']);
@@ -4,19 +4,18 @@ import { union, tails, some, recursion, precedence, state, constraint, validate,
4
4
  import { unsafelink } from '../link';
5
5
  import { linebreak, unescsource, str } from '../../source';
6
6
 
7
- const closer = /^[-+*=~^_,.;:!?]*(?=[\\"`|\[\](){}<>]|[^\x21-\x7E]|$)/;
8
-
9
7
  export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'https://'], rewrite(
10
8
  open(
11
9
  /^https?:\/\/(?=[\x21-\x7E])/,
12
10
  precedence(1, some(union([
13
11
  verify(bracket, ns => ns.length > 0),
14
- some(unescsource, closer),
15
- ]), undefined, [[/^[^\x21-\x7E]/, 3]])),
12
+ // 再帰に注意
13
+ some(unescsource, /^[-+*=~^_/,.;:!?]{2}|^[-+*=~^_,.;:!?]?(?=[\\"`|\[\](){}<>]|[^\x21-\x7E]|$)/),
14
+ ]), undefined, [[/^[^\x21-\x7E]|^\$/, 9]])),
16
15
  false,
17
16
  [3 | Backtrack.autolink]),
18
17
  union([
19
- constraint(State.autolink, false, state(State.autolink, convert(
18
+ constraint(State.autolink, state(State.autolink, convert(
20
19
  url => `{ ${url} }`,
21
20
  unsafelink,
22
21
  false))),
@@ -30,7 +29,7 @@ export const lineurl: AutolinkParser.UrlParser.LineUrlParser = lazy(() => open(
30
29
  tails([
31
30
  str('!'),
32
31
  union([
33
- constraint(State.autolink, false, state(State.autolink, convert(
32
+ constraint(State.autolink, state(State.autolink, convert(
34
33
  url => `{ ${url} }`,
35
34
  unsafelink,
36
35
  false))),
@@ -1,7 +1,8 @@
1
1
  import { BracketParser } from '../inline';
2
- import { Recursion, Backtrack } from '../context';
3
- import { union, some, recursion, precedence, surround, lazy } from '../../combinator';
2
+ import { State, Recursion, Backtrack } from '../context';
3
+ import { union, some, recursion, precedence, surround, isBacktrack, setBacktrack, lazy } from '../../combinator';
4
4
  import { inline } from '../inline';
5
+ import { textlink } from './link';
5
6
  import { str } from '../source';
6
7
  import { unshift, push } from 'spica/array';
7
8
  import { html, defrag } from 'typed-dom/dom';
@@ -41,7 +42,28 @@ export const bracket: BracketParser = lazy(() => union([
41
42
  precedence(1, recursion(Recursion.bracket, some(inline, ']', [[']', 1]]))),
42
43
  str(']'),
43
44
  true,
44
- undefined,
45
+ ([as, bs = [], cs], rest, context) => {
46
+ if (context.state! & State.link) {
47
+ const { recent } = context;
48
+ const head = recent!.reduce((a, b) => a + b.length, rest.length);
49
+ if (context.linebreak! > 0 || rest[0] !== '{') {
50
+ setBacktrack(context, [2 | Backtrack.link], head);
51
+ }
52
+ else {
53
+ context.state! ^= State.link;
54
+ assert(rest.length > 0);
55
+ const result = !isBacktrack(context, [1 | Backtrack.link], rest)
56
+ ? textlink({ source: rest, context })
57
+ : undefined;
58
+ if (!result) {
59
+ setBacktrack(context, [2 | Backtrack.link], head);
60
+ }
61
+ context.state! ^= State.link;
62
+ context.recent = recent;
63
+ }
64
+ }
65
+ return [push(unshift(as, bs), cs), rest];
66
+ },
45
67
  ([as, bs = []], rest) => [unshift(as, bs), rest],
46
68
  [2 | Backtrack.bracket]),
47
69
  surround(