securemark 0.281.4 → 0.283.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 (69) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/design.md +5 -9
  3. package/dist/index.js +7354 -7166
  4. package/package.json +2 -2
  5. package/src/combinator/control/manipulation/convert.ts +4 -8
  6. package/src/combinator/control/manipulation/indent.ts +3 -5
  7. package/src/combinator/control/manipulation/scope.ts +6 -3
  8. package/src/combinator/control/manipulation/surround.ts +17 -2
  9. package/src/combinator/data/parser/context.test.ts +6 -6
  10. package/src/combinator/data/parser/context.ts +25 -39
  11. package/src/combinator/data/parser.ts +2 -3
  12. package/src/parser/api/bind.ts +6 -6
  13. package/src/parser/api/parse.test.ts +17 -16
  14. package/src/parser/api/parse.ts +0 -3
  15. package/src/parser/block/blockquote.ts +4 -3
  16. package/src/parser/block/dlist.test.ts +1 -1
  17. package/src/parser/block/dlist.ts +6 -6
  18. package/src/parser/block/extension/aside.ts +4 -3
  19. package/src/parser/block/extension/example.ts +4 -3
  20. package/src/parser/block/extension/table.ts +7 -7
  21. package/src/parser/block/heading.ts +3 -2
  22. package/src/parser/block/ilist.ts +2 -1
  23. package/src/parser/block/olist.ts +4 -3
  24. package/src/parser/block/reply/cite.ts +3 -3
  25. package/src/parser/block/reply/quote.ts +3 -3
  26. package/src/parser/block/sidefence.ts +2 -1
  27. package/src/parser/block/table.ts +9 -9
  28. package/src/parser/block/ulist.ts +6 -5
  29. package/src/parser/block.ts +16 -5
  30. package/src/parser/context.ts +9 -21
  31. package/src/parser/inline/annotation.ts +5 -4
  32. package/src/parser/inline/autolink/email.ts +2 -1
  33. package/src/parser/inline/autolink/url.ts +6 -5
  34. package/src/parser/inline/autolink.ts +2 -2
  35. package/src/parser/inline/bracket.ts +17 -17
  36. package/src/parser/inline/code.test.ts +1 -1
  37. package/src/parser/inline/code.ts +2 -1
  38. package/src/parser/inline/deletion.ts +3 -3
  39. package/src/parser/inline/emphasis.ts +3 -3
  40. package/src/parser/inline/emstrong.ts +3 -3
  41. package/src/parser/inline/extension/index.test.ts +4 -4
  42. package/src/parser/inline/extension/index.ts +18 -6
  43. package/src/parser/inline/extension/indexee.ts +37 -7
  44. package/src/parser/inline/extension/indexer.test.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 +7 -7
  53. package/src/parser/inline/mark.test.ts +2 -2
  54. package/src/parser/inline/mark.ts +3 -3
  55. package/src/parser/inline/math.test.ts +3 -3
  56. package/src/parser/inline/math.ts +6 -5
  57. package/src/parser/inline/media.test.ts +1 -1
  58. package/src/parser/inline/media.ts +5 -5
  59. package/src/parser/inline/reference.ts +6 -5
  60. package/src/parser/inline/remark.ts +2 -2
  61. package/src/parser/inline/ruby.ts +3 -3
  62. package/src/parser/inline/strong.ts +3 -3
  63. package/src/parser/inline/template.ts +4 -4
  64. package/src/parser/inline.ts +1 -0
  65. package/src/parser/source/escapable.ts +2 -1
  66. package/src/parser/source/str.ts +3 -2
  67. package/src/parser/source/text.ts +2 -1
  68. package/src/parser/source/unescapable.ts +2 -1
  69. package/src/combinator/data/parser/context/memo.ts +0 -57
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.281.4",
3
+ "version": "0.283.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",
@@ -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 { log } = context;
12
+ context.log = sub ? log : {};
17
13
  const result = parser({ source: src, context });
18
14
  assert(check(src, result));
19
- context.offset -= offset;
15
+ context.log = log;
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 { log } = context;
24
+ context.log = {};
27
25
  const result = parser({ source: trimBlockEnd(lines.join('')), context });
28
- context.offset -= offset;
26
+ context.log = log;
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 { log } = context;
38
+ context.log = {};
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.log = log;
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 !== '');
@@ -6,6 +6,7 @@ export function surround<P extends Parser<unknown>, S = string>(
6
6
  opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
7
7
  f?: (rss: [S[], SubTree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
8
8
  g?: (rss: [S[], SubTree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
9
+ log?: 0 | 1 | 2 | 3,
9
10
  ): P;
10
11
  export function surround<P extends Parser<unknown>, S = string>(
11
12
  opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
@@ -16,26 +17,29 @@ export function surround<P extends Parser<unknown>, S = string>(
16
17
  opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
17
18
  f?: (rss: [S[], Tree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
18
19
  g?: (rss: [S[], Tree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
20
+ log?: 0 | 1 | 2 | 3,
19
21
  ): P;
20
22
  export function surround<P extends Parser<unknown>, S = string>(
21
23
  opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
22
24
  f?: (rss: [S[], Tree<P>[] | undefined, S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
23
25
  g?: (rss: [S[], Tree<P>[] | undefined, string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
26
+ log?: 0 | 1 | 2 | 3,
24
27
  ): P;
25
28
  export function surround<T>(
26
29
  opener: string | RegExp | Parser<T>, parser: Parser<T>, closer: string | RegExp | Parser<T>, optional: boolean = false,
27
30
  f?: (rss: [T[], T[], T[]], rest: string, context: Ctx) => Result<T>,
28
31
  g?: (rss: [T[], T[], string], rest: string, context: Ctx) => Result<T>,
32
+ log: 0 | 1 | 2 | 3 = 0,
29
33
  ): Parser<T> {
30
34
  switch (typeof opener) {
31
35
  case 'string':
32
36
  case 'object':
33
- return surround(match(opener), parser, closer, optional, f, g);
37
+ opener = match(opener);
34
38
  }
35
39
  switch (typeof closer) {
36
40
  case 'string':
37
41
  case 'object':
38
- return surround(opener, parser, match(closer), optional, f, g);
42
+ closer = match(closer);
39
43
  }
40
44
  return ({ source, context }) => {
41
45
  const lmr_ = source;
@@ -45,6 +49,13 @@ export function surround<T>(
45
49
  if (res1 === undefined) return;
46
50
  const rl = eval(res1);
47
51
  const mr_ = exec(res1);
52
+ if (log & 1) {
53
+ const { log = {}, offset = 0 } = context;
54
+ for (let i = 0; i < source.length - mr_.length; ++i) {
55
+ if (source[i] !== source[0]) break;
56
+ if (source.length + offset - i in log) return;
57
+ }
58
+ }
48
59
  const res2 = mr_ !== '' ? parser({ source: mr_, context }) : undefined;
49
60
  assert(check(mr_, res2));
50
61
  const rm = eval(res2);
@@ -55,6 +66,10 @@ export function surround<T>(
55
66
  const rr = eval(res3);
56
67
  const rest = exec(res3, r_);
57
68
  if (rest.length === lmr_.length) return;
69
+ if (log & 2 && rr === undefined) {
70
+ const { log = {}, offset = 0 } = context;
71
+ log[source.length + offset] = 0;
72
+ }
58
73
  return rr
59
74
  ? f
60
75
  ? 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.log = {};
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
+ log?: Record<number, 0>;
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 };
@@ -298,35 +298,35 @@ describe('Unit: parser/api/parse', () => {
298
298
 
299
299
  it('recursion', () => {
300
300
  assert.deepStrictEqual(
301
- [...parse('{'.repeat(21)).children].map(el => el.outerHTML),
302
- [`<p>${'{'.repeat(21)}</p>`]);
301
+ [...parse(`${'{'.repeat(20)}a`).children].map(el => el.outerHTML),
302
+ [`<p>${'{'.repeat(20)}a</p>`]);
303
303
  assert.deepStrictEqual(
304
- [...parse('{'.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
304
+ [...parse(`${'{'.repeat(21)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
305
305
  [
306
306
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
307
- `<pre class="error" translate="no">${'{'.repeat(22)}</pre>`,
307
+ `<pre class="error" translate="no">${'{'.repeat(21)}a</pre>`,
308
308
  ]);
309
309
  assert.deepStrictEqual(
310
- [...parse('('.repeat(21)).children].map(el => el.outerHTML),
311
- [`<p>${'('.repeat(21)}</p>`]);
310
+ [...parse(`${'('.repeat(22)}a`).children].map(el => el.outerHTML),
311
+ [`<p>${'('.repeat(22)}a</p>`]);
312
312
  assert.deepStrictEqual(
313
- [...parse('('.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
313
+ [...parse(`${'('.repeat(23)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
314
314
  [
315
315
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
316
- `<pre class="error" translate="no">${'('.repeat(22)}</pre>`,
316
+ `<pre class="error" translate="no">${'('.repeat(23)}a</pre>`,
317
317
  ]);
318
318
  assert.deepStrictEqual(
319
- [...parse('['.repeat(21)).children].map(el => el.outerHTML),
320
- [`<p>${'['.repeat(21)}</p>`]);
319
+ [...parse(`${'['.repeat(23)}a`).children].map(el => el.outerHTML),
320
+ [`<p>${'['.repeat(23)}a</p>`]);
321
321
  assert.deepStrictEqual(
322
- [...parse('['.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
322
+ [...parse(`${'['.repeat(24)}a`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
323
323
  [
324
324
  '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
325
- `<pre class="error" translate="no">${'['.repeat(22)}</pre>`,
325
+ `<pre class="error" translate="no">${'['.repeat(24)}a</pre>`,
326
326
  ]);
327
327
  assert.deepStrictEqual(
328
- [...parse('['.repeat(20) + '\na').children].map(el => el.outerHTML),
329
- [`<p>${'['.repeat(20)}<br>a</p>`]);
328
+ [...parse(`${'['.repeat(22)}\na`).children].map(el => el.outerHTML),
329
+ [`<p>${'['.repeat(22)}<br>a</p>`]);
330
330
  });
331
331
 
332
332
  if (!navigator.userAgent.includes('Chrome')) return;
@@ -351,9 +351,10 @@ describe('Unit: parser/api/parse', () => {
351
351
  it('recovery', function () {
352
352
  this.timeout(5000);
353
353
  assert.deepStrictEqual(
354
- [...parse(`!>> ${'['.repeat(20)}${'{a}'.repeat(518)}\n> ${'{a}'.repeat(4)}\n\na`).children].map(el => el.outerHTML),
354
+ [...parse(`${'{'.repeat(21)}\n\na`).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
355
355
  [
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>`,
356
+ `<h1 id="error:rnd" class="error">Error: Too much recursion</h1>`,
357
+ `<pre class="error" translate="no">${'{'.repeat(21)}\n</pre>`,
357
358
  '<p>a</p>',
358
359
  ]);
359
360
  });
@@ -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
+ }))));
@@ -1,7 +1,7 @@
1
1
  import { max, min, isArray } from 'spica/alias';
2
2
  import { ExtensionParser } from '../../block';
3
3
  import { Tree, eval } from '../../../combinator/data/parser';
4
- import { union, subsequence, inits, some, creation, block, line, validate, fence, rewrite, surround, open, clear, convert, dup, lazy, fmap } from '../../../combinator';
4
+ import { union, subsequence, inits, some, block, line, validate, fence, rewrite, surround, open, clear, convert, dup, lazy, fmap } from '../../../combinator';
5
5
  import { inline, medialink, media, shortmedia } from '../../inline';
6
6
  import { str, anyline, emptyline, contentline } from '../../source';
7
7
  import { lineable } from '../../util';
@@ -78,7 +78,7 @@ const align: AlignParser = line(fmap(
78
78
 
79
79
  const delimiter = /^[-=<>]+(?:\/[-=^v]*)?(?=[^\S\n]*\n)|^[#:](?:(?!:\D|0)\d*:(?!0)\d*)?(?:!+[+]?)?(?=\s)/;
80
80
 
81
- const head: CellParser.HeadParser = creation(1, false, block(fmap(open(
81
+ const head: CellParser.HeadParser = block(fmap(open(
82
82
  str(/^#(?:(?!:\D|0)\d*:(?!0)\d*)?(?:!+[+]?)?(?=\s)/),
83
83
  rewrite(
84
84
  inits([
@@ -93,9 +93,9 @@ const head: CellParser.HeadParser = creation(1, false, block(fmap(open(
93
93
  ])),
94
94
  true),
95
95
  ns => [html('th', attributes(ns.shift()! as string), trimNodeEnd(defrag(ns)))]),
96
- false));
96
+ false);
97
97
 
98
- const data: CellParser.DataParser = creation(1, false, block(fmap(open(
98
+ const data: CellParser.DataParser = block(fmap(open(
99
99
  str(/^:(?:(?!:\D|0)\d*:(?!0)\d*)?(?:!+[+]?)?(?=\s)/),
100
100
  rewrite(
101
101
  inits([
@@ -110,15 +110,15 @@ const data: CellParser.DataParser = creation(1, false, block(fmap(open(
110
110
  ])),
111
111
  true),
112
112
  ns => [html('td', attributes(ns.shift()! as string), trimNodeEnd(defrag(ns)))]),
113
- false));
113
+ false);
114
114
 
115
- const dataline: CellParser.DatalineParser = creation(1, false, line(
115
+ const dataline: CellParser.DatalineParser = line(
116
116
  rewrite(
117
117
  contentline,
118
118
  union([
119
119
  validate(/^!+\s/, convert(source => `:${source}`, data)),
120
120
  convert(source => `: ${source}`, data),
121
- ]))));
121
+ ])));
122
122
 
123
123
  function attributes(source: string): Record<string, string | undefined> {
124
124
  let [, rowspan = undefined, colspan = undefined, highlight = undefined, extension = undefined] =
@@ -1,6 +1,6 @@
1
1
  import { HeadingParser } from '../block';
2
2
  import { union, some, state, block, line, validate, focus, rewrite, open, fmap } from '../../combinator';
3
- import { inline, indexee, indexer } from '../inline';
3
+ import { inline, indexee, indexer, dataindex } from '../inline';
4
4
  import { str } from '../source';
5
5
  import { State } from '../context';
6
6
  import { visualize, trimBlankStart, trimNodeEnd } from '../visibility';
@@ -11,6 +11,7 @@ export const segment: HeadingParser.SegmentParser = block(validate('#', focus(
11
11
  some(line(({ source }) => [[source], ''])))));
12
12
 
13
13
  export const heading: HeadingParser = block(rewrite(segment,
14
+ // その他の表示制御は各所のCSSで行う。
14
15
  state(State.annotation | State.reference | State.index | State.label | State.link | State.media,
15
16
  line(indexee(fmap(union([
16
17
  open(
@@ -23,7 +24,7 @@ export const heading: HeadingParser = block(rewrite(segment,
23
24
  ]),
24
25
  ([h, ...ns]: [string, ...(HTMLElement | string)[]]) => [
25
26
  h.length <= 6
26
- ? html(`h${h.length as 1}`, trimNodeEnd(defrag(ns)))
27
+ ? html(`h${h.length as 1}`, { 'data-index': dataindex(ns) }, trimNodeEnd(defrag(ns)))
27
28
  : html(`h6`, {
28
29
  class: 'invalid',
29
30
  'data-invalid-syntax': 'heading',