securemark 0.255.0 → 0.257.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 (52) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.js +209 -170
  3. package/markdown.d.ts +18 -11
  4. package/package.json +1 -1
  5. package/src/combinator/control/constraint/contract.ts +3 -13
  6. package/src/combinator/control/manipulation/context.ts +12 -7
  7. package/src/combinator/control/manipulation/resource.ts +37 -3
  8. package/src/combinator/control/manipulation/surround.ts +6 -6
  9. package/src/combinator/data/parser/inits.ts +1 -1
  10. package/src/combinator/data/parser/sequence.ts +1 -1
  11. package/src/combinator/data/parser/some.ts +16 -38
  12. package/src/combinator/data/parser.ts +34 -18
  13. package/src/debug.test.ts +2 -2
  14. package/src/parser/api/bind.ts +9 -11
  15. package/src/parser/api/parse.test.ts +51 -9
  16. package/src/parser/api/parse.ts +6 -5
  17. package/src/parser/block/extension/aside.ts +3 -3
  18. package/src/parser/block/extension/example.ts +3 -3
  19. package/src/parser/block.ts +1 -1
  20. package/src/parser/inline/annotation.test.ts +6 -5
  21. package/src/parser/inline/annotation.ts +9 -6
  22. package/src/parser/inline/autolink/url.ts +6 -6
  23. package/src/parser/inline/bracket.test.ts +11 -7
  24. package/src/parser/inline/bracket.ts +11 -11
  25. package/src/parser/inline/comment.test.ts +4 -3
  26. package/src/parser/inline/comment.ts +4 -4
  27. package/src/parser/inline/deletion.ts +3 -3
  28. package/src/parser/inline/emphasis.ts +3 -3
  29. package/src/parser/inline/emstrong.ts +4 -5
  30. package/src/parser/inline/extension/index.test.ts +1 -0
  31. package/src/parser/inline/extension/index.ts +8 -7
  32. package/src/parser/inline/extension/indexer.ts +3 -5
  33. package/src/parser/inline/extension/label.ts +1 -1
  34. package/src/parser/inline/extension/placeholder.test.ts +8 -7
  35. package/src/parser/inline/extension/placeholder.ts +4 -4
  36. package/src/parser/inline/html.test.ts +2 -0
  37. package/src/parser/inline/html.ts +5 -5
  38. package/src/parser/inline/insertion.ts +3 -3
  39. package/src/parser/inline/link.test.ts +1 -0
  40. package/src/parser/inline/link.ts +8 -7
  41. package/src/parser/inline/mark.ts +3 -3
  42. package/src/parser/inline/math.test.ts +21 -14
  43. package/src/parser/inline/math.ts +4 -15
  44. package/src/parser/inline/media.test.ts +0 -2
  45. package/src/parser/inline/media.ts +6 -6
  46. package/src/parser/inline/reference.test.ts +9 -9
  47. package/src/parser/inline/reference.ts +19 -7
  48. package/src/parser/inline/ruby.ts +29 -27
  49. package/src/parser/inline/strong.ts +3 -3
  50. package/src/parser/inline/template.ts +4 -4
  51. package/src/parser/inline.test.ts +13 -10
  52. package/src/parser/util.ts +18 -2
package/markdown.d.ts CHANGED
@@ -1,6 +1,22 @@
1
1
  import { Parser, Ctx } from './src/combinator/data/parser';
2
2
  import { Dict } from 'spica/dict';
3
3
 
4
+ /*
5
+
6
+ Operator precedence
7
+
8
+ 9: \n, \\\n
9
+ 8: `, "
10
+ 7: $
11
+ 6: (()), [[]]
12
+ 5: <tag></tag>
13
+ 4: [% %]
14
+ 3: (), [], {}
15
+ 2: ==, ++, ~~
16
+ 1: *, **
17
+
18
+ */
19
+
4
20
  declare abstract class Markdown<T> {
5
21
  private parser?: T;
6
22
  }
@@ -671,7 +687,7 @@ export namespace MarkdownParser {
671
687
  export interface AnnotationParser extends
672
688
  // ((abc))
673
689
  Inline<'annotation'>,
674
- Parser<HTMLElement, Context, [
690
+ Parser<HTMLElement | string, Context, [
675
691
  InlineParser,
676
692
  ]> {
677
693
  }
@@ -680,7 +696,7 @@ export namespace MarkdownParser {
680
696
  // [[^abbr]]
681
697
  // [[^abbr| abc]]
682
698
  Inline<'reference'>,
683
- Parser<HTMLElement, Context, [
699
+ Parser<HTMLElement | string, Context, [
684
700
  ReferenceParser.AbbrParser,
685
701
  InlineParser,
686
702
  InlineParser,
@@ -740,7 +756,6 @@ export namespace MarkdownParser {
740
756
  MathParser.BracketParser,
741
757
  Parser<string, Context, [
742
758
  MathParser.BracketParser,
743
- MathParser.QuoteParser,
744
759
  SourceParser.StrParser,
745
760
  ]>,
746
761
  ]> {
@@ -753,14 +768,6 @@ export namespace MarkdownParser {
753
768
  SourceParser.EscapableSourceParser,
754
769
  ]> {
755
770
  }
756
- export interface QuoteParser extends
757
- Inline<'math/quote'>,
758
- Parser<HTMLElement, Context, [
759
- QuoteParser,
760
- BracketParser,
761
- SourceParser.StrParser,
762
- ]> {
763
- }
764
771
  }
765
772
  export interface ExtensionParser extends
766
773
  // [#abc]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.255.0",
3
+ "version": "0.257.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",
@@ -9,11 +9,9 @@ import { Parser, Ctx, Tree, Context, eval, exec, check } from '../../data/parser
9
9
 
10
10
  export function validate<P extends Parser<unknown>>(patterns: string | RegExp | (string | RegExp)[], parser: P): P;
11
11
  export function validate<P extends Parser<unknown>>(patterns: string | RegExp | (string | RegExp)[], has: string, parser: P): P;
12
- export function validate<P extends Parser<unknown>>(patterns: string | RegExp | (string | RegExp)[], has: string, end: string, parser: P): P;
13
- export function validate<T>(patterns: string | RegExp | (string | RegExp)[], has: string | Parser<T>, end?: string | Parser<T>, parser?: Parser<T>): Parser<T> {
14
- if (typeof has === 'function') return validate(patterns, '', '', has);
15
- if (typeof end === 'function') return validate(patterns, has, '', end);
16
- if (!isArray(patterns)) return validate([patterns], has, end!, parser!);
12
+ export function validate<T>(patterns: string | RegExp | (string | RegExp)[], has: string | Parser<T>, parser?: Parser<T>): Parser<T> {
13
+ if (typeof has === 'function') return validate(patterns, '', has);
14
+ if (!isArray(patterns)) return validate([patterns], has, parser!);
17
15
  assert(patterns.length > 0);
18
16
  assert(patterns.every(pattern => pattern instanceof RegExp ? !pattern.flags.match(/[gmy]/) && pattern.source.startsWith('^') : true));
19
17
  assert(parser);
@@ -26,17 +24,9 @@ export function validate<T>(patterns: string | RegExp | (string | RegExp)[], has
26
24
  ? `|| source.slice(0, ${pattern.length}) === '${pattern}'`
27
25
  : `|| /${pattern.source}/${pattern.flags}.test(source)`),
28
26
  ].join(''))();
29
- const match2 = (source: string): boolean => {
30
- if (!has) return true;
31
- const i = end ? source.indexOf(end, 1) : -1;
32
- return i !== -1
33
- ? source.slice(0, i).indexOf(has, 1) !== -1
34
- : source.indexOf(has, 1) !== -1;
35
- };
36
27
  return (source, context) => {
37
28
  if (source === '') return;
38
29
  if (!match(source)) return;
39
- if (!match2(source)) return;
40
30
  const result = parser!(source, context);
41
31
  assert(check(source, result));
42
32
  if (!result) return;
@@ -13,20 +13,18 @@ export function guard<T>(f: (context: Ctx) => boolean, parser: Parser<T>): Parse
13
13
  : undefined;
14
14
  }
15
15
 
16
- export function reset<P extends Parser<unknown>>(context: Context<P>, parser: P): P;
16
+ export function reset<P extends Parser<unknown>>(base: Context<P>, parser: P): P;
17
17
  export function reset<T>(base: Ctx, parser: Parser<T>): Parser<T> {
18
18
  assert(Object.getPrototypeOf(base) === Object.prototype);
19
19
  assert(Object.freeze(base));
20
- if (isEmpty(base)) return parser;
21
20
  return (source, context) =>
22
21
  parser(source, inherit(ObjectCreate(context), base));
23
22
  }
24
23
 
25
- export function context<P extends Parser<unknown>>(context: Context<P>, parser: P): P;
24
+ export function context<P extends Parser<unknown>>(base: Context<P>, parser: P): P;
26
25
  export function context<T>(base: Ctx, parser: Parser<T>): Parser<T> {
27
26
  assert(Object.getPrototypeOf(base) === Object.prototype);
28
27
  assert(Object.freeze(base));
29
- if (isEmpty(base)) return parser;
30
28
  const override = memoize<Ctx, Ctx>(context => inherit(ObjectCreate(context), base), new WeakMap());
31
29
  return (source, context) =>
32
30
  parser(source, override(context));
@@ -40,6 +38,7 @@ const inherit = template((prop, target, source) => {
40
38
  switch (prop) {
41
39
  case 'resources':
42
40
  assert(typeof source[prop] === 'object');
41
+ assert(target[prop] || !(prop in target));
43
42
  if (prop in target && !hasOwnProperty(target, prop)) return;
44
43
  return target[prop] = ObjectCreate(source[prop]);
45
44
  }
@@ -59,7 +58,13 @@ const inherit = template((prop, target, source) => {
59
58
  }
60
59
  });
61
60
 
62
- function isEmpty(context: Ctx): boolean {
63
- for (const _ in context) return false;
64
- return true;
61
+ export function precedence<P extends Parser<unknown>>(precedence: number, parser: P): P;
62
+ export function precedence<T>(precedence: number, parser: Parser<T>): Parser<T> {
63
+ return (source, context) => {
64
+ const p = context.precedence;
65
+ context.precedence = precedence;
66
+ const result = parser(source, context);
67
+ context.precedence = p;
68
+ return result;
69
+ };
65
70
  }
@@ -4,11 +4,11 @@ export function creator<P extends Parser<unknown>>(parser: P): P;
4
4
  export function creator<P extends Parser<unknown>>(cost: number, parser: P): P;
5
5
  export function creator(cost: number | Parser<unknown>, parser?: Parser<unknown>): Parser<unknown> {
6
6
  if (typeof cost === 'function') return creator(1, cost);
7
- assert(cost > 0);
7
+ assert(cost >= 0);
8
8
  return (source, context) => {
9
9
  const { resources = { budget: 1, recursion: 1 } } = context;
10
- if (resources.budget <= 0) throw new Error('Too many creations.');
11
- if (resources.recursion <= 0) throw new Error('Too much recursion.');
10
+ if (resources.budget <= 0) throw new Error('Too many creations');
11
+ if (resources.recursion <= 0) throw new Error('Too much recursion');
12
12
  --resources.recursion;
13
13
  const result = parser!(source, context);
14
14
  ++resources.recursion;
@@ -18,3 +18,37 @@ export function creator(cost: number | Parser<unknown>, parser?: Parser<unknown>
18
18
  return result;
19
19
  };
20
20
  }
21
+
22
+ export function uncreator<P extends Parser<unknown>>(parser: P): P;
23
+ export function uncreator<P extends Parser<unknown>>(cost: number, parser: P): P;
24
+ export function uncreator(cost: number | Parser<unknown>, parser?: Parser<unknown>): Parser<unknown> {
25
+ if (typeof cost === 'function') return uncreator(1, cost);
26
+ assert(cost >= 0);
27
+ return (source, context) => {
28
+ const { resources = { budget: 1, recursion: 1 } } = context;
29
+ if (resources.budget <= 0) throw new Error('Too many creations');
30
+ if (resources.recursion <= 0) throw new Error('Too much recursion');
31
+ ++resources.recursion;
32
+ const result = parser!(source, context);
33
+ --resources.recursion;
34
+ if (result) {
35
+ resources.budget += cost;
36
+ }
37
+ return result;
38
+ };
39
+ }
40
+
41
+ export function recursion<P extends Parser<unknown>>(parser: P): P;
42
+ export function recursion<P extends Parser<unknown>>(cost: number, parser: P): P;
43
+ export function recursion(cost: number | Parser<unknown>, parser?: Parser<unknown>): Parser<unknown> {
44
+ if (typeof cost === 'function') return recursion(1, cost);
45
+ assert(cost >= 0);
46
+ return (source, context) => {
47
+ const { resources = { recursion: 1 } } = context;
48
+ if (resources.recursion <= 0) throw new Error('Too much recursion');
49
+ --resources.recursion;
50
+ const result = parser!(source, context);
51
+ ++resources.recursion;
52
+ return result;
53
+ };
54
+ }
@@ -6,27 +6,27 @@ import { unshift, push } from 'spica/array';
6
6
  export function surround<P extends Parser<unknown>, S = string>(
7
7
  opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
8
8
  f?: (rss: [S[], SubTree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
9
- g?: (rss: [S[], SubTree<P>[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
9
+ g?: (rss: [S[], SubTree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
10
10
  ): P;
11
11
  export function surround<P extends Parser<unknown>, S = string>(
12
12
  opener: string | RegExp | Parser<S, Context<P>>, parser: IntermediateParser<P>, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
13
13
  f?: (rss: [S[], SubTree<P>[] | undefined, S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
14
- g?: (rss: [S[], SubTree<P>[] | undefined], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
14
+ g?: (rss: [S[], SubTree<P>[] | undefined, string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
15
15
  ): P;
16
16
  export function surround<P extends Parser<unknown>, S = string>(
17
17
  opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: false,
18
18
  f?: (rss: [S[], Tree<P>[], S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
19
- g?: (rss: [S[], Tree<P>[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
19
+ g?: (rss: [S[], Tree<P>[], string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
20
20
  ): P;
21
21
  export function surround<P extends Parser<unknown>, S = string>(
22
22
  opener: string | RegExp | Parser<S, Context<P>>, parser: P, closer: string | RegExp | Parser<S, Context<P>>, optional?: boolean,
23
23
  f?: (rss: [S[], Tree<P>[] | undefined, S[]], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
24
- g?: (rss: [S[], Tree<P>[] | undefined], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
24
+ g?: (rss: [S[], Tree<P>[] | undefined, string], rest: string, context: Context<P>) => Result<Tree<P>, Context<P>, SubParsers<P>>,
25
25
  ): P;
26
26
  export function surround<T>(
27
27
  opener: string | RegExp | Parser<T>, parser: Parser<T>, closer: string | RegExp | Parser<T>, optional: boolean = false,
28
28
  f?: (rss: [T[], T[], T[]], rest: string, context: Ctx) => Result<T>,
29
- g?: (rss: [T[], T[]], rest: string, context: Ctx) => Result<T>,
29
+ g?: (rss: [T[], T[], string], rest: string, context: Ctx) => Result<T>,
30
30
  ): Parser<T> {
31
31
  switch (typeof opener) {
32
32
  case 'string':
@@ -60,7 +60,7 @@ export function surround<T>(
60
60
  ? f([rl, rm!, rr], rest, context)
61
61
  : [push(unshift(rl, rm ?? []), rr), rest]
62
62
  : g
63
- ? g([rl, rm!], rest, context)
63
+ ? g([rl, rm!, mr_], rest, context)
64
64
  : undefined;
65
65
  };
66
66
  }
@@ -14,7 +14,7 @@ export function inits<T, D extends Parser<T>[]>(parsers: D): Parser<T, Ctx, D> {
14
14
  const result = parsers[i](rest, context);
15
15
  assert(check(rest, result));
16
16
  if (!result) break;
17
- assert(!context?.delimiters?.match(rest));
17
+ assert(!context?.delimiters?.match(rest, context.precedence));
18
18
  nodes = nodes
19
19
  ? push(nodes, eval(result))
20
20
  : eval(result);
@@ -14,7 +14,7 @@ export function sequence<T, D extends Parser<T>[]>(parsers: D): Parser<T, Ctx, D
14
14
  const result = parsers[i](rest, context);
15
15
  assert(check(rest, result));
16
16
  if (!result) return;
17
- assert(!context?.delimiters?.match(rest));
17
+ assert(!context?.delimiters?.match(rest, context.precedence));
18
18
  nodes = nodes
19
19
  ? push(nodes, eval(result))
20
20
  : eval(result);
@@ -3,6 +3,8 @@ import { Parser, Delimiters, eval, exec, check } from '../parser';
3
3
  import { memoize, reduce } from 'spica/memoize';
4
4
  import { push } from 'spica/array';
5
5
 
6
+ type DelimiterOption = readonly [delimiter: string | RegExp, precedence: number];
7
+
6
8
  const signature = (pattern: string | RegExp | undefined): string => {
7
9
  switch (typeof pattern) {
8
10
  case 'undefined':
@@ -26,31 +28,29 @@ const matcher = memoize(
26
28
  },
27
29
  signature);
28
30
 
29
- export function some<P extends Parser<unknown>>(parser: P, until?: string | RegExp | number, deep?: string | RegExp, limit?: number): P;
30
- export function some<T>(parser: Parser<T>, until?: string | RegExp | number, deep?: string | RegExp, limit = -1): Parser<T> {
31
+ export function some<P extends Parser<unknown>>(parser: P, until?: string | RegExp | number, deeps?: readonly DelimiterOption[], limit?: number): P;
32
+ export function some<T>(parser: Parser<T>, until?: string | RegExp | number, deeps: readonly DelimiterOption[] = [], limit = -1): Parser<T> {
33
+ if (typeof until === 'number') return some(parser, undefined, deeps, until);
31
34
  assert(parser);
32
- assert(until instanceof RegExp ? !until.flags.match(/[gmy]/) && until.source.startsWith('^') : true);
33
- assert(deep instanceof RegExp ? !deep.flags.match(/[gmy]/) && deep.source.startsWith('^') : true);
34
- if (typeof until === 'number') return some(parser, undefined, deep, until);
35
+ assert([until].concat(deeps.map(o => o[0])).every(d => d instanceof RegExp ? !d.flags.match(/[gmy]/) && d.source.startsWith('^') : true));
35
36
  const match = matcher(until);
36
- const delimiter = {
37
- signature: signature(deep),
38
- matcher: matcher(deep),
39
- } as const;
37
+ const delimiters = deeps.map(([delimiter, precedence]) => ({
38
+ signature: signature(delimiter),
39
+ matcher: matcher(delimiter),
40
+ precedence,
41
+ }));
40
42
  return (source, context) => {
41
43
  if (source === '') return;
42
44
  let rest = source;
43
45
  let nodes: T[] | undefined;
44
- if (deep && context) {
45
- // bracket > link > media | bracket
46
- // bracket > index > bracket
46
+ if (delimiters.length > 0) {
47
47
  context.delimiters ??= new Delimiters();
48
- context.delimiters.push(delimiter);
48
+ context.delimiters.push(...delimiters);
49
49
  }
50
50
  while (true) {
51
51
  if (rest === '') break;
52
52
  if (match(rest)) break;
53
- if (context.delimiters?.match(rest)) break;
53
+ if (context.delimiters?.match(rest, context.precedence)) break;
54
54
  const result = parser(rest, context);
55
55
  assert.doesNotThrow(() => limit < 0 && check(rest, result));
56
56
  if (!result) break;
@@ -60,8 +60,8 @@ export function some<T>(parser: Parser<T>, until?: string | RegExp | number, dee
60
60
  rest = exec(result);
61
61
  if (limit >= 0 && source.length - rest.length > limit) break;
62
62
  }
63
- if (deep && context.delimiters) {
64
- context.delimiters.pop();
63
+ if (delimiters.length > 0) {
64
+ context.delimiters!.pop(delimiters.length);
65
65
  }
66
66
  assert(rest.length <= source.length);
67
67
  return nodes && rest.length < source.length
@@ -69,25 +69,3 @@ export function some<T>(parser: Parser<T>, until?: string | RegExp | number, dee
69
69
  : undefined;
70
70
  };
71
71
  }
72
-
73
- export function escape<P extends Parser<unknown>>(parser: P, delim: string): P;
74
- export function escape<T>(parser: Parser<T>, delim: string): Parser<T> {
75
- assert(parser);
76
- const delimiter = {
77
- signature: signature(delim),
78
- matcher: (source: string) => source.slice(0, delim.length) !== delim && undefined,
79
- escape: true,
80
- } as const;
81
- return (source, context) => {
82
- if (source === '') return;
83
- if (context) {
84
- context.delimiters ??= new Delimiters();
85
- context.delimiters.push(delimiter);
86
- }
87
- const result = parser(source, context);
88
- if (context.delimiters) {
89
- context.delimiters.pop();
90
- }
91
- return result;
92
- };
93
- }
@@ -1,5 +1,3 @@
1
- import { undefined } from 'spica/global';
2
-
3
1
  export type Parser<T, C extends Ctx = Ctx, D extends Parser<unknown, C>[] = any>
4
2
  = (source: string, context: C) => Result<T, C, D>;
5
3
  export type Result<T, C extends Ctx = Ctx, D extends Parser<unknown, C>[] = any>
@@ -11,6 +9,7 @@ export interface Ctx {
11
9
  budget: number;
12
10
  recursion: number;
13
11
  };
12
+ precedence?: number;
14
13
  delimiters?: Delimiters;
15
14
  }
16
15
  export type Tree<P extends Parser<unknown>> = P extends Parser<infer T> ? T : never;
@@ -22,28 +21,44 @@ type ExtractSubTree<D extends Parser<unknown>[]> = ExtractSubParser<D> extends i
22
21
  type ExtractSubParser<D extends Parser<unknown>[]> = D extends (infer P)[] ? P extends Parser<unknown> ? P : never : never;
23
22
 
24
23
  export class Delimiters {
25
- private readonly matchers: ((source: string) => boolean | undefined)[] = [];
26
- private readonly record: Record<string, boolean> = {};
27
- public push(delimiter: { readonly signature: string; readonly matcher: (source: string) => boolean | undefined; readonly escape?: boolean; }): void {
28
- const { signature, matcher, escape } = delimiter;
29
- if (this.record[signature] === !escape) {
30
- this.matchers.unshift(() => undefined);
31
- }
32
- else {
33
- this.matchers.unshift(matcher);
34
- this.record[signature] = !escape;
24
+ private readonly matchers: [number, string, number, (source: string) => boolean | undefined][] = [];
25
+ private readonly registry: Record<string, boolean> = {};
26
+ private length = 0;
27
+ public push(
28
+ ...delimiters: readonly {
29
+ readonly signature: string;
30
+ readonly matcher: (source: string) => boolean | undefined;
31
+ readonly precedence?: number;
32
+ }[]
33
+ ): void {
34
+ for (let i = 0; i < delimiters.length; ++i) {
35
+ const delimiter = delimiters[i];
36
+ assert(this.length >= this.matchers.length);
37
+ const { signature, matcher, precedence = 1 } = delimiter;
38
+ if (!this.registry[signature]) {
39
+ this.matchers.unshift([this.length, signature, precedence, matcher]);
40
+ this.registry[signature] = true;
41
+ }
42
+ ++this.length;
35
43
  }
36
44
  }
37
- public pop(): void {
38
- assert(this.matchers.length > 0);
39
- this.matchers.shift();
45
+ public pop(count = 1): void {
46
+ assert(count > 0);
47
+ for (let i = 0; i < count; ++i) {
48
+ assert(this.matchers.length > 0);
49
+ assert(this.length >= this.matchers.length);
50
+ if (--this.length === this.matchers[0][0]) {
51
+ this.registry[this.matchers.shift()![1]] = false;
52
+ }
53
+ }
40
54
  }
41
- public match(source: string): boolean {
55
+ public match(source: string, precedence = 1): boolean {
42
56
  const { matchers } = this;
43
57
  for (let i = 0; i < matchers.length; ++i) {
44
- switch (matchers[i](source)) {
58
+ switch (matchers[i][3](source)) {
45
59
  case true:
46
- return true;
60
+ if (precedence < matchers[i][2]) return true;
61
+ continue;
47
62
  case false:
48
63
  return false;
49
64
  }
@@ -73,6 +88,7 @@ export function exec(result: Result<unknown>, default_?: string): string | undef
73
88
 
74
89
  export function check(source: string, result: Result<unknown>, mustConsume = true): true {
75
90
  assert.doesNotThrow(() => {
91
+ if (source.length > 1000) return;
76
92
  if (source.slice(+mustConsume).slice(-exec(result, '').length || source.length) !== exec(result, '')) throw new Error();
77
93
  });
78
94
  return true;
package/src/debug.test.ts CHANGED
@@ -4,8 +4,8 @@ import { querySelector, querySelectorAll } from 'typed-dom/query';
4
4
 
5
5
  export function inspect(result: Result<HTMLElement | string>, until: number | string = Infinity): Result<string> {
6
6
  return result && [
7
- eval(result).map(node => {
8
- assert(node);
7
+ eval(result).map((node, i, nodes) => {
8
+ assert(node || node === '' && '([{'.includes(nodes[i + 1] as string));
9
9
  if (typeof node === 'string') return node;
10
10
  node = node.cloneNode(true);
11
11
  assert(!querySelector(node, '.invalid[data-invalid-message$="."]'));
@@ -1,5 +1,4 @@
1
1
  import { undefined, location } from 'spica/global';
2
- import { ObjectAssign, ObjectCreate } from 'spica/alias';
3
2
  import { ParserSettings, Progress } from '../../..';
4
3
  import { MarkdownParser } from '../../../markdown';
5
4
  import { eval } from '../../combinator/data/parser';
@@ -21,11 +20,10 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
21
20
  nearest: (position: number) => HTMLElement | undefined;
22
21
  index: (block: HTMLElement) => number;
23
22
  } {
24
- const context: MarkdownParser.Context = ObjectAssign(ObjectCreate(settings), {
23
+ let context: MarkdownParser.Context = {
24
+ ...settings,
25
25
  host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
26
- footnotes: undefined,
27
- chunk: undefined,
28
- });
26
+ };
29
27
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
30
28
  assert(!settings.id);
31
29
  type Block = readonly [segment: string, blocks: readonly HTMLElement[], url: string];
@@ -41,14 +39,14 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
41
39
  };
42
40
 
43
41
  function* parse(source: string): Generator<Progress, undefined, undefined> {
44
- if (settings.chunk && revision) throw new Error('Chunks cannot be updated.');
42
+ if (settings.chunk && revision) throw new Error('Chunks cannot be updated');
45
43
  const url = headers(source).find(field => field.toLowerCase().startsWith('url:'))?.slice(4).trim() ?? '';
46
44
  source = normalize(validate(source, MAX_INPUT_SIZE) ? source : source.slice(0, MAX_INPUT_SIZE + 1));
47
- ObjectAssign<MarkdownParser.Context, MarkdownParser.Context>(
48
- context,
49
- {
50
- url: url ? new ReadonlyURL(url as ':') : undefined,
51
- });
45
+ // Change the object identity.
46
+ context = {
47
+ ...context,
48
+ url: url ? new ReadonlyURL(url as ':') : undefined,
49
+ };
52
50
  const rev = revision = Symbol();
53
51
  const sourceSegments: string[] = [];
54
52
  for (const seg of segment(source)) {
@@ -223,22 +223,64 @@ describe('Unit: parser/api/parse', () => {
223
223
  ['<p>a<span class="linebreak"> </span>b</p>']);
224
224
  });
225
225
 
226
- it('creation', () => {
226
+ it('backtrack', () => {
227
227
  assert.deepStrictEqual(
228
- [...parse('"[% '.repeat(100)).children].map(el => el.outerHTML),
229
- [`<p>${'"[% '.repeat(100).trim()}</p>`]);
228
+ [...parse('"[% '.repeat(100) + '\n\na').children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
229
+ [
230
+ '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
231
+ `<pre class="error" translate="no">${'"[% '.repeat(100)}\n</pre>`,
232
+ '<p>a</p>',
233
+ ]);
230
234
  });
231
235
 
232
236
  it('recursion', () => {
233
237
  assert.deepStrictEqual(
234
- [...parse('('.repeat(199)).children].map(el => el.outerHTML),
235
- [`<p>${'('.repeat(199)}</p>`]);
238
+ [...parse('('.repeat(20)).children].map(el => el.outerHTML),
239
+ [`<p>${'('.repeat(20)}</p>`]);
236
240
  assert.deepStrictEqual(
237
- [...parse('('.repeat(200) + '\n\na').children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
241
+ [...parse('('.repeat(21)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
238
242
  [
239
- '<h1 id="error:rnd" class="error">Error: Too much recursion.</h1>',
240
- `<pre class="error" translate="no">${'('.repeat(200)}\n</pre>`,
241
- '<p>a</p>',
243
+ '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
244
+ `<pre class="error" translate="no">${'('.repeat(21)}</pre>`,
245
+ ]);
246
+ assert.deepStrictEqual(
247
+ [...parse('['.repeat(20)).children].map(el => el.outerHTML),
248
+ [`<p>${'['.repeat(20)}</p>`]);
249
+ assert.deepStrictEqual(
250
+ [...parse('['.repeat(21)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
251
+ [
252
+ '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
253
+ `<pre class="error" translate="no">${'['.repeat(21)}</pre>`,
254
+ ]);
255
+ assert.deepStrictEqual(
256
+ [...parse('{'.repeat(20)).children].map(el => el.outerHTML),
257
+ [`<p>${'{'.repeat(20)}</p>`]);
258
+ assert.deepStrictEqual(
259
+ [...parse('{'.repeat(21)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
260
+ [
261
+ '<h1 id="error:rnd" class="error">Error: Too much recursion</h1>',
262
+ `<pre class="error" translate="no">${'{'.repeat(21)}</pre>`,
263
+ ]);
264
+ });
265
+
266
+ if (!navigator.userAgent.includes('Chrome')) return;
267
+
268
+ it('creation', function () {
269
+ this.timeout(5000);
270
+ // 実測500ms程度
271
+ assert.deepStrictEqual(
272
+ [...parse('.'.repeat(50000)).children].map(el => el.outerHTML),
273
+ [`<p>${'.'.repeat(50000)}</p>`]);
274
+ });
275
+
276
+ it('creation error', function () {
277
+ this.timeout(5000);
278
+ // 実測500ms程度
279
+ assert.deepStrictEqual(
280
+ [...parse('.'.repeat(50001)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
281
+ [
282
+ '<h1 id="error:rnd" class="error">Error: Too many creations</h1>',
283
+ `<pre class="error" translate="no">${'.'.repeat(1000).slice(0, 997)}...</pre>`,
242
284
  ]);
243
285
  });
244
286
 
@@ -1,4 +1,4 @@
1
- import { undefined, location } from 'spica/global';
1
+ import { location } from 'spica/global';
2
2
  import { ParserOptions } from '../../..';
3
3
  import { MarkdownParser } from '../../../markdown';
4
4
  import { eval } from '../../combinator/data/parser';
@@ -22,12 +22,13 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
22
22
  source = !context ? normalize(source) : source;
23
23
  assert(!context?.delimiters);
24
24
  context = {
25
- url: url ? new ReadonlyURL(url as ':') : context?.url,
26
25
  host: opts.host ?? context?.host ?? new ReadonlyURL(location.pathname, location.origin),
26
+ url: url ? new ReadonlyURL(url as ':') : context?.url,
27
+ id: opts.id ?? context?.id,
27
28
  caches: context?.caches,
28
- footnotes: undefined,
29
- test: undefined,
30
- ...opts,
29
+ ...context?.resources && {
30
+ resources: context.resources,
31
+ },
31
32
  };
32
33
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
33
34
  const node = frag();
@@ -1,10 +1,10 @@
1
1
  import { ExtensionParser } from '../../block';
2
- import { block, validate, fence, creator, fmap } from '../../../combinator';
2
+ import { block, validate, fence, fmap } from '../../../combinator';
3
3
  import { identity, text } from '../../inline/extension/indexee';
4
4
  import { parse } from '../../api/parse';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
- export const aside: ExtensionParser.AsideParser = creator(100, block(validate('~~~', fmap(
7
+ export const aside: ExtensionParser.AsideParser = block(validate('~~~', fmap(
8
8
  fence(/^(~{3,})aside(?!\S)([^\n]*)(?:$|\n)/, 300),
9
9
  // Bug: Type mismatch between outer and inner.
10
10
  ([body, overflow, closer, opener, delim, param]: string[], _, context) => {
@@ -43,4 +43,4 @@ export const aside: ExtensionParser.AsideParser = creator(100, block(validate('~
43
43
  references,
44
44
  ]),
45
45
  ];
46
- }))));
46
+ })));
@@ -1,13 +1,13 @@
1
1
  import { ExtensionParser } from '../../block';
2
2
  import { eval } from '../../../combinator/data/parser';
3
- import { block, validate, fence, creator, fmap } from '../../../combinator';
3
+ import { block, validate, fence, fmap } from '../../../combinator';
4
4
  import { parse } from '../../api/parse';
5
5
  import { mathblock } from '../mathblock';
6
6
  import { html } from 'typed-dom/dom';
7
7
 
8
8
  const opener = /^(~{3,})(?:example\/(\S+))?(?!\S)([^\n]*)(?:$|\n)/;
9
9
 
10
- export const example: ExtensionParser.ExampleParser = creator(100, block(validate('~~~', fmap(
10
+ export const example: ExtensionParser.ExampleParser = block(validate('~~~', fmap(
11
11
  fence(opener, 300),
12
12
  // Bug: Type mismatch between outer and inner.
13
13
  ([body, overflow, closer, opener, delim, type = 'markdown', param]: string[], _, context) => {
@@ -58,4 +58,4 @@ export const example: ExtensionParser.ExampleParser = creator(100, block(validat
58
58
  }, `${opener}${body}${closer}`),
59
59
  ];
60
60
  }
61
- }))));
61
+ })));
@@ -36,7 +36,7 @@ export import ReplyParser = BlockParser.ReplyParser;
36
36
  export import ParagraphParser = BlockParser.ParagraphParser;
37
37
 
38
38
  export const block: BlockParser = creator(error(
39
- reset({ resources: { budget: 100 * 1000, recursion: 200 } },
39
+ reset({ resources: { budget: 50 * 1000, recursion: 20 + 1 } },
40
40
  union([
41
41
  emptyline,
42
42
  horizontalrule,