securemark 0.279.1 → 0.280.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 (47) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +10 -10
  3. package/dist/index.js +120 -138
  4. package/markdown.d.ts +1 -7
  5. package/package.json +1 -1
  6. package/src/combinator/data/parser/context/delimiter.ts +49 -21
  7. package/src/combinator/data/parser/context/memo.ts +6 -6
  8. package/src/combinator/data/parser/context.ts +23 -7
  9. package/src/combinator/data/parser/some.ts +1 -1
  10. package/src/parser/api/bind.ts +4 -1
  11. package/src/parser/api/parse.test.ts +13 -13
  12. package/src/parser/api/parse.ts +4 -1
  13. package/src/parser/block/extension/table.ts +1 -1
  14. package/src/parser/block/heading.ts +1 -1
  15. package/src/parser/block/reply/quote.ts +9 -46
  16. package/src/parser/block.ts +2 -2
  17. package/src/parser/inline/annotation.ts +4 -4
  18. package/src/parser/inline/autolink/account.ts +1 -1
  19. package/src/parser/inline/autolink/url.ts +1 -1
  20. package/src/parser/inline/autolink.ts +2 -2
  21. package/src/parser/inline/bracket.test.ts +3 -2
  22. package/src/parser/inline/bracket.ts +8 -9
  23. package/src/parser/inline/deletion.ts +4 -4
  24. package/src/parser/inline/emphasis.ts +4 -4
  25. package/src/parser/inline/emstrong.ts +4 -4
  26. package/src/parser/inline/extension/index.test.ts +6 -0
  27. package/src/parser/inline/extension/index.ts +11 -11
  28. package/src/parser/inline/extension/placeholder.ts +4 -4
  29. package/src/parser/inline/html.test.ts +1 -1
  30. package/src/parser/inline/html.ts +5 -5
  31. package/src/parser/inline/insertion.ts +4 -4
  32. package/src/parser/inline/link.ts +6 -6
  33. package/src/parser/inline/mark.ts +4 -4
  34. package/src/parser/inline/media.ts +4 -4
  35. package/src/parser/inline/reference.ts +3 -3
  36. package/src/parser/inline/remark.test.ts +1 -0
  37. package/src/parser/inline/remark.ts +4 -4
  38. package/src/parser/inline/ruby.ts +2 -2
  39. package/src/parser/inline/strong.ts +4 -4
  40. package/src/parser/inline/template.ts +7 -4
  41. package/src/parser/inline.test.ts +2 -5
  42. package/src/parser/source/str.ts +0 -19
  43. package/src/parser/source/text.test.ts +1 -1
  44. package/src/parser/source/text.ts +3 -3
  45. package/src/parser/source.ts +1 -1
  46. package/src/parser/visibility.ts +3 -1
  47. package/src/util/toc.ts +4 -1
package/markdown.d.ts CHANGED
@@ -631,12 +631,6 @@ export namespace MarkdownParser {
631
631
  export namespace QuoteParser {
632
632
  export interface BlockParser extends
633
633
  Block<'reply/quote/block'>,
634
- Parser<string | HTMLElement, Context, [
635
- TextParser,
636
- ]> {
637
- }
638
- export interface TextParser extends
639
- Block<'reply/quote/text'>,
640
634
  Parser<string | HTMLElement, Context, [
641
635
  InlineParser.MathParser,
642
636
  InlineParser.AutolinkParser,
@@ -784,8 +778,8 @@ export namespace MarkdownParser {
784
778
  // [#index|signature]
785
779
  Inline<'extension/index'>,
786
780
  Parser<HTMLAnchorElement, Context, [
787
- IndexParser.SignatureParser,
788
781
  InlineParser,
782
+ IndexParser.SignatureParser,
789
783
  ]> {
790
784
  }
791
785
  export namespace IndexParser {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.279.1",
3
+ "version": "0.280.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",
@@ -1,5 +1,12 @@
1
1
  import { memoize, reduce } from 'spica/memoize';
2
2
 
3
+ interface Delimiter {
4
+ readonly index: number;
5
+ readonly signature: string;
6
+ readonly matcher: (source: string) => boolean | undefined;
7
+ readonly precedence: number;
8
+ }
9
+
3
10
  export class Delimiters {
4
11
  public static signature(pattern: string | RegExp | undefined): string {
5
12
  switch (typeof pattern) {
@@ -23,43 +30,64 @@ export class Delimiters {
23
30
  }
24
31
  },
25
32
  this.signature);
26
- private readonly matchers: [number, string, number, (source: string) => boolean | undefined][] = [];
27
- private readonly registry: Record<string, boolean> = {};
28
- private length = 0;
33
+ private readonly registry = memoize<(signature: string) => Delimiter[]>(() => []);
34
+ private readonly delimiters: Delimiter[] = [];
35
+ private readonly order: number[] = [];
29
36
  public push(
30
- ...delimiters: readonly {
37
+ delims: readonly {
31
38
  readonly signature: string;
32
39
  readonly matcher: (source: string) => boolean | undefined;
33
40
  readonly precedence?: number;
34
41
  }[]
35
42
  ): void {
36
- for (let i = 0; i < delimiters.length; ++i) {
37
- const delimiter = delimiters[i];
38
- assert(this.length >= this.matchers.length);
39
- const { signature, matcher, precedence = 1 } = delimiter;
40
- if (!this.registry[signature]) {
41
- this.matchers.unshift([this.length, signature, precedence, matcher]);
42
- this.registry[signature] = true;
43
+ const { registry, delimiters, order } = this;
44
+ for (let i = 0; i < delims.length; ++i) {
45
+ const { signature, matcher, precedence = 1 } = delims[i];
46
+ const stack = registry(signature);
47
+ const index = stack[0]?.index ?? delimiters.length;
48
+ if (stack.length === 0 || precedence > delimiters[index].precedence) {
49
+ const delimiter: Delimiter = {
50
+ index,
51
+ signature,
52
+ matcher,
53
+ precedence,
54
+ };
55
+ delimiters[index] = delimiter;
56
+ stack.push(delimiter);
57
+ order.push(index);
58
+ }
59
+ else {
60
+ order.push(-1);
43
61
  }
44
- ++this.length;
45
62
  }
46
63
  }
47
64
  public pop(count = 1): void {
48
65
  assert(count > 0);
66
+ const { registry, delimiters, order } = this;
49
67
  for (let i = 0; i < count; ++i) {
50
- assert(this.matchers.length > 0);
51
- assert(this.length >= this.matchers.length);
52
- if (--this.length === this.matchers[0][0]) {
53
- this.registry[this.matchers.shift()![1]] = false;
68
+ assert(this.order.length > 0);
69
+ const index = order.pop()!;
70
+ if (index === -1) continue;
71
+ const stack = registry(delimiters[index].signature);
72
+ assert(stack.length > 0);
73
+ if (stack.length === 1) {
74
+ assert(index === delimiters.length - 1);
75
+ assert(stack[0] === delimiters.at(-1));
76
+ stack.pop();
77
+ delimiters.pop();
78
+ }
79
+ else {
80
+ stack.pop();
81
+ delimiters[index] = stack.at(-1)!;
54
82
  }
55
83
  }
56
84
  }
57
85
  public match(source: string, precedence = 1): boolean {
58
- const { matchers } = this;
59
- for (let i = 0; i < matchers.length; ++i) {
60
- const matcher = matchers[i];
61
- if (precedence >= matcher[2]) continue;
62
- switch (matcher[3](source)) {
86
+ const { delimiters } = this;
87
+ for (let i = 0; i < delimiters.length; ++i) {
88
+ const delimiter = delimiters[i];
89
+ if (precedence >= delimiter.precedence) continue;
90
+ switch (delimiter.matcher(source)) {
63
91
  case true:
64
92
  return true;
65
93
  case false:
@@ -3,7 +3,7 @@ export class Memo {
3
3
  this.targets = targets;
4
4
  }
5
5
  public readonly targets: number;
6
- private readonly memory: Record<string, readonly [any[], number] | readonly []>[/* pos */] = [];
6
+ private readonly memory: Record<number, Record<number, readonly [any[], number] | readonly []>>[/* pos */] = [];
7
7
  public get length(): number {
8
8
  return this.memory.length;
9
9
  }
@@ -12,8 +12,8 @@ export class Memo {
12
12
  syntax: number,
13
13
  state: number,
14
14
  ): readonly [any[], number] | readonly [] | undefined {
15
- //console.log('get', position, syntax, state, this.memory[position - 1]?.[`${syntax}:${state}`]);;
16
- const cache = this.memory[position - 1]?.[`${syntax}:${state}`];
15
+ //console.log('get', position, syntax, state, this.memory[position - 1]?.[syntax]?.[state]);
16
+ const cache = this.memory[position - 1]?.[syntax]?.[state];
17
17
  return cache?.length === 2
18
18
  ? [cache[0].slice(), cache[1]]
19
19
  : cache;
@@ -26,11 +26,11 @@ export class Memo {
26
26
  offset: number,
27
27
  ): void {
28
28
  const record = this.memory[position - 1] ??= {};
29
- assert(!record[`${syntax}:${state}`]);
30
- record[`${syntax}:${state}`] = nodes
29
+ assert(!record[syntax]?.[state]);
30
+ (record[syntax] ??= {})[state] = nodes
31
31
  ? [nodes.slice(), offset]
32
32
  : [];
33
- //console.log('set', position, syntax, state, record[`${syntax}:${state}`]);
33
+ //console.log('set', position, syntax, state, record[syntax]?.[state]);
34
34
  }
35
35
  public clear(position: number): void {
36
36
  const memory = this.memory;
@@ -66,9 +66,9 @@ function apply<T>(parser: Parser<T>, source: string, context: Ctx, changes: read
66
66
  return result;
67
67
  }
68
68
 
69
- export function syntax<P extends Parser<unknown>>(syntax: number, precedence: number, cost: number, state: number, parser: P): P;
70
- export function syntax<T>(syntax: number, prec: number, cost: number, state: number, parser?: Parser<T>): Parser<T> {
71
- return creation(cost, precedence(prec, ({ source, context }) => {
69
+ export function syntax<P extends Parser<unknown>>(syntax: number, precedence: number, state: number, parser: P): P;
70
+ export function syntax<T>(syntax: number, prec: number, state: number, parser?: Parser<T>): Parser<T> {
71
+ return precedence(prec, ({ source, context }) => {
72
72
  if (source === '') return;
73
73
  const memo = context.memo ??= new Memo();
74
74
  context.offset ??= 0;
@@ -91,7 +91,7 @@ export function syntax<T>(syntax: number, prec: number, cost: number, state: num
91
91
  }
92
92
  context.state = stateOuter;
93
93
  return result;
94
- }));
94
+ });
95
95
  }
96
96
 
97
97
  export function creation<P extends Parser<unknown>>(parser: P): P;
@@ -100,11 +100,13 @@ export function creation<P extends Parser<unknown>>(cost: number, recursion: boo
100
100
  export function creation(cost: number | Parser<unknown>, recursion?: boolean | Parser<unknown>, parser?: Parser<unknown>): Parser<unknown> {
101
101
  if (typeof cost === 'function') return creation(1, true, cost);
102
102
  if (typeof recursion === 'function') return creation(cost, true, recursion);
103
- assert(cost > 0);
103
+ assert(cost >= 0);
104
+ assert(recursion !== undefined);
104
105
  return ({ source, context }) => {
105
- const resources = context.resources ?? { clock: 1, recursion: 1 };
106
+ assert([recursion = recursion!]);
107
+ const resources = context.resources ?? { clock: cost || 1, recursion: 1 };
106
108
  if (resources.clock <= 0) throw new Error('Too many creations');
107
- if (resources.recursion <= 0) throw new Error('Too much recursion');
109
+ if (resources.recursion < +recursion) throw new Error('Too much recursion');
108
110
  recursion && --resources.recursion;
109
111
  const result = parser!({ source, context });
110
112
  recursion && ++resources.recursion;
@@ -171,3 +173,17 @@ export function state<T>(state: number, positive: boolean | Parser<T>, parser?:
171
173
  return result;
172
174
  };
173
175
  }
176
+
177
+ //export function log<P extends Parser<unknown>>(log: number, parser: P, cond?: (ns: readonly Tree<P>[]) => boolean): P;
178
+ //export function log<T>(log: number, parser: Parser<T>, cond: (ns: readonly T[]) => boolean = () => true): Parser<T> {
179
+ // assert(log);
180
+ // return ({ source, context }) => {
181
+ // const l = context.log ?? 0;
182
+ // context.log = 0;
183
+ // const result = parser!({ source, context });
184
+ // context.log = result && cond(eval(result))
185
+ // ? l | log
186
+ // : l;
187
+ // return result;
188
+ // };
189
+ //}
@@ -22,7 +22,7 @@ export function some<T>(parser: Parser<T>, end?: string | RegExp | number, delim
22
22
  let nodes: T[] | undefined;
23
23
  if (delims.length > 0) {
24
24
  context.delimiters ??= new Delimiters();
25
- context.delimiters.push(...delims);
25
+ context.delimiters.push(delims);
26
26
  }
27
27
  while (true) {
28
28
  if (rest === '') break;
@@ -26,9 +26,12 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
26
26
  host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
27
27
  memo: new Memo({ targets: State.backtrackers }),
28
28
  };
29
+ assert(!context.offset);
30
+ assert(!context.precedence);
31
+ assert(!context.delimiters);
32
+ assert(!context.state);
29
33
  if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
30
34
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
31
- assert(!settings.id);
32
35
  type Block = readonly [segment: string, blocks: readonly HTMLElement[], url: string];
33
36
  const blocks: Block[] = [];
34
37
  const adds: [HTMLElement, Node | null][] = [];
@@ -298,17 +298,17 @@ describe('Unit: parser/api/parse', () => {
298
298
 
299
299
  it('recursion', () => {
300
300
  assert.deepStrictEqual(
301
- [...parse('{'.repeat(20)).children].map(el => el.outerHTML),
302
- [`<p>${'{'.repeat(20)}</p>`]);
301
+ [...parse('{'.repeat(21)).children].map(el => el.outerHTML),
302
+ [`<p>${'{'.repeat(21)}</p>`]);
303
303
  assert.deepStrictEqual(
304
- [...parse('{'.repeat(21)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
304
+ [...parse('{'.repeat(22)).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(21)}</pre>`,
307
+ `<pre class="error" translate="no">${'{'.repeat(22)}</pre>`,
308
308
  ]);
309
309
  assert.deepStrictEqual(
310
- [...parse('('.repeat(20)).children].map(el => el.outerHTML),
311
- [`<p>${'('.repeat(20)}</p>`]);
310
+ [...parse('('.repeat(21)).children].map(el => el.outerHTML),
311
+ [`<p>${'('.repeat(21)}</p>`]);
312
312
  assert.deepStrictEqual(
313
313
  [...parse('('.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
314
314
  [
@@ -316,8 +316,8 @@ describe('Unit: parser/api/parse', () => {
316
316
  `<pre class="error" translate="no">${'('.repeat(22)}</pre>`,
317
317
  ]);
318
318
  assert.deepStrictEqual(
319
- [...parse('['.repeat(20)).children].map(el => el.outerHTML),
320
- [`<p>${'['.repeat(20)}</p>`]);
319
+ [...parse('['.repeat(21)).children].map(el => el.outerHTML),
320
+ [`<p>${'['.repeat(21)}</p>`]);
321
321
  assert.deepStrictEqual(
322
322
  [...parse('['.repeat(22)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
323
323
  [
@@ -325,8 +325,8 @@ describe('Unit: parser/api/parse', () => {
325
325
  `<pre class="error" translate="no">${'['.repeat(22)}</pre>`,
326
326
  ]);
327
327
  assert.deepStrictEqual(
328
- [...parse('['.repeat(17) + '\na').children].map(el => el.outerHTML),
329
- [`<p>${'['.repeat(17)}<br>a</p>`]);
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;
@@ -335,15 +335,15 @@ describe('Unit: parser/api/parse', () => {
335
335
  this.timeout(5000);
336
336
  // 実測500ms程度
337
337
  assert.deepStrictEqual(
338
- [...parse('.'.repeat(50000)).children].map(el => el.outerHTML),
339
- [`<p>${'.'.repeat(50000)}</p>`]);
338
+ [...parse('.'.repeat(20000)).children].map(el => el.outerHTML),
339
+ [`<p>${'.'.repeat(20000)}</p>`]);
340
340
  });
341
341
 
342
342
  it('creation error', function () {
343
343
  this.timeout(5000);
344
344
  // 実測500ms程度
345
345
  assert.deepStrictEqual(
346
- [...parse('.'.repeat(50001)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
346
+ [...parse('.'.repeat(20001)).children].map(el => el.outerHTML.replace(/:\w+/, ':rnd')),
347
347
  [
348
348
  '<h1 id="error:rnd" class="error">Error: Too many creations</h1>',
349
349
  `<pre class="error" translate="no">${'.'.repeat(1000).slice(0, 997)}...</pre>`,
@@ -21,7 +21,6 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
21
21
  if (!validate(source, MAX_SEGMENT_SIZE)) throw new Error(`Too large input over ${MAX_SEGMENT_SIZE.toLocaleString('en')} bytes`);
22
22
  const url = headers(source).find(field => field.toLowerCase().startsWith('url:'))?.slice(4).trim() ?? '';
23
23
  source = !context ? normalize(source) : source;
24
- assert(!context?.delimiters);
25
24
  context = {
26
25
  host: opts.host ?? context?.host ?? new ReadonlyURL(location.pathname, location.origin),
27
26
  url: url ? new ReadonlyURL(url as ':') : context?.url,
@@ -32,6 +31,10 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
32
31
  },
33
32
  memo: new Memo({ targets: State.backtrackers }),
34
33
  };
34
+ assert(!context.offset);
35
+ assert(!context.precedence);
36
+ assert(!context.delimiters);
37
+ assert(!context.state);
35
38
  if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
36
39
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
37
40
  const node = frag();
@@ -119,7 +119,7 @@ const dataline: CellParser.DatalineParser = creation(1, false, line(
119
119
  convert(source => `: ${source}`, data),
120
120
  ]))));
121
121
 
122
- function attributes(source: string) {
122
+ function attributes(source: string): Record<string, string | undefined> {
123
123
  let [, rowspan = undefined, colspan = undefined, highlight = undefined, extension = undefined] =
124
124
  source.match(/^[#:](?:(\d+)?:(\d+)?)?(?:(!+)([+]?))?$/) ?? [];
125
125
  assert(rowspan?.[0] !== '0');
@@ -18,7 +18,7 @@ export const heading: HeadingParser = block(rewrite(segment,
18
18
  visualize(trimBlankStart(some(union([indexer, inline])))), true),
19
19
  open(
20
20
  str('#'),
21
- state(State.autolink,
21
+ state(State.linkers,
22
22
  visualize(trimBlankStart(some(union([indexer, inline]))))), true),
23
23
  ]),
24
24
  ([h, ...ns]: [string, ...(HTMLElement | string)[]]) => [
@@ -1,6 +1,5 @@
1
1
  import { ReplyParser } from '../../block';
2
- import { eval } from '../../../combinator/data/parser';
3
- import { union, some, creation, block, line, validate, rewrite, lazy, fmap } from '../../../combinator';
2
+ import { union, some, creation, block, line, validate, rewrite, convert, lazy, fmap } from '../../../combinator';
4
3
  import { math } from '../../inline/math';
5
4
  import { autolink } from '../../inline/autolink';
6
5
  import { linebreak, unescsource, str, anyline } from '../../source';
@@ -33,47 +32,11 @@ export const quote: ReplyParser.QuoteParser = lazy(() => creation(1, false, bloc
33
32
  ]),
34
33
  false)));
35
34
 
36
- const qblock: ReplyParser.QuoteParser.BlockParser = ({ source, context }) => {
37
- source = source.replace(/\n$/, '');
38
- const lines = source.match(/^.*\n?/mg)!;
39
- assert(lines);
40
- const quotes = source.match(/^>+[^\S\n]/mg)!;
41
- assert(quotes);
42
- assert(quotes.length > 0);
43
- const content = lines.reduce((acc, line, i) => acc + line.slice(quotes[i].length), '');
44
- const nodes = eval(text({ source: `\r${content}`, context }), []);
45
- nodes.unshift(quotes.shift()!);
46
- for (let i = 0; i < nodes.length; ++i) {
47
- const child = nodes[i] as string | Text | Element;
48
- if (typeof child === 'string') continue;
49
- if ('wholeText' in child) {
50
- nodes[i] = child.data;
51
- continue;
52
- }
53
- assert(child instanceof HTMLElement);
54
- if (child.tagName === 'BR') {
55
- assert(quotes.length > 0);
56
- nodes.splice(i + 1, 0, quotes.shift()!);
57
- ++i;
58
- continue;
59
- }
60
- if (child.className === 'cite' || child.classList.contains('quote')) {
61
- context.resources && (context.resources.clock -= child.childNodes.length);
62
- nodes.splice(i, 1, ...child.childNodes as NodeListOf<HTMLElement>);
63
- --i;
64
- continue;
65
- }
66
- }
67
- nodes.unshift('');
68
- assert(nodes.length > 1);
69
- assert(nodes.every(n => typeof n === 'string' || n instanceof HTMLElement));
70
- assert(quotes.length === 0);
71
- return [nodes, ''];
72
- };
73
-
74
- const text: ReplyParser.QuoteParser.TextParser = some(union([
75
- math, // quote補助関数が残した数式をパースする。他の構文で数式を残す場合はソーステキストを直接使用する。
76
- autolink,
77
- linebreak,
78
- unescsource,
79
- ]));
35
+ const qblock: ReplyParser.QuoteParser.BlockParser = convert(
36
+ source => source.replace(/\n$/, '').replace(/(?<=^>+[^\S\n])/mg, '\r'),
37
+ some(union([
38
+ math, // quote補助関数が残した数式をパースする。他の構文で数式を残す場合はソーステキストを直接使用する。
39
+ autolink,
40
+ linebreak,
41
+ unescsource,
42
+ ])));
@@ -36,8 +36,8 @@ export import MediaBlockParser = BlockParser.MediaBlockParser;
36
36
  export import ReplyParser = BlockParser.ReplyParser;
37
37
  export import ParagraphParser = BlockParser.ParagraphParser;
38
38
 
39
- export const block: BlockParser = creation(1, false, error(
40
- reset({ resources: { clock: 50 * 1000, recursion: 20 } },
39
+ export const block: BlockParser = creation(0, false, error(
40
+ reset({ resources: { clock: 20000, recursion: 20 + 1 } },
41
41
  union([
42
42
  emptyline,
43
43
  pagebreak,
@@ -1,15 +1,15 @@
1
1
  import { AnnotationParser } from '../inline';
2
- import { union, some, syntax, constraint, surround, lazy } from '../../combinator';
2
+ import { union, some, syntax, creation, constraint, surround, lazy } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { Syntax, State } from '../context';
5
5
  import { trimBlankStart, trimNodeEnd } from '../visibility';
6
6
  import { html, defrag } from 'typed-dom/dom';
7
7
 
8
- export const annotation: AnnotationParser = lazy(() => surround(
8
+ export const annotation: AnnotationParser = lazy(() => creation(surround(
9
9
  '((',
10
10
  constraint(State.annotation, false,
11
- syntax(Syntax.none, 6, 1, State.annotation | State.media,
11
+ syntax(Syntax.none, 6, State.annotation | State.media,
12
12
  trimBlankStart(some(union([inline]), ')', [[/^\\?\n/, 9], [')', 2], ['))', 6]])))),
13
13
  '))',
14
14
  false,
15
- ([, ns], rest) => [[html('sup', { class: 'annotation' }, [html('span', trimNodeEnd(defrag(ns)))])], rest]));
15
+ ([, ns], rest) => [[html('sup', { class: 'annotation' }, [html('span', trimNodeEnd(defrag(ns)))])], rest])));
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, constraint, tails, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, tails, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { State } from '../../context';
@@ -28,5 +28,5 @@ const bracket: AutolinkParser.UrlParser.BracketParser = lazy(() => creation(prec
28
28
  surround('(', some(union([bracket, unescsource]), ')'), ')', true),
29
29
  surround('[', some(union([bracket, unescsource]), ']'), ']', true),
30
30
  surround('{', some(union([bracket, unescsource]), '}'), '}', true),
31
- surround('"', precedence(8, some(unescsource, '"')), '"', true),
31
+ surround('"', precedence(3, some(unescsource, '"')), '"', true),
32
32
  ]))));
@@ -12,9 +12,9 @@ import { Syntax, State } from '../context';
12
12
  import { stringify } from '../util';
13
13
 
14
14
  export const autolink: AutolinkParser = lazy(() =>
15
- validate(/^(?:[@#>0-9a-z\r\n]|\S[#>])/iu,
15
+ validate(/^(?:[@#>0-9a-z]|\S[#>]|[\r\n]!?https?:\/\/)/iu,
16
16
  constraint(State.autolink, false,
17
- syntax(Syntax.autolink, 1, 1, ~State.shortcut,
17
+ syntax(Syntax.autolink, 1, ~State.shortcut,
18
18
  union([
19
19
  some(union([lineurl])),
20
20
  fmap(some(union([
@@ -48,6 +48,9 @@ describe('Unit: parser/inline/bracket', () => {
48
48
  assert.deepStrictEqual(inspect(parser('(A)')), [['(', 'A', ')'], '']);
49
49
  assert.deepStrictEqual(inspect(parser('(A,B)')), [['(', 'A,B', ')'], '']);
50
50
  assert.deepStrictEqual(inspect(parser('(A、B)')), [['(', 'A、B', ')'], '']);
51
+ assert.deepStrictEqual(inspect(parser('(<bdi>a\\\nb</bdi>)')), [['<span class="paren">(<bdi>a<br>b</bdi>)</span>'], '']);
52
+ assert.deepStrictEqual(inspect(parser('([% a\\\nb %])')), [['<span class="paren">(<span class="remark"><input type="checkbox"><span>[% a<br>b %]</span></span>)</span>'], '']);
53
+ assert.deepStrictEqual(inspect(parser('({{\\\n}})')), [['<span class="paren">(<span class="template">{{\\\n}}</span>)</span>'], '']);
51
54
  });
52
55
 
53
56
  it('[', () => {
@@ -77,8 +80,6 @@ describe('Unit: parser/inline/bracket', () => {
77
80
  assert.deepStrictEqual(inspect(parser('"(")"')), [['"', '(', '"'], ')"']);
78
81
  assert.deepStrictEqual(inspect(parser('"(("')), [['"', '(', '(', '"'], '']);
79
82
  assert.deepStrictEqual(inspect(parser('"(\\")"')), [['"', '<span class="paren">(")</span>', '"'], '']);
80
- assert.deepStrictEqual(inspect(parser('"(\n)"')), [['"', '<span class="paren">(<br>)</span>', '"'], '']);
81
- assert.deepStrictEqual(inspect(parser('"(\\\n)"')), [['"', '<span class="paren">(<br>)</span>', '"'], '']);
82
83
  });
83
84
 
84
85
  });
@@ -1,5 +1,5 @@
1
1
  import { BracketParser } from '../inline';
2
- import { union, some, syntax, surround, lazy } from '../../combinator';
2
+ import { union, some, syntax, creation, surround, lazy } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { str } from '../source';
5
5
  import { Syntax, State } from '../context';
@@ -9,22 +9,21 @@ import { html, defrag } from 'typed-dom/dom';
9
9
  const index = /^[0-9A-Za-z]+(?:(?:[.-]|, )[0-9A-Za-z]+)*/;
10
10
 
11
11
  export const bracket: BracketParser = lazy(() => union([
12
- surround(str('('), syntax(Syntax.none, 2, 1, State.none, str(index)), str(')')),
13
- surround(str('('), syntax(Syntax.bracket, 2, 1, State.none, some(inline, ')', [[')', 2]])), str(')'), true,
12
+ surround(str('('), creation(syntax(Syntax.none, 2, State.none, str(index))), str(')')),
13
+ surround(str('('), creation(syntax(Syntax.bracket, 2, State.none, some(inline, ')', [[/^\\?\n/, 3], [')', 2]]))), str(')'), true,
14
14
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
15
15
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
16
- surround(str('('), syntax(Syntax.none, 2, 1, State.none, str(new RegExp(index.source.replace(', ', '[,、]').replace(/[09AZaz.]|\-(?!\w)/g, c => c.trimStart() && String.fromCharCode(c.charCodeAt(0) + 0xFEE0))))), str(')')),
17
- surround(str('('), syntax(Syntax.bracket, 2, 1, State.none, some(inline, ')', [[')', 2]])), str(')'), true,
16
+ surround(str('('), creation(syntax(Syntax.none, 2, State.none, str(new RegExp(index.source.replace(', ', '[,、]').replace(/[09AZaz.]|\-(?!\w)/g, c => c.trimStart() && String.fromCharCode(c.charCodeAt(0) + 0xFEE0)))))), str(')')),
17
+ surround(str('('), creation(syntax(Syntax.bracket, 2, State.none, some(inline, ')', [[/^\\?\n/, 3], [')', 2]]))), str(')'), true,
18
18
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
19
19
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
20
- surround(str('['), syntax(Syntax.bracket, 2, 1, State.none, some(inline, ']', [[']', 2]])), str(']'), true,
20
+ surround(str('['), creation(syntax(Syntax.bracket, 2, State.none, some(inline, ']', [[/^\\?\n/, 3], [']', 2]]))), str(']'), true,
21
21
  undefined,
22
22
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
23
- surround(str('{'), syntax(Syntax.bracket, 2, 1, State.none, some(inline, '}', [['}', 2]])), str('}'), true,
23
+ surround(str('{'), creation(syntax(Syntax.bracket, 2, State.none, some(inline, '}', [[/^\\?\n/, 3], ['}', 2]]))), str('}'), true,
24
24
  undefined,
25
25
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
26
- // Control media blinking in editing rather than control confusion of pairs of quote marks.
27
- surround(str('"'), syntax(Syntax.none, 3, 1, State.none, some(inline, '"', [['"', 3]])), str('"'), true,
26
+ surround(str('"'), creation(syntax(Syntax.none, 3, State.none, some(inline, '"', [[/^\\?\n/, 4], ['"', 3]]))), str('"'), true,
28
27
  undefined,
29
28
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
30
29
  ]));
@@ -1,5 +1,5 @@
1
1
  import { DeletionParser } from '../inline';
2
- import { union, some, syntax, surround, open, lazy } from '../../combinator';
2
+ import { union, some, syntax, creation, surround, open, lazy } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { str } from '../source';
5
5
  import { Syntax, State } from '../context';
@@ -7,13 +7,13 @@ import { blankWith } from '../visibility';
7
7
  import { unshift } from 'spica/array';
8
8
  import { html, defrag } from 'typed-dom/dom';
9
9
 
10
- export const deletion: DeletionParser = lazy(() => surround(
10
+ export const deletion: DeletionParser = lazy(() => creation(surround(
11
11
  str('~~', '~'),
12
- syntax(Syntax.none, 1, 1, State.none,
12
+ syntax(Syntax.none, 1, State.none,
13
13
  some(union([
14
14
  some(inline, blankWith('\n', '~~')),
15
15
  open('\n', some(inline, '~'), true),
16
16
  ]))),
17
17
  str('~~'), false,
18
18
  ([, bs], rest) => [[html('del', defrag(bs))], rest],
19
- ([as, bs], rest) => [unshift(as, bs), rest]));
19
+ ([as, bs], rest) => [unshift(as, bs), rest])));
@@ -1,5 +1,5 @@
1
1
  import { EmphasisParser } from '../inline';
2
- import { union, some, syntax, surround, open, lazy } from '../../combinator';
2
+ import { union, some, syntax, creation, surround, open, lazy } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { emstrong } from './emstrong';
5
5
  import { strong } from './strong';
@@ -9,9 +9,9 @@ import { startTight, blankWith } from '../visibility';
9
9
  import { unshift } from 'spica/array';
10
10
  import { html, defrag } from 'typed-dom/dom';
11
11
 
12
- export const emphasis: EmphasisParser = lazy(() => surround(
12
+ export const emphasis: EmphasisParser = lazy(() => creation(surround(
13
13
  str('*', '*'),
14
- syntax(Syntax.none, 1, 1, State.none,
14
+ syntax(Syntax.none, 1, State.none,
15
15
  startTight(some(union([
16
16
  strong,
17
17
  some(inline, blankWith('*'), [[/^\\?\n/, 9]]),
@@ -23,4 +23,4 @@ export const emphasis: EmphasisParser = lazy(() => surround(
23
23
  ])))),
24
24
  str('*'), false,
25
25
  ([, bs], rest) => [[html('em', defrag(bs))], rest],
26
- ([as, bs], rest) => [unshift(as, bs), rest]));
26
+ ([as, bs], rest) => [unshift(as, bs), rest])));
@@ -1,6 +1,6 @@
1
1
  import { EmStrongParser, EmphasisParser, StrongParser } from '../inline';
2
2
  import { Result, IntermediateParser } from '../../combinator/data/parser';
3
- import { union, syntax, some, surround, open, lazy, bind } from '../../combinator';
3
+ import { union, syntax, creation, some, surround, open, lazy, bind } from '../../combinator';
4
4
  import { inline } from '../inline';
5
5
  import { strong } from './strong';
6
6
  import { emphasis } from './emphasis';
@@ -27,9 +27,9 @@ const subemphasis: IntermediateParser<EmphasisParser> = lazy(() => some(union([
27
27
  ])),
28
28
  ])));
29
29
 
30
- export const emstrong: EmStrongParser = lazy(() => surround(
30
+ export const emstrong: EmStrongParser = lazy(() => creation(surround(
31
31
  str('***'),
32
- syntax(Syntax.none, 1, 1, State.none,
32
+ syntax(Syntax.none, 1, State.none,
33
33
  startTight(some(union([
34
34
  some(inline, blankWith('*'), [[/^\\?\n/, 9]]),
35
35
  open(some(inline, '*', [[/^\\?\n/, 9]]), inline),
@@ -59,4 +59,4 @@ export const emstrong: EmStrongParser = lazy(() => surround(
59
59
  }
60
60
  assert(false);
61
61
  },
62
- ([as, bs], rest) => [unshift(as, bs), rest]));
62
+ ([as, bs], rest) => [unshift(as, bs), rest])));
@@ -20,6 +20,12 @@ describe('Unit: parser/inline/extension/index', () => {
20
20
  assert.deepStrictEqual(inspect(parser('[#\\]')), undefined);
21
21
  assert.deepStrictEqual(inspect(parser('[#a')), undefined);
22
22
  assert.deepStrictEqual(inspect(parser('[#*a\nb*]')), undefined);
23
+ assert.deepStrictEqual(inspect(parser('[#(a\nb)]')), undefined);
24
+ assert.deepStrictEqual(inspect(parser('[#"a\nb"]')), undefined);
25
+ assert.deepStrictEqual(inspect(parser('[#<bdi>a\nb</bdi>]')), undefined);
26
+ assert.deepStrictEqual(inspect(parser('[#[% a\nb %]]')), undefined);
27
+ assert.deepStrictEqual(inspect(parser('[#{{ a\nb }}]')), undefined);
28
+ assert.deepStrictEqual(inspect(parser('[#({{ a\nb }})]')), undefined);
23
29
  assert.deepStrictEqual(inspect(parser('[#a\n|b]')), undefined);
24
30
  assert.deepStrictEqual(inspect(parser('[#a|\n]')), undefined);
25
31
  assert.deepStrictEqual(inspect(parser('[#a|\\\n]')), undefined);