securemark 0.290.2 → 0.291.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/design.md +48 -2
  3. package/dist/index.js +215 -121
  4. package/markdown.d.ts +6 -17
  5. package/package.json +1 -1
  6. package/src/combinator/control/manipulation/surround.ts +17 -13
  7. package/src/combinator/data/parser/context/delimiter.ts +2 -2
  8. package/src/combinator/data/parser/context.ts +4 -3
  9. package/src/combinator/data/parser/some.ts +3 -3
  10. package/src/combinator/data/parser.ts +5 -3
  11. package/src/parser/api/parse.test.ts +34 -30
  12. package/src/parser/block/dlist.ts +1 -1
  13. package/src/parser/block/heading.ts +2 -2
  14. package/src/parser/block/mediablock.ts +2 -2
  15. package/src/parser/block/pagebreak.ts +1 -1
  16. package/src/parser/block/reply.ts +1 -1
  17. package/src/parser/block.ts +3 -1
  18. package/src/parser/header.ts +1 -1
  19. package/src/parser/inline/annotation.ts +4 -5
  20. package/src/parser/inline/autolink/account.ts +1 -1
  21. package/src/parser/inline/autolink/anchor.ts +1 -1
  22. package/src/parser/inline/autolink/channel.ts +1 -1
  23. package/src/parser/inline/autolink/email.ts +1 -1
  24. package/src/parser/inline/autolink/hashnum.ts +1 -1
  25. package/src/parser/inline/autolink/hashtag.ts +1 -1
  26. package/src/parser/inline/autolink/url.test.ts +8 -2
  27. package/src/parser/inline/autolink/url.ts +5 -6
  28. package/src/parser/inline/bracket.ts +25 -3
  29. package/src/parser/inline/code.ts +2 -2
  30. package/src/parser/inline/extension/index.ts +42 -26
  31. package/src/parser/inline/extension/indexee.ts +6 -3
  32. package/src/parser/inline/extension/label.ts +1 -1
  33. package/src/parser/inline/html.test.ts +22 -19
  34. package/src/parser/inline/html.ts +82 -78
  35. package/src/parser/inline/link.ts +12 -9
  36. package/src/parser/inline/mark.ts +1 -1
  37. package/src/parser/inline/math.test.ts +1 -0
  38. package/src/parser/inline/math.ts +18 -9
  39. package/src/parser/inline/media.test.ts +3 -3
  40. package/src/parser/inline/media.ts +14 -6
  41. package/src/parser/inline/reference.ts +67 -10
  42. package/src/parser/inline/ruby.ts +6 -5
  43. package/src/parser/inline/shortmedia.ts +1 -1
  44. package/src/parser/inline.ts +4 -19
  45. package/src/parser/segment.test.ts +3 -2
  46. package/src/parser/segment.ts +2 -2
  47. package/src/parser/source/escapable.ts +1 -1
  48. package/src/parser/source/text.ts +2 -2
  49. package/src/parser/source/unescapable.ts +1 -1
  50. package/src/parser/util.ts +14 -4
  51. package/src/parser/visibility.ts +1 -0
@@ -103,9 +103,9 @@ describe('Unit: parser/inline/media', () => {
103
103
  });
104
104
 
105
105
  it('attribute', () => {
106
- assert.deepStrictEqual(inspect(parser('![]{/ __proto__}')), [['<a href="/" target="_blank"><img class="media invalid" data-src="/" alt="/"></a>'], '']);
107
- assert.deepStrictEqual(inspect(parser('![]{/ constructor}')), [['<a href="/" target="_blank"><img class="media invalid" data-src="/" alt="/"></a>'], '']);
108
- assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio}')), [['<a href="/" target="_blank"><img class="media invalid" data-src="/" alt="/"></a>'], '']);
106
+ assert.deepStrictEqual(inspect(parser('![]{/ __proto__}')), [['<a href="/" target="_blank"><img class="invalid" data-src="/" alt="/"></a>'], '']);
107
+ assert.deepStrictEqual(inspect(parser('![]{/ constructor}')), [['<a href="/" target="_blank"><img class="invalid" data-src="/" alt="/"></a>'], '']);
108
+ assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio}')), [['<a href="/" target="_blank"><img class="invalid" data-src="/" alt="/"></a>'], '']);
109
109
  assert.deepStrictEqual(inspect(parser('![]{/ nofollow}')), [['<a href="/" rel="nofollow" target="_blank"><img class="media" data-src="/" alt="/"></a>'], '']);
110
110
  assert.deepStrictEqual(inspect(parser('![]{/ width="4" height="3"}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="/" width="4" height="3"></a>'], '']);
111
111
  assert.deepStrictEqual(inspect(parser('![]{/ 4x3}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="/" width="4" height="3"></a>'], '']);
@@ -7,7 +7,6 @@ import { unsafehtmlentity } from './htmlentity';
7
7
  import { txt, linebreak, str } from '../source';
8
8
  import { invalid } from '../util';
9
9
  import { ReadonlyURL } from 'spica/url';
10
- import { push } from 'spica/array';
11
10
  import { html, define } from 'typed-dom/dom';
12
11
 
13
12
  const optspec = {
@@ -18,7 +17,7 @@ const optspec = {
18
17
  } as const;
19
18
  Object.setPrototypeOf(optspec, null);
20
19
 
21
- export const media: MediaParser = lazy(() => constraint(State.media, false, validate(['![', '!{'], creation(10, open(
20
+ export const media: MediaParser = lazy(() => constraint(State.media, validate(['![', '!{'], creation(10, open(
22
21
  '!',
23
22
  bind(verify(fmap(tails([
24
23
  dup(surround(
@@ -31,7 +30,7 @@ export const media: MediaParser = lazy(() => constraint(State.media, false, vali
31
30
  ']',
32
31
  true,
33
32
  ([, ns = []], rest, context) =>
34
- context.linebreak === undefined
33
+ context.linebreak === 0
35
34
  ? [ns, rest]
36
35
  : undefined,
37
36
  undefined,
@@ -68,7 +67,7 @@ export const media: MediaParser = lazy(() => constraint(State.media, false, vali
68
67
  el.setAttribute('alt', text);
69
68
  if (!sanitize(el, uri)) return [[el], rest];
70
69
  assert(!el.matches('.invalid'));
71
- define(el, attributes('media', push([], el.classList), optspec, params));
70
+ define(el, attributes('media', optspec, params));
72
71
  assert(el.matches('img') || !el.matches('.invalid'));
73
72
  // Awaiting the generic support for attr().
74
73
  if (el.hasAttribute('aspect-ratio')) {
@@ -99,8 +98,17 @@ const bracket: MediaParser.TextParser.BracketParser = lazy(() => recursion(Recur
99
98
  ])));
100
99
 
101
100
  const option: MediaParser.ParameterParser.OptionParser = lazy(() => union([
102
- fmap(str(/^[^\S\n]+[1-9][0-9]*x[1-9][0-9]*(?=[^\S\n]|})/), ([opt]) => [` width="${opt.slice(1).split('x')[0]}"`, ` height="${opt.slice(1).split('x')[1]}"`]),
103
- fmap(str(/^[^\S\n]+[1-9][0-9]*:[1-9][0-9]*(?=[^\S\n]|})/), ([opt]) => [` aspect-ratio="${opt.slice(1).split(':').join('/')}"`]),
101
+ surround(
102
+ open(/^[^\S\n]+/, str(/^[1-9][0-9]*/)),
103
+ str(/^[x:]/),
104
+ str(/^[1-9][0-9]*(?=[^\S\n]|})/),
105
+ false,
106
+ ([[a], [b], [c]], rest) => [
107
+ b === 'x'
108
+ ? [`width="${a}"`, `height="${c}"`]
109
+ : [`aspect-ratio="${a}/${c}"`],
110
+ rest
111
+ ]),
104
112
  linkoption,
105
113
  ]));
106
114
 
@@ -1,30 +1,87 @@
1
1
  import { ReferenceParser } from '../inline';
2
2
  import { State, Backtrack, Command } from '../context';
3
- import { union, subsequence, some, precedence, state, constraint, surround, setBacktrack, lazy } from '../../combinator';
3
+ import { eval, exec } from '../../combinator/data/parser';
4
+ import { union, subsequence, some, precedence, state, constraint, surround, isBacktrack, setBacktrack, lazy } from '../../combinator';
4
5
  import { inline } from '../inline';
6
+ import { textlink } from './link';
5
7
  import { str } from '../source';
6
8
  import { blank, trimBlankStart, trimBlankNodeEnd } from '../visibility';
7
9
  import { html, defrag } from 'typed-dom/dom';
8
- import { unshift } from 'spica/array';
10
+ import { unshift, push } from 'spica/array';
9
11
  import { invalid } from '../util';
10
12
 
11
- export const reference: ReferenceParser = lazy(() => constraint(State.reference, false, surround(
13
+ export const reference: ReferenceParser = lazy(() => constraint(State.reference, surround(
12
14
  str('[['),
13
- precedence(1, state(State.annotation | State.reference | State.media,
15
+ precedence(1, state(State.annotation | State.reference,
14
16
  subsequence([
15
17
  abbr,
16
18
  trimBlankStart(some(inline, ']', [[']', 1]])),
17
19
  ]))),
18
20
  ']]',
19
21
  false,
20
- ([, ns], rest, context) =>
21
- context.linebreak === undefined &&
22
- trimBlankNodeEnd(ns).length > 0
23
- ? [[html('sup', attributes(ns), [html('span', defrag(ns))])], rest]
24
- : undefined,
22
+ ([, ns], rest, context) => {
23
+ if (context.linebreak === 0) {
24
+ return [[html('sup', attributes(ns), [html('span', defrag(trimBlankNodeEnd(ns)))])], rest];
25
+ }
26
+ else {
27
+ const head = context.recent!.reduce((a, b) => a + b.length, rest.length);
28
+ setBacktrack(context, [2 | Backtrack.link], head, 2);
29
+ }
30
+ },
25
31
  ([as, bs], rest, context) => {
32
+ const { recent } = context;
33
+ const head = recent!.reduce((a, b) => a + b.length, rest.length);
26
34
  if (rest[0] !== ']') {
27
- setBacktrack(context, [2 | Backtrack.bracket], context.recent!.reduce((a, b) => a + b.length, 0), 2);
35
+ setBacktrack(context, [2 | Backtrack.bracket], head, 2);
36
+ }
37
+ else if (context.linebreak! > 0) {
38
+ setBacktrack(context, [2 | Backtrack.link], head, 2);
39
+ }
40
+ else {
41
+ assert(rest[0] === ']');
42
+ if (context.state! & State.annotation) {
43
+ push(bs, [rest[0]]);
44
+ }
45
+ const source = rest.slice(1);
46
+ let result: ReturnType<typeof textlink>;
47
+ if (source[0] !== '{') {
48
+ setBacktrack(context, [2 | Backtrack.link], head - 1);
49
+ result = [[], source];
50
+ }
51
+ else {
52
+ assert(source.length > 0);
53
+ result = !isBacktrack(context, [1 | Backtrack.link], source)
54
+ ? textlink({ source, context })
55
+ : undefined;
56
+ context.recent = recent;
57
+ if (!result) {
58
+ setBacktrack(context, [2 | Backtrack.link], head - 1);
59
+ result = [[], source];
60
+ }
61
+ }
62
+ assert(result);
63
+ if (exec(result) === '') {
64
+ setBacktrack(context, [2 | Backtrack.link], head);
65
+ }
66
+ else {
67
+ assert(context.state! ^ State.link);
68
+ const next = surround(
69
+ '',
70
+ some(inline, ']', [[']', 1]]),
71
+ str(']'),
72
+ true,
73
+ ([, cs = [], ds], rest) =>
74
+ [push(cs, ds), rest],
75
+ ([, cs = []], rest) => {
76
+ setBacktrack(context, [2 | Backtrack.link], head);
77
+ return [cs, rest];
78
+ })
79
+ ({ source: exec(result), context });
80
+ if (context.state! & State.annotation && next) {
81
+ push(push(bs, eval(result)), eval(next));
82
+ rest = exec(next);
83
+ }
84
+ }
28
85
  }
29
86
  return context.state! & State.annotation
30
87
  ? [unshift(as, bs), rest]
@@ -18,15 +18,16 @@ export const ruby: RubyParser = lazy(() => bind(
18
18
  return isTightNodeStart(ns) ? [ns, rest] : undefined;
19
19
  },
20
20
  undefined,
21
- [3 | Backtrack.ruby | 1, Backtrack.link, 1 | Backtrack.bracket])),
21
+ [3 | Backtrack.ruby, 1 | Backtrack.bracket])),
22
22
  dup(surround(
23
23
  '(', rtext, ')',
24
24
  false, undefined, undefined,
25
- [3 | Backtrack.ruby | 1, Backtrack.link, 1 | Backtrack.bracket])),
25
+ [3 | Backtrack.ruby, 1 | Backtrack.bracket])),
26
26
  ]),
27
27
  ([texts, rubies], rest, context) => {
28
28
  if (rubies === undefined) {
29
- return void setBacktrack(context, [2 | Backtrack.ruby], context.recent!.reduce((a, b) => a + b.length, 0));
29
+ const head = context.recent!.reduce((a, b) => a + b.length, rest.length);
30
+ return void setBacktrack(context, [2 | Backtrack.ruby], head);
30
31
  }
31
32
  switch (true) {
32
33
  case rubies.length <= texts.length:
@@ -65,7 +66,7 @@ const rtext: RubyParser.TextParser = ({ source, context }) => {
65
66
  const acc = [''];
66
67
  let state = false;
67
68
  while (source !== '') {
68
- if (!/^(?:\\[^\n]|[^\\[\](){}<>"\n])/.test(source)) break;
69
+ if (!/^(?:\\[^\n]|[^\\[\](){}<>"$#\n])/.test(source)) break;
69
70
  assert(source[0] !== '\n');
70
71
  switch (source[0]) {
71
72
  // @ts-expect-error
@@ -73,7 +74,7 @@ const rtext: RubyParser.TextParser = ({ source, context }) => {
73
74
  const result = unsafehtmlentity({ source, context });
74
75
  if (result) {
75
76
  acc[acc.length - 1] += eval(result)[0];
76
- source = exec(result, source.slice(1));
77
+ source = exec(result) ?? source.slice(1);
77
78
  state ||= acc.at(-1)!.trimStart() !== '';
78
79
  continue;
79
80
  }
@@ -5,7 +5,7 @@ import { url } from './autolink/url';
5
5
  import { media } from './media';
6
6
  import { linebreak } from '../source';
7
7
 
8
- export const shortmedia: ShortMediaParser = constraint(State.media, false, rewrite(
8
+ export const shortmedia: ShortMediaParser = constraint(State.media, rewrite(
9
9
  open('!', url),
10
10
  convert(
11
11
  source => `!{ ${source.slice(1)} }`,
@@ -1,5 +1,5 @@
1
1
  import { MarkdownParser } from '../../markdown';
2
- import { union, verify, lazy } from '../combinator';
2
+ import { union, lazy } from '../combinator';
3
3
  import { annotation } from './inline/annotation';
4
4
  import { reference } from './inline/reference';
5
5
  import { template } from './inline/template';
@@ -47,12 +47,10 @@ export import ShortMediaParser = InlineParser.ShortMediaParser;
47
47
  export import BracketParser = InlineParser.BracketParser;
48
48
  export import AutolinkParser = InlineParser.AutolinkParser;
49
49
 
50
- export const inline: InlineParser = lazy(() => verify(union([
50
+ export const inline: InlineParser = lazy(() => union([
51
51
  input => {
52
- const { source, context } = input;
52
+ const { source } = input;
53
53
  if (source === '') return;
54
- context.depth ??= 0;
55
- ++context.depth;
56
54
  switch (source.slice(0, 2)) {
57
55
  case '((':
58
56
  return annotation(input);
@@ -106,20 +104,7 @@ export const inline: InlineParser = lazy(() => verify(union([
106
104
  bracket,
107
105
  autolink,
108
106
  text
109
- ]), (_, rest, context) => {
110
- --context.depth!;
111
- assert([rest]);
112
- // ヒープを効率的に削除可能な場合は削除する。
113
- // ヒープサイズは括弧類など特定の構文が完成しなかった場合にしか増加しないため
114
- // ブロックごとに平均数ノード以下となることから削除せずとも平均的にはあまり影響はない。
115
- //if (context.depth === 0) {
116
- // const { backtracks } = context;
117
- // while (backtracks.peek()?.key! > rest.length) {
118
- // backtracks.extract();
119
- // }
120
- //}
121
- return true;
122
- })) as any;
107
+ ])) as any;
123
108
 
124
109
  export { indexee } from './inline/extension/indexee';
125
110
  export { indexer } from './inline/extension/indexer';
@@ -1,15 +1,16 @@
1
1
  import { segment } from './segment';
2
+ import { Command } from './context';
2
3
 
3
4
  describe('Unit: parser/segment', () => {
4
5
  describe('segment', () => {
5
6
  it('huge input', () => {
6
7
  const result = segment(`${'\n'.repeat(10 * 1000 ** 2)}`).next().value?.split('\n', 1)[0];
7
- assert(result?.startsWith('\x07Too large input'));
8
+ assert(result?.startsWith(`${Command.Error}Too large input`));
8
9
  });
9
10
 
10
11
  it('huge segment', () => {
11
12
  const result = segment(`${'\n'.repeat(1000 ** 2 - 1)}`).next().value?.split('\n', 1)[0];
12
- assert(result?.startsWith('\x07Too large segment'));
13
+ assert(result?.startsWith(`${Command.Error}Too large segment`));
13
14
  });
14
15
 
15
16
  it('basic', () => {
@@ -18,8 +18,8 @@ const parser: SegmentParser = union([
18
18
  codeblock,
19
19
  mathblock,
20
20
  extension,
21
- some(contentline, MAX_SEGMENT_SIZE * 2),
22
- some(emptyline, MAX_SEGMENT_SIZE * 2),
21
+ some(contentline, MAX_SEGMENT_SIZE + 1),
22
+ some(emptyline, MAX_SEGMENT_SIZE + 1),
23
23
  ]);
24
24
 
25
25
  export function* segment(source: string): Generator<string, undefined, undefined> {
@@ -34,7 +34,7 @@ export const escsource: EscapableSourceParser = ({ source, context }) => {
34
34
  return [[source.slice(0, 2)], source.slice(2)];
35
35
  }
36
36
  case '\n':
37
- context.linebreak ??= source.length;
37
+ context.linebreak ||= source.length;
38
38
  return [[html('br')], source.slice(1)];
39
39
  default:
40
40
  assert(source[0] !== '\n');
@@ -4,7 +4,7 @@ import { union, consume, focus } from '../../combinator';
4
4
  import { str } from './str';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
- export const delimiter = /[\s\x00-\x7F()[]{}“”‘’「」『』]|\S#|[0-9A-Za-z]>/u;
7
+ export const delimiter = /[\s\x00-\x7F()[]{}]|\S#|[0-9A-Za-z]>/u;
8
8
  export const nonWhitespace = /[\S\n]|$/u;
9
9
  export const nonAlphanumeric = /[^0-9A-Za-z]|\S#|[0-9A-Za-z]>|$/u;
10
10
  const repeat = str(/^(.)\1*/);
@@ -36,7 +36,7 @@ export const text: TextParser = ({ source, context }) => {
36
36
  return [[source.slice(1, 2)], source.slice(2)];
37
37
  }
38
38
  case '\n':
39
- context.linebreak ??= source.length;
39
+ context.linebreak ||= source.length;
40
40
  return [[html('br')], source.slice(1)];
41
41
  case '*':
42
42
  case '`':
@@ -22,7 +22,7 @@ export const unescsource: UnescapableSourceParser = ({ source, context }) => {
22
22
  consume(1, context);
23
23
  return [[source.slice(1, 2)], source.slice(2)];
24
24
  case '\n':
25
- context.linebreak ??= source.length;
25
+ context.linebreak ||= source.length;
26
26
  return [[html('br')], source.slice(1)];
27
27
  default:
28
28
  assert(source[0] !== '\n');
@@ -4,12 +4,22 @@ import { Parser, Result, Ctx, Node, Context, eval, exec } from '../combinator/da
4
4
  import { convert } from '../combinator';
5
5
  import { define } from 'typed-dom/dom';
6
6
 
7
- export function lineable<P extends Parser<HTMLElement | string>>(parser: P, fillTrailingLinebreak?: boolean): P;
8
- export function lineable<N extends HTMLElement | string>(parser: Parser<N>, fillTrailingLinebreak = false): Parser<N> {
7
+ export function lineable<P extends Parser<HTMLElement | string>>(parser: P, trim?: 0 | 1 | -1): P;
8
+ export function lineable<N extends HTMLElement | string>(parser: Parser<N>, trim = -1): Parser<N> {
9
9
  return convert(
10
- source => `\r${source}${fillTrailingLinebreak && source.at(-1) !== '\n' ? '\n' : ''}`,
10
+ source => `\r${
11
+ trim === 0
12
+ ? source
13
+ : trim > 0
14
+ ? source.at(-1) === '\n'
15
+ ? source
16
+ : source + '\n'
17
+ : source.at(-1) === '\n'
18
+ ? source.slice(0, -1)
19
+ : source
20
+ }`,
11
21
  parser,
12
- !fillTrailingLinebreak);
22
+ trim === 0);
13
23
  }
14
24
 
15
25
  export function repeat<P extends Parser<HTMLElement | string>>(symbol: string, parser: P, cons: (nodes: Node<P>[], context: Context<P>) => Node<P>[], termination?: (acc: Node<P>[][], rest: string, prefix: number, postfix: number, state: boolean) => Result<string | Node<P>>): P;
@@ -51,6 +51,7 @@ export function blankWith(starts: '' | '\n', delimiter?: string | RegExp): RegEx
51
51
  return new RegExp(String.raw
52
52
  `^(?:(?=${starts})(?:\\?\s|&(?:${invisibleHTMLEntityNames.join('|')});|<wbr[^\S\n]*>)${
53
53
  // 空行除去
54
+ // 完全な空行はエスケープ済みなので再帰的バックトラックにはならない。
54
55
  starts && '+'
55
56
  })?${
56
57
  typeof delimiter === 'string'