securemark 0.282.0 → 0.283.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 (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/design.md +5 -9
  3. package/dist/index.js +7373 -7195
  4. package/markdown.d.ts +2 -2
  5. package/package.json +2 -2
  6. package/src/combinator/control/manipulation/convert.ts +4 -8
  7. package/src/combinator/control/manipulation/indent.ts +3 -5
  8. package/src/combinator/control/manipulation/scope.ts +6 -3
  9. package/src/combinator/control/manipulation/surround.ts +31 -7
  10. package/src/combinator/data/parser/context.test.ts +6 -6
  11. package/src/combinator/data/parser/context.ts +25 -39
  12. package/src/combinator/data/parser.ts +2 -3
  13. package/src/parser/api/bind.ts +6 -6
  14. package/src/parser/api/parse.test.ts +35 -28
  15. package/src/parser/api/parse.ts +0 -3
  16. package/src/parser/block/blockquote.ts +4 -3
  17. package/src/parser/block/dlist.test.ts +1 -1
  18. package/src/parser/block/dlist.ts +6 -6
  19. package/src/parser/block/extension/aside.ts +4 -3
  20. package/src/parser/block/extension/example.ts +4 -3
  21. package/src/parser/block/extension/table.ts +7 -7
  22. package/src/parser/block/heading.ts +3 -2
  23. package/src/parser/block/ilist.ts +2 -1
  24. package/src/parser/block/olist.ts +4 -3
  25. package/src/parser/block/reply/cite.ts +3 -3
  26. package/src/parser/block/reply/quote.ts +3 -3
  27. package/src/parser/block/sidefence.ts +2 -1
  28. package/src/parser/block/table.ts +9 -9
  29. package/src/parser/block/ulist.ts +6 -5
  30. package/src/parser/block.ts +16 -5
  31. package/src/parser/context.ts +19 -21
  32. package/src/parser/inline/annotation.test.ts +3 -1
  33. package/src/parser/inline/annotation.ts +6 -5
  34. package/src/parser/inline/autolink/email.ts +2 -1
  35. package/src/parser/inline/autolink/url.ts +6 -5
  36. package/src/parser/inline/autolink.ts +2 -2
  37. package/src/parser/inline/bracket.ts +17 -17
  38. package/src/parser/inline/code.test.ts +1 -1
  39. package/src/parser/inline/code.ts +2 -1
  40. package/src/parser/inline/deletion.ts +3 -3
  41. package/src/parser/inline/emphasis.ts +3 -3
  42. package/src/parser/inline/emstrong.ts +3 -3
  43. package/src/parser/inline/extension/index.ts +22 -10
  44. package/src/parser/inline/extension/indexee.ts +2 -2
  45. package/src/parser/inline/extension/indexer.ts +2 -1
  46. package/src/parser/inline/extension/label.ts +2 -2
  47. package/src/parser/inline/extension/placeholder.ts +4 -4
  48. package/src/parser/inline/html.ts +2 -2
  49. package/src/parser/inline/htmlentity.ts +2 -1
  50. package/src/parser/inline/insertion.ts +3 -3
  51. package/src/parser/inline/link.test.ts +2 -2
  52. package/src/parser/inline/link.ts +12 -8
  53. package/src/parser/inline/mark.ts +3 -3
  54. package/src/parser/inline/math.test.ts +3 -3
  55. package/src/parser/inline/math.ts +6 -5
  56. package/src/parser/inline/media.test.ts +1 -1
  57. package/src/parser/inline/media.ts +16 -11
  58. package/src/parser/inline/reference.test.ts +3 -0
  59. package/src/parser/inline/reference.ts +7 -6
  60. package/src/parser/inline/remark.ts +2 -2
  61. package/src/parser/inline/ruby.ts +13 -12
  62. package/src/parser/inline/strong.ts +3 -3
  63. package/src/parser/inline/template.ts +11 -9
  64. package/src/parser/inline.test.ts +1 -2
  65. package/src/parser/inline.ts +1 -0
  66. package/src/parser/source/escapable.ts +2 -1
  67. package/src/parser/source/str.ts +3 -2
  68. package/src/parser/source/text.ts +2 -1
  69. package/src/parser/source/unescapable.ts +2 -1
  70. package/src/util/quote.ts +6 -8
  71. package/src/combinator/data/parser/context/memo.ts +0 -57
package/markdown.d.ts CHANGED
@@ -819,8 +819,8 @@ export namespace MarkdownParser {
819
819
  // [AB](a b)
820
820
  Inline<'ruby'>,
821
821
  Parser<HTMLElement, Context, [
822
- SourceParser.StrParser,
823
- SourceParser.StrParser,
822
+ Parser<string[], Context, []>,
823
+ Parser<string[], Context, []>,
824
824
  ]> {
825
825
  }
826
826
  export namespace RubyParser {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.282.0",
3
+ "version": "0.283.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",
@@ -28,7 +28,7 @@
28
28
  "LICENSE"
29
29
  ],
30
30
  "dependencies": {
31
- "spica": "0.0.804"
31
+ "spica": "0.0.805"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/dompurify": "3.0.5",
@@ -1,8 +1,5 @@
1
1
  import { Parser, Ctx, Context, check } from '../../data/parser';
2
- import { max } from 'spica/alias';
3
2
 
4
- // 設計上キャッシュが汚染されるが運用で回避可能
5
- // 変換の前または後のみキャッシュされるなら問題ない
6
3
  export function convert<P extends Parser<unknown>>(conv: (source: string, context: Context<P>) => string, parser: P, empty?: boolean): P;
7
4
  export function convert<T>(conv: (source: string, context: Ctx) => string, parser: Parser<T>, empty = false): Parser<T> {
8
5
  assert(parser);
@@ -10,13 +7,12 @@ export function convert<T>(conv: (source: string, context: Ctx) => string, parse
10
7
  if (source === '') return;
11
8
  const src = conv(source, context);
12
9
  if (src === '') return empty ? [[], ''] : undefined;
13
- const offset = max(source.length - src.length, 0);
14
- assert(offset >= 0);
15
- context.offset ??= 0;
16
- context.offset += offset;
10
+ const sub = source.endsWith(src);
11
+ const { logger } = context;
12
+ context.logger = sub ? logger : {};
17
13
  const result = parser({ source: src, context });
18
14
  assert(check(src, result));
19
- context.offset -= offset;
15
+ context.logger = logger;
20
16
  return result;
21
17
  };
22
18
  }
@@ -20,12 +20,10 @@ export function indent<T>(opener: RegExp | Parser<T>, parser?: Parser<T> | boole
20
20
  ([indent]) => indent.length * 2 + +(indent[0] === ' '), {})), separation),
21
21
  (lines, rest, context) => {
22
22
  assert(parser = parser as Parser<T>);
23
- const offset = rest.length;
24
- assert(offset >= 0);
25
- context.offset ??= 0;
26
- context.offset += offset;
23
+ const { logger } = context;
24
+ context.logger = {};
27
25
  const result = parser({ source: trimBlockEnd(lines.join('')), context });
28
- context.offset -= offset;
26
+ context.logger = logger;
29
27
  return result && exec(result) === ''
30
28
  ? [eval(result), rest]
31
29
  : undefined;
@@ -34,11 +34,14 @@ export function rewrite<T>(scope: Parser<unknown>, parser: Parser<T>): Parser<T>
34
34
  assert(parser);
35
35
  return ({ source, context }) => {
36
36
  if (source === '') return;
37
- const memo = context.memo;
38
- context.memo = undefined;
37
+ const { logger } = context;
38
+ context.logger = {};
39
+ //const { resources = { clock: 0 } } = context;
40
+ //const clock = resources.clock;
39
41
  const res1 = scope({ source, context });
40
42
  assert(check(source, res1));
41
- context.memo = memo;
43
+ //resources.clock = clock;
44
+ context.logger = logger;
42
45
  if (res1 === undefined || exec(res1).length >= source.length) return;
43
46
  const src = source.slice(0, source.length - exec(res1).length);
44
47
  assert(src !== '');
@@ -3,39 +3,49 @@ import { fmap } from '../monad/fmap';
3
3
  import { unshift, push } from 'spica/array';
4
4
 
5
5
  export function surround<P extends Parser<unknown>, S = string>(
6
- opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
6
+ opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>,
7
+ optional?: false,
7
8
  f?: (rss: [S[], SubTree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
8
9
  g?: (rss: [S[], SubTree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
10
+ log?: number,
9
11
  ): P;
10
12
  export function surround<P extends Parser<unknown>, S = string>(
11
- opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
13
+ opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>,
14
+ optional?: boolean,
12
15
  f?: (rss: [S[], SubTree<P>[] | undefined, S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
13
16
  g?: (rss: [S[], SubTree<P>[] | undefined, string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
17
+ log?: number,
14
18
  ): P;
15
19
  export function surround<P extends Parser<unknown>, S = string>(
16
- opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
20
+ opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>,
21
+ optional?: false,
17
22
  f?: (rss: [S[], Tree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
18
23
  g?: (rss: [S[], Tree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
24
+ log?: number,
19
25
  ): P;
20
26
  export function surround<P extends Parser<unknown>, S = string>(
21
- opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
27
+ opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>,
28
+ optional?: boolean,
22
29
  f?: (rss: [S[], Tree<P>[] | undefined, S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
23
30
  g?: (rss: [S[], Tree<P>[] | undefined, string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
31
+ log?: number,
24
32
  ): P;
25
33
  export function surround<T>(
26
- opener: string | RegExp | Parser<T>, parser: Parser<T>, closer: string | RegExp | Parser<T>, optional: boolean = false,
34
+ opener: string | RegExp | Parser<T>, parser: Parser<T>, closer: string | RegExp | Parser<T>,
35
+ optional: boolean = false,
27
36
  f?: (rss: [T[], T[], T[]], rest: string, context: Ctx) => Result<T>,
28
37
  g?: (rss: [T[], T[], string], rest: string, context: Ctx) => Result<T>,
38
+ log: number = 0,
29
39
  ): Parser<T> {
30
40
  switch (typeof opener) {
31
41
  case 'string':
32
42
  case 'object':
33
- return surround(match(opener), parser, closer, optional, f, g);
43
+ opener = match(opener);
34
44
  }
35
45
  switch (typeof closer) {
36
46
  case 'string':
37
47
  case 'object':
38
- return surround(opener, parser, match(closer), optional, f, g);
48
+ closer = match(closer);
39
49
  }
40
50
  return ({ source, context }) => {
41
51
  const lmr_ = source;
@@ -45,6 +55,16 @@ export function surround<T>(
45
55
  if (res1 === undefined) return;
46
56
  const rl = eval(res1);
47
57
  const mr_ = exec(res1);
58
+ if (log & 1) {
59
+ const { logger = {}, offset = 0 } = context;
60
+ for (let i = 0; i < source.length - mr_.length; ++i) {
61
+ if (source[i] !== source[0]) break;
62
+ const j = source.length + offset - i;
63
+ if (!(j in logger)) continue;
64
+ assert(log >>> 2);
65
+ if (logger[j] & 1 << (log >>> 2)) return;
66
+ }
67
+ }
48
68
  const res2 = mr_ !== '' ? parser({ source: mr_, context }) : undefined;
49
69
  assert(check(mr_, res2));
50
70
  const rm = eval(res2);
@@ -55,6 +75,10 @@ export function surround<T>(
55
75
  const rr = eval(res3);
56
76
  const rest = exec(res3, r_);
57
77
  if (rest.length === lmr_.length) return;
78
+ if (log & 2 && rr === undefined) {
79
+ const { logger = {}, offset = 0 } = context;
80
+ logger[source.length + offset] |= 1 << (log >>> 2);
81
+ }
58
82
  return rr
59
83
  ? f
60
84
  ? f([rl, rm!, rr], rest, context)
@@ -9,11 +9,11 @@ describe('Unit: combinator/data/parser/context', () => {
9
9
  }
10
10
 
11
11
  describe('reset', () => {
12
- const parser: Parser<number> = some(creation(
12
+ const parser: Parser<number> = some(creation(1, 1,
13
13
  ({ source, context }) => [[context.resources?.clock ?? NaN], source.slice(1)]));
14
14
 
15
15
  it('root', () => {
16
- const base: Context = { resources: { clock: 3, recursion: 1 } };
16
+ const base: Context = { resources: { clock: 3, recursions: [1] } };
17
17
  const ctx: Context = {};
18
18
  assert.deepStrictEqual(reset(base, parser)({ source: '123', context: ctx }), [[3, 2, 1], '']);
19
19
  assert(base.resources?.clock === 3);
@@ -24,8 +24,8 @@ describe('Unit: combinator/data/parser/context', () => {
24
24
  });
25
25
 
26
26
  it('node', () => {
27
- const base: Context = { resources: { clock: 3, recursion: 1 } };
28
- const ctx: Context = { resources: { clock: 1, recursion: 1 } };
27
+ const base: Context = { resources: { clock: 3, recursions: [1] } };
28
+ const ctx: Context = { resources: { clock: 1, recursions: [1] } };
29
29
  assert.deepStrictEqual(reset(base, parser)({ source: '1', context: ctx }), [[1], '']);
30
30
  assert(base.resources?.clock === 3);
31
31
  assert(ctx.resources?.clock === 0);
@@ -36,12 +36,12 @@ describe('Unit: combinator/data/parser/context', () => {
36
36
  });
37
37
 
38
38
  describe('context', () => {
39
- const parser: Parser<boolean, Context> = some(creation(
39
+ const parser: Parser<boolean, Context> = some(creation(1, 1,
40
40
  ({ source, context }) => [[context.status!], source.slice(1)]));
41
41
 
42
42
  it('', () => {
43
43
  const base: Context = { status: true };
44
- const ctx: Context = { resources: { clock: 3, recursion: 1 } };
44
+ const ctx: Context = { resources: { clock: 3, recursions: [1] } };
45
45
  assert.deepStrictEqual(context(base, parser)({ source: '123', context: ctx }), [[true, true, true], '']);
46
46
  assert(ctx.resources?.clock === 0);
47
47
  assert(ctx.status === undefined);
@@ -1,6 +1,6 @@
1
- import { ObjectCreate } from 'spica/alias';
2
- import { Parser, Result, Ctx, Tree, Context, eval, exec } from '../../data/parser';
3
- import { Memo } from './context/memo';
1
+ import { ObjectCreate, min } from 'spica/alias';
2
+ import { Parser, Result, Ctx, Tree, Context } from '../../data/parser';
3
+ import { clone } from 'spica/assign';
4
4
 
5
5
  export function reset<P extends Parser<unknown>>(base: Context<P>, parser: P): P;
6
6
  export function reset<T>(base: Ctx, parser: Parser<T>): Parser<T> {
@@ -24,7 +24,9 @@ export function context<T>(base: Ctx, parser: Parser<T>): Parser<T> {
24
24
 
25
25
  function apply<P extends Parser<unknown>>(parser: P, source: string, context: Context<P>, changes: readonly [string, unknown][], values: unknown[], reset?: boolean): Result<Tree<P>>;
26
26
  function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: readonly [string, unknown][], values: unknown[], reset = false): Result<T> {
27
- reset && context.memo?.clear();
27
+ if (reset) {
28
+ context.logger = {};
29
+ }
28
30
  for (let i = 0; i < changes.length; ++i) {
29
31
  const change = changes[i];
30
32
  const prop = change[0];
@@ -35,7 +37,7 @@ function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: read
35
37
  assert(!context.precedence);
36
38
  assert(!context.delimiters);
37
39
  assert(!context.state);
38
- context[prop as string] ??= ObjectCreate(change[1] as object);
40
+ context[prop as string] ??= clone({}, change[1] as object);
39
41
  continue;
40
42
  }
41
43
  values[i] = context[prop];
@@ -56,51 +58,35 @@ function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: read
56
58
  return result;
57
59
  }
58
60
 
59
- export function syntax<P extends Parser<unknown>>(syntax: number, precedence: number, state: number, parser: P): P;
60
- export function syntax<T>(syntax: number, prec: number, state: number, parser: Parser<T>): Parser<T> {
61
+ export function syntax<P extends Parser<unknown>>(precedence: number, state: number, parser: P): P;
62
+ export function syntax<T>(prec: number, state: number, parser: Parser<T>): Parser<T> {
61
63
  return precedence(prec, ({ source, context }) => {
62
64
  if (source === '') return;
63
- const memo = context.memo ??= new Memo();
64
65
  context.offset ??= 0;
65
- const position = source.length + context.offset!;
66
66
  const stateOuter = context.state ?? 0;
67
- const stateInner = context.state = stateOuter | state;
68
- const cache = syntax & memo.targets && stateInner && memo.get(position, syntax, stateInner);
69
- const result: Result<T> = cache
70
- ? cache.length === 0
71
- ? undefined
72
- : [cache[0] as T[], source.slice(cache[1])]
73
- : parser({ source, context });
74
- if (stateOuter && !cache && syntax & memo.targets) {
75
- memo.set(position, syntax, stateInner, eval(result), source.length - exec(result, '').length);
76
- }
77
- else if (!stateOuter && result && memo.length >= position + memo.margin) {
78
- memo.resize(position + memo.margin);
79
- }
67
+ context.state = stateOuter | state;
68
+ const result: Result<T> = parser({ source, context });
80
69
  context.state = stateOuter;
81
70
  return result;
82
71
  });
83
72
  }
84
73
 
85
- export function creation<P extends Parser<unknown>>(parser: P): P;
86
- export function creation<P extends Parser<unknown>>(cost: number, parser: P): P;
87
- export function creation<P extends Parser<unknown>>(cost: number, recursion: boolean, parser: P): P;
88
- export function creation(cost: number | Parser<unknown>, recursion?: boolean | Parser<unknown>, parser?: Parser<unknown>): Parser<unknown> {
89
- if (typeof cost === 'function') return creation(1, true, cost);
90
- if (typeof recursion === 'function') return creation(cost, true, recursion);
74
+ export function creation<P extends Parser<unknown>>(cost: number, recursion: number, parser: P): P;
75
+ export function creation(cost: number, recursion: number, parser: Parser<unknown>): Parser<unknown> {
91
76
  assert(cost >= 0);
92
- assert(recursion !== undefined);
77
+ assert(recursion >= 0);
93
78
  return ({ source, context }) => {
94
- assert([recursion = recursion!]);
95
- const resources = context.resources ?? { clock: cost || 1, recursion: 1 };
96
- if (resources.clock <= 0) throw new Error('Too many creations');
97
- if (resources.recursion < +recursion) throw new Error('Too much recursion');
98
- recursion && --resources.recursion;
99
- const result = parser!({ source, context });
100
- recursion && ++resources.recursion;
101
- if (result) {
102
- resources.clock -= cost;
103
- }
79
+ const resources = context.resources ?? { clock: cost || 1, recursions: [1] };
80
+ const { recursions } = resources;
81
+ assert(recursions.length > 0);
82
+ const rec = min(recursion, recursions.length);
83
+ if (rec > 0 && recursions[rec - 1] < 1) throw new Error('Too much recursion');
84
+ rec > 0 && --recursions[rec - 1];
85
+ const result = parser({ source, context });
86
+ rec > 0 && ++recursions[rec - 1];
87
+ if (result === undefined) return;
88
+ if (resources.clock < cost) throw new Error('Too many creations');
89
+ resources.clock -= cost;
104
90
  return result;
105
91
  };
106
92
  }
@@ -1,5 +1,4 @@
1
1
  import { Delimiters } from './parser/context/delimiter';
2
- import { Memo } from './parser/context/memo';
3
2
 
4
3
  export type Parser<T, C extends Ctx = Ctx, D extends Parser<unknown, C>[] = any>
5
4
  = (input: Input<C>) => Result<T, C, D>;
@@ -14,13 +13,13 @@ export type Result<T, C extends Ctx = Ctx, D extends Parser<unknown, C>[] = any>
14
13
  export interface Ctx {
15
14
  readonly resources?: {
16
15
  clock: number;
17
- recursion: number;
16
+ recursions: number[];
18
17
  };
19
18
  offset?: number;
20
19
  precedence?: number;
21
20
  delimiters?: Delimiters;
22
21
  state?: number;
23
- memo?: Memo;
22
+ logger?: Record<number, number>;
24
23
  }
25
24
  export type Tree<P extends Parser<unknown>> = P extends Parser<infer T> ? T : never;
26
25
  export type SubParsers<P extends Parser<unknown>> = P extends Parser<unknown, Ctx, infer D> ? D : never;
@@ -1,8 +1,6 @@
1
1
  import { ParserSettings, Progress } from '../../..';
2
2
  import { MarkdownParser } from '../../../markdown';
3
3
  import { eval } from '../../combinator/data/parser';
4
- import { Memo } from '../../combinator/data/parser/context/memo';
5
- import { Syntax, Margin } from '../context';
6
4
  import { segment, validate, MAX_INPUT_SIZE } from '../segment';
7
5
  import { header } from '../header';
8
6
  import { block } from '../block';
@@ -24,7 +22,6 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
24
22
  let context: MarkdownParser.Context = {
25
23
  ...settings,
26
24
  host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
27
- memo: new Memo(Syntax.targets, Margin),
28
25
  };
29
26
  assert(!context.offset);
30
27
  assert(!context.precedence);
@@ -87,9 +84,10 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
87
84
  // All deletion processes always run after all addition processes have done.
88
85
  // Therefore any `base` node will never be unavailable by deletions until all the dependent `el` nodes are added.
89
86
  push(adds, es.map(el => [el, base] as const));
87
+ adds.reverse();
90
88
  while (adds.length > 0) {
91
89
  assert(rev === revision);
92
- const [el, base] = adds.shift()!;
90
+ const [el, base] = adds.pop()!;
93
91
  target.insertBefore(el, base);
94
92
  assert(el.parentNode);
95
93
  yield { type: 'block', value: el };
@@ -103,17 +101,19 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
103
101
  push(dels, es.map(el => [el]));
104
102
  }
105
103
  assert(blocks.length === sourceSegments.length);
104
+ adds.reverse();
106
105
  while (adds.length > 0) {
107
106
  assert(rev === revision);
108
- const [el, base] = adds.shift()!;
107
+ const [el, base] = adds.pop()!;
109
108
  target.insertBefore(el, base);
110
109
  assert(el.parentNode);
111
110
  yield { type: 'block', value: el };
112
111
  if (rev !== revision) return yield { type: 'cancel' };
113
112
  }
113
+ dels.reverse();
114
114
  while (dels.length > 0) {
115
115
  assert(rev === revision);
116
- const [el] = dels.shift()!;
116
+ const [el] = dels.pop()!;
117
117
  el.parentNode?.removeChild(el);
118
118
  assert(!el.parentNode);
119
119
  yield { type: 'block', value: el };
@@ -286,47 +286,47 @@ describe('Unit: parser/api/parse', () => {
286
286
  ['<p>a<br>b</p>']);
287
287
  });
288
288
 
289
- it('backtrack', () => {
289
+ it('recursion', () => {
290
+ assert.deepStrictEqual(
291
+ [...parse(`${'{'.repeat(20)}a`).children].map(el => el.outerHTML),
292
+ [`<p>${'{'.repeat(20)}a</p>`]);
290
293
  assert.deepStrictEqual(
291
- [...parse('"[% '.repeat(100) + '\n\na').children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
294
+ [...parse(`${'{'.repeat(21)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
292
295
  [
293
296
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
294
- `<pre class="error" translate="no">${'"[% '.repeat(100)}\n</pre>`,
295
- '<p>a</p>',
297
+ `<pre class="error" translate="no">${'{'.repeat(21)}a</pre>`,
296
298
  ]);
297
- });
298
-
299
- it('recursion', () => {
300
299
  assert.deepStrictEqual(
301
- [...parse('{'.repeat(21)).children].map(el => el.outerHTML),
302
- [`<p>${'{'.repeat(21)}</p>`]);
300
+ [...parse(`${'('.repeat(22)}a`).children].map(el => el.outerHTML),
301
+ [`<p>${'('.repeat(22)}a</p>`]);
303
302
  assert.deepStrictEqual(
304
- [...parse('{'.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
303
+ [...parse(`${'('.repeat(23)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
305
304
  [
306
305
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
307
- `<pre class="error" translate="no">${'{'.repeat(22)}</pre>`,
306
+ `<pre class="error" translate="no">${'('.repeat(23)}a</pre>`,
308
307
  ]);
309
308
  assert.deepStrictEqual(
310
- [...parse('('.repeat(21)).children].map(el => el.outerHTML),
311
- [`<p>${'('.repeat(21)}</p>`]);
309
+ [...parse(`${'['.repeat(23)}a`).children].map(el => el.outerHTML),
310
+ [`<p>${'['.repeat(23)}a</p>`]);
312
311
  assert.deepStrictEqual(
313
- [...parse('('.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
312
+ [...parse(`${'['.repeat(24)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
314
313
  [
315
314
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
316
- `<pre class="error" translate="no">${'('.repeat(22)}</pre>`,
315
+ `<pre class="error" translate="no">${'['.repeat(24)}a</pre>`,
317
316
  ]);
318
317
  assert.deepStrictEqual(
319
- [...parse('['.repeat(21)).children].map(el => el.outerHTML),
320
- [`<p>${'['.repeat(21)}</p>`]);
318
+ [...parse(`${'['.repeat(22)}\na`).children].map(el => el.outerHTML),
319
+ [`<p>${'['.repeat(22)}<br>a</p>`]);
320
+ });
321
+
322
+ it('recovery', () => {
321
323
  assert.deepStrictEqual(
322
- [...parse('['.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
324
+ [...parse(`${'{'.repeat(21)}\n\na`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
323
325
  [
324
- '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
325
- `<pre class="error" translate="no">${'['.repeat(22)}</pre>`,
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>',
326
329
  ]);
327
- assert.deepStrictEqual(
328
- [...parse('['.repeat(20) + '\na').children].map(el => el.outerHTML),
329
- [`<p>${'['.repeat(20)}<br>a</p>`]);
330
330
  });
331
331
 
332
332
  if (!navigator.userAgent.includes('Chrome')) return;
@@ -344,17 +344,24 @@ describe('Unit: parser/api/parse', () => {
344
344
  [...parse('.'.repeat(20001)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
345
345
  [
346
346
  '<h1 id="error:rnd" class="error">Error: Too many creations</h1>',
347
- `<pre class="error" translate="no">${'.'.repeat(1000).slice(0, 997)}...</pre>`,
347
+ `<pre class="error" translate="no">${'.'.repeat(1000 - 3)}...</pre>`,
348
348
  ]);
349
349
  });
350
350
 
351
- it('recovery', function () {
351
+ it('backtrack', function () {
352
+ this.timeout(5000);
353
+ assert.deepStrictEqual(
354
+ [...parse(`(({{${'['.repeat(19)}${'.'.repeat(3301)}`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
355
+ [`<p>(({{${'['.repeat(19)}${'.'.repeat(3301)}</p>`]);
356
+ });
357
+
358
+ it('backtrack error', function () {
352
359
  this.timeout(5000);
353
360
  assert.deepStrictEqual(
354
- [...parse(`!>> ${'['.repeat(20)}${'{a}'.repeat(518)}\n> ${'{a}'.repeat(4)}\n\na`).children].map(el => el.outerHTML),
361
+ [...parse(`(({{${'['.repeat(19)}${'.'.repeat(3302)}`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
355
362
  [
356
- `<blockquote><blockquote><section><p>${'['.repeat(20)}${'<a class="url" href="a">a</a>'.repeat(518)}</p><h2>References</h2><ol class="references"></ol></section></blockquote><section><h1 class="error">Error: Too many creations</h1><pre class="error" translate="no">{a}{a}{a}{a}</pre><h2>References</h2><ol class="references"></ol></section></blockquote>`,
357
- '<p>a</p>',
363
+ '<h1 id="error:rnd" class="error">Error: Too many creations</h1>',
364
+ `<pre class="error" translate="no">(({{${'['.repeat(19)}${'.'.repeat(1000 - 4 - 19 - 3)}...</pre>`,
358
365
  ]);
359
366
  });
360
367
 
@@ -1,8 +1,6 @@
1
1
  import { ParserOptions } from '../../..';
2
2
  import { MarkdownParser } from '../../../markdown';
3
3
  import { eval } from '../../combinator/data/parser';
4
- import { Memo } from '../../combinator/data/parser/context/memo';
5
- import { Syntax, Margin } from '../context';
6
4
  import { segment, validate, MAX_SEGMENT_SIZE } from '../segment';
7
5
  import { header } from '../header';
8
6
  import { block } from '../block';
@@ -27,7 +25,6 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
27
25
  id: opts.id ?? context?.id,
28
26
  caches: context?.caches,
29
27
  resources: context?.resources,
30
- memo: new Memo(Syntax.targets, Margin),
31
28
  };
32
29
  assert(!context.offset);
33
30
  assert(!context.precedence);
@@ -2,6 +2,7 @@ import { BlockquoteParser } from '../block';
2
2
  import { union, some, creation, block, validate, rewrite, open, convert, lazy, fmap } from '../../combinator';
3
3
  import { autolink } from '../autolink';
4
4
  import { contentline } from '../source';
5
+ import { Recursion } from '../context';
5
6
  import { parse } from '../api/parse';
6
7
  import { html, defrag } from 'typed-dom/dom';
7
8
 
@@ -19,7 +20,7 @@ const indent = block(open(opener, some(contentline, /^>(?:$|\s)/)), false);
19
20
  const unindent = (source: string) => source.replace(/(?<=^|\n)>(?:[^\S\n]|(?=>*(?:$|\s)))|\n$/g, '');
20
21
 
21
22
  const source: BlockquoteParser.SourceParser = lazy(() => fmap(
22
- some(creation(1, false, union([
23
+ some(creation(0, Recursion.blockquote, union([
23
24
  rewrite(
24
25
  indent,
25
26
  convert(unindent, source, true)),
@@ -30,11 +31,11 @@ const source: BlockquoteParser.SourceParser = lazy(() => fmap(
30
31
  ns => [html('blockquote', ns)]));
31
32
 
32
33
  const markdown: BlockquoteParser.MarkdownParser = lazy(() => fmap(
33
- some(creation(1, false, union([
34
+ some(creation(0, Recursion.blockquote, union([
34
35
  rewrite(
35
36
  indent,
36
37
  convert(unindent, markdown, true)),
37
- creation(99, false,
38
+ creation(10, Recursion.ignore,
38
39
  rewrite(
39
40
  some(contentline, opener),
40
41
  convert(unindent, ({ source, context }) => {
@@ -64,7 +64,7 @@ describe('Unit: parser/block/dlist', () => {
64
64
  assert.deepStrictEqual(inspect(parser('~ a\n: b\n~ c\n: d\n~ e\n: f')), [['<dl><dt id="index::a">a</dt><dd>b</dd><dt id="index::c">c</dt><dd>d</dd><dt id="index::e">e</dt><dd>f</dd></dl>'], '']);
65
65
  });
66
66
 
67
- it('index', () => {
67
+ it('indexer', () => {
68
68
  assert.deepStrictEqual(inspect(parser('~ a [|b]')), [['<dl><dt id="index::b">a<span class="indexer" data-index="b"></span></dt><dd></dd></dl>'], '']);
69
69
  assert.deepStrictEqual(inspect(parser('~ a [|b]\\')), [['<dl><dt id="index::a_b">a <span class="invalid">b</span></dt><dd></dd></dl>'], '']);
70
70
  assert.deepStrictEqual(inspect(parser('~ A')), [['<dl><dt id="index::A">A</dt><dd></dd></dl>'], '']);
@@ -1,6 +1,6 @@
1
1
  import { DListParser } from '../block';
2
- import { union, inits, some, creation, state, block, line, validate, rewrite, open, lazy, fmap } from '../../combinator';
3
- import { inline, indexee, indexer } from '../inline';
2
+ import { union, inits, some, state, block, line, validate, rewrite, open, lazy, fmap } from '../../combinator';
3
+ import { inline, indexee, indexer, dataindex } from '../inline';
4
4
  import { anyline } from '../source';
5
5
  import { State } from '../context';
6
6
  import { lineable } from '../util';
@@ -17,20 +17,20 @@ export const dlist: DListParser = lazy(() => block(fmap(validate(
17
17
  ]))),
18
18
  es => [html('dl', fillTrailingDescription(es))])));
19
19
 
20
- const term: DListParser.TermParser = creation(1, false, line(indexee(fmap(open(
20
+ const term: DListParser.TermParser = line(indexee(fmap(open(
21
21
  /^~[^\S\n]+(?=\S)/,
22
22
  visualize(trimBlankStart(some(union([indexer, inline])))),
23
23
  true),
24
- ns => [html('dt', trimNodeEnd(defrag(ns)))]))));
24
+ ns => [html('dt', { 'data-index': dataindex(ns) }, trimNodeEnd(defrag(ns)))])));
25
25
 
26
- const desc: DListParser.DescriptionParser = creation(1, false, block(fmap(open(
26
+ const desc: DListParser.DescriptionParser = block(fmap(open(
27
27
  /^:[^\S\n]+(?=\S)|/,
28
28
  rewrite(
29
29
  some(anyline, /^[~:][^\S\n]+\S/),
30
30
  visualize(lineable(some(union([inline]))))),
31
31
  true),
32
32
  ns => [html('dd', trimNodeEnd(defrag(ns)))]),
33
- false));
33
+ false);
34
34
 
35
35
  function fillTrailingDescription(es: HTMLElement[]): HTMLElement[] {
36
36
  return es.length > 0 && es[es.length - 1].tagName === 'DT'
@@ -1,10 +1,11 @@
1
1
  import { ExtensionParser } from '../../block';
2
- import { block, validate, fence, fmap } from '../../../combinator';
2
+ import { creation, block, validate, fence, fmap } from '../../../combinator';
3
3
  import { identity } from '../../inline/extension/indexee';
4
+ import { Recursion } from '../../context';
4
5
  import { parse } from '../../api/parse';
5
6
  import { html } from 'typed-dom/dom';
6
7
 
7
- export const aside: ExtensionParser.AsideParser = block(validate('~~~', fmap(
8
+ export const aside: ExtensionParser.AsideParser = creation(0, Recursion.block, block(validate('~~~', fmap(
8
9
  fence(/^(~{3,})aside(?!\S)([^\n]*)(?:$|\n)/, 300),
9
10
  // Bug: Type mismatch between outer and inner.
10
11
  ([body, overflow, closer, opener, delim, param]: string[], _, context) => {
@@ -42,4 +43,4 @@ export const aside: ExtensionParser.AsideParser = block(validate('~~~', fmap(
42
43
  references,
43
44
  ]),
44
45
  ];
45
- })));
46
+ }))));
@@ -1,13 +1,14 @@
1
1
  import { ExtensionParser } from '../../block';
2
2
  import { eval } from '../../../combinator/data/parser';
3
- import { block, validate, fence, fmap } from '../../../combinator';
3
+ import { creation, block, validate, fence, fmap } from '../../../combinator';
4
4
  import { parse } from '../../api/parse';
5
5
  import { mathblock } from '../mathblock';
6
+ import { Recursion } from '../../context';
6
7
  import { html } from 'typed-dom/dom';
7
8
 
8
9
  const opener = /^(~{3,})(?:example\/(\S+))?(?!\S)([^\n]*)(?:$|\n)/;
9
10
 
10
- export const example: ExtensionParser.ExampleParser = block(validate('~~~', fmap(
11
+ export const example: ExtensionParser.ExampleParser = creation(0, Recursion.block, block(validate('~~~', fmap(
11
12
  fence(opener, 300),
12
13
  // Bug: Type mismatch between outer and inner.
13
14
  ([body, overflow, closer, opener, delim, type = 'markdown', param]: string[], _, context) => {
@@ -58,4 +59,4 @@ export const example: ExtensionParser.ExampleParser = block(validate('~~~', fmap
58
59
  }, `${opener}${body}${closer}`),
59
60
  ];
60
61
  }
61
- })));
62
+ }))));