securemark 0.295.8 → 0.296.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 (40) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/design.md +1 -1
  3. package/dist/index.js +268 -282
  4. package/package.json +1 -1
  5. package/src/combinator/control/constraint/contract.ts +5 -21
  6. package/src/combinator/data/parser/context.ts +3 -1
  7. package/src/combinator/data/parser/sequence.ts +3 -3
  8. package/src/combinator/data/parser.ts +7 -2
  9. package/src/parser/api/normalize.ts +10 -7
  10. package/src/parser/block/extension/figbase.test.ts +3 -0
  11. package/src/parser/block/extension/figbase.ts +1 -1
  12. package/src/parser/block/paragraph.test.ts +2 -2
  13. package/src/parser/block/reply/cite.ts +2 -1
  14. package/src/parser/block/reply/quote.ts +2 -1
  15. package/src/parser/block/reply.ts +2 -1
  16. package/src/parser/inline/annotation.test.ts +4 -5
  17. package/src/parser/inline/annotation.ts +2 -2
  18. package/src/parser/inline/emphasis.ts +3 -3
  19. package/src/parser/inline/emstrong.ts +4 -4
  20. package/src/parser/inline/extension/index.ts +2 -2
  21. package/src/parser/inline/extension/indexer.ts +3 -3
  22. package/src/parser/inline/extension/placeholder.ts +2 -2
  23. package/src/parser/inline/html.ts +2 -1
  24. package/src/parser/inline/htmlentity.test.ts +1 -1
  25. package/src/parser/inline/htmlentity.ts +10 -5
  26. package/src/parser/inline/italic.ts +2 -2
  27. package/src/parser/inline/link.test.ts +5 -4
  28. package/src/parser/inline/link.ts +6 -4
  29. package/src/parser/inline/mark.ts +2 -2
  30. package/src/parser/inline/media.test.ts +5 -2
  31. package/src/parser/inline/media.ts +9 -4
  32. package/src/parser/inline/reference.test.ts +5 -6
  33. package/src/parser/inline/reference.ts +2 -2
  34. package/src/parser/inline/remark.ts +1 -1
  35. package/src/parser/inline/strong.ts +3 -3
  36. package/src/parser/node.ts +17 -0
  37. package/src/parser/source/str.ts +9 -7
  38. package/src/parser/source/text.ts +4 -4
  39. package/src/parser/visibility.ts +53 -75
  40. /package/src/combinator/data/{data.ts → node.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.295.8",
3
+ "version": "0.296.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",
@@ -1,5 +1,5 @@
1
- import { Parser, Input, List, Node, Context, failsafe } from '../../data/parser';
2
- import { matcher } from '../../../combinator';
1
+ import { Parser, Input, List, Node, Context } from '../../data/parser';
2
+ import { matcher, bind } from '../../../combinator';
3
3
 
4
4
  //export function contract<P extends Parser>(patterns: string | RegExp | (string | RegExp)[], parser: P, cond: (nodes: readonly Data<P>[], rest: string) => boolean): P;
5
5
  //export function contract<N>(patterns: string | RegExp | (string | RegExp)[], parser: Parser<N>, cond: (nodes: readonly N[], rest: string) => boolean): Parser<N> {
@@ -11,32 +11,16 @@ export function validate<P extends Parser>(cond: ((input: Input<Parser.Context<P
11
11
  export function validate<N>(pattern: string | RegExp | ((input: Input<Context>) => boolean), parser: Parser<N>): Parser<N> {
12
12
  if (typeof pattern === 'function') return guard(pattern, parser);
13
13
  const match = matcher(pattern, false);
14
- return input => {
15
- const { context } = input;
16
- const { source, position } = context;
17
- if (position === source.length) return;
18
- if (!match(input)) return;
19
- return parser(input);
20
- };
14
+ return input => match(input) ? parser(input) : undefined;
21
15
  }
22
16
 
23
17
  function guard<P extends Parser>(f: (input: Input<Parser.Context<P>>) => boolean, parser: P): P;
24
18
  function guard<N>(f: (input: Input<Context>) => boolean, parser: Parser<N>): Parser<N> {
25
- return input =>
26
- f(input)
27
- ? parser(input)
28
- : undefined;
19
+ return input => f(input) ? parser(input) : undefined;
29
20
  }
30
21
 
31
22
  export function verify<P extends Parser>(parser: P, cond: (nodes: List<Node<Parser.Node<P>>>, context: Parser.Context<P>) => boolean): P;
32
23
  export function verify<N>(parser: Parser<N>, cond: (nodes: List<Node<N>>, context: Context) => boolean): Parser<N> {
33
24
  assert(parser);
34
- return failsafe(input => {
35
- const { context } = input;
36
- const { source, position } = context;
37
- if (position === source.length) return;
38
- const result = parser(input);
39
- if (result && !cond(result, context)) return;
40
- return result;
41
- });
25
+ return bind(parser, (nodes, context) => cond(nodes, context) ? nodes : undefined)
42
26
  }
@@ -159,7 +159,7 @@ export function constraint<N>(state: number, positive: boolean | Parser<N>, pars
159
159
  };
160
160
  }
161
161
 
162
- export function matcher(pattern: string | RegExp, advance: boolean): Parser<string> {
162
+ export function matcher(pattern: string | RegExp, advance: boolean, verify?: (source: string, position: number, range: number) => boolean): Parser<string> {
163
163
  assert(pattern instanceof RegExp ? !pattern.flags.match(/[gm]/) && pattern.sticky && !pattern.source.startsWith('^') : true);
164
164
  const count = typeof pattern === 'object'
165
165
  ? /[^^\\*+][*+]/.test(pattern.source)
@@ -169,6 +169,7 @@ export function matcher(pattern: string | RegExp, advance: boolean): Parser<stri
169
169
  return ({ context }) => {
170
170
  const { source, position } = context;
171
171
  if (!source.startsWith(pattern, position)) return;
172
+ if (verify?.(source, position, pattern.length) === false) return;
172
173
  if (advance) {
173
174
  context.position += pattern.length;
174
175
  }
@@ -182,6 +183,7 @@ export function matcher(pattern: string | RegExp, advance: boolean): Parser<stri
182
183
  if (!pattern.test(source)) return;
183
184
  const src = source.slice(position, pattern.lastIndex);
184
185
  count && consume(src.length, context);
186
+ if (verify?.(source, position, src.length) === false) return;
185
187
  if (advance) {
186
188
  context.position += src.length;
187
189
  }
@@ -1,10 +1,10 @@
1
- import { Parser, List, Node, Context } from '../parser';
1
+ import { Parser, List, Node, Context, failsafe } from '../parser';
2
2
 
3
3
  export function sequence<P extends Parser>(parsers: Parser.SubParsers<P>): Parser.SubNode<P> extends Parser.Node<P> ? P : Parser<Parser.SubNode<P>, Parser.Context<P>, Parser.SubParsers<P>>;
4
4
  export function sequence<N, D extends Parser<N>[]>(parsers: D): Parser<N, Context, D> {
5
5
  assert(parsers.every(f => f));
6
6
  if (parsers.length === 1) return parsers[0];
7
- return input => {
7
+ return failsafe(input => {
8
8
  const { context } = input;
9
9
  const { source } = context;
10
10
  let nodes: List<Node<N>> | undefined;
@@ -16,5 +16,5 @@ export function sequence<N, D extends Parser<N>[]>(parsers: D): Parser<N, Contex
16
16
  nodes = nodes?.import(result) ?? result;
17
17
  }
18
18
  return nodes;
19
- };
19
+ });
20
20
  }
@@ -1,4 +1,4 @@
1
- import { List } from './data';
1
+ import { List } from './node';
2
2
  import { Delimiters } from './delimiter';
3
3
 
4
4
  export type Parser<N = unknown, C extends Context = Context, D extends Parser<unknown, C>[] = any>
@@ -20,7 +20,10 @@ export type Result<N, C extends Context = Context, D extends Parser<unknown, C>[
20
20
  | undefined;
21
21
  export { List };
22
22
  export class Node<N> implements List.Node {
23
- constructor(public value: N) {
23
+ constructor(
24
+ public value: N,
25
+ public flags: number = 0,
26
+ ) {
24
27
  }
25
28
  public next?: this = undefined;
26
29
  public prev?: this = undefined;
@@ -66,6 +69,8 @@ export class Context {
66
69
  // Objectの内部実装を利用する。
67
70
  // 探索木を直接使用する場合は探索速度が重要で挿入は相対的に少なく削除は不要かつ不確実であるため
68
71
  // AVL木が適当と思われる。
72
+ // メモリの局所性を得るために木ごとに最初の数十から数百byte分のノードをプールしノードが不足した場合は
73
+ // 使い捨てノードを追加またはテーブルに移行するとよいだろう。
69
74
  // 最大セグメントサイズ10KB内で探索コストが平均実行性能を圧迫するほど大きくなるとは考えにくいが
70
75
  // 探索コストを減らすにはバックトラック位置数が規定数を超えた場合一定区間ごとに探索木を分割する方法が考えられる。
71
76
  // 10KBの入力すべてを保持する探索木を1024文字ごとに分割するために必要なテーブルサイズは64bit*98=784byteとなる。
@@ -1,6 +1,4 @@
1
- import { input } from '../../combinator/data/parser';
2
- import { Context } from '../context';
3
- import { unsafehtmlentity } from '../inline/htmlentity';
1
+ import { html } from 'typed-dom/dom';
4
2
 
5
3
  const UNICODE_REPLACEMENT_CHARACTER = '\uFFFD';
6
4
  assert(UNICODE_REPLACEMENT_CHARACTER.trim());
@@ -15,7 +13,7 @@ function format(source: string): string {
15
13
 
16
14
  const invalid = new RegExp([
17
15
  /(?![\t\r\n])[\x00-\x1F\x7F]/g.source,
18
- /(?!\u200D)[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]/g.source,
16
+ /(?![\u200C\u200D])[\u2006\u200B-\u200F\u202A-\u202F\u2060\uFEFF]/g.source,
19
17
  // 後読みが重い
20
18
  ///(?<![\u1820\u1821])\u180E/g.source,
21
19
  ///[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g.source,
@@ -59,7 +57,12 @@ export const invisibleHTMLEntityNames = [
59
57
  'it',
60
58
  'InvisibleComma',
61
59
  'ic',
62
- ] as const;
60
+ ] as readonly string[];
61
+ const parser = (el => (entity: string): string => {
62
+ if (entity === '&NewLine;') return entity;
63
+ el.innerHTML = entity;
64
+ return el.textContent!;
65
+ })(html('span'));
63
66
  const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => ![
64
67
  'Tab',
65
68
  'NewLine',
@@ -69,7 +72,7 @@ const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name =>
69
72
  'zwnj',
70
73
  ].includes(name));
71
74
  const unreadableEscapeCharacters = unreadableEscapeHTMLEntityNames
72
- .map(name => unsafehtmlentity(input(`&${name};`, new Context()))!.head!.value);
75
+ .map(name => parser(`&${name};`));
73
76
  assert(unreadableEscapeCharacters.length === unreadableEscapeHTMLEntityNames.length);
74
77
  assert(unreadableEscapeCharacters.every(c => c.length === 1));
75
78
  const unreadableEscapeCharacter = new RegExp(`[${unreadableEscapeCharacters.join('')}]`, 'g');
@@ -84,7 +87,7 @@ const unreadableSpecialCharacters = [
84
87
  // ZERO WIDTH SPACE
85
88
  '\u200B',
86
89
  // ZERO WIDTH NON-JOINER
87
- '\u200C',
90
+ //'\u200C',
88
91
  // ZERO WIDTH JOINER
89
92
  //'\u200D',
90
93
  // LEFT-TO-RIGHT MARK
@@ -13,10 +13,13 @@ describe('Unit: parser/block/extension/figbase', () => {
13
13
  assert.deepStrictEqual(inspect(parser, input('$-0.', new Context())), undefined);
14
14
  assert.deepStrictEqual(inspect(parser, input('$-0.1', new Context())), undefined);
15
15
  assert.deepStrictEqual(inspect(parser, input('$-1', new Context())), undefined);
16
+ assert.deepStrictEqual(inspect(parser, input('$-0 0', new Context())), undefined);
16
17
  assert.deepStrictEqual(inspect(parser, input('$-0\n 0', new Context())), undefined);
17
18
  assert.deepStrictEqual(inspect(parser, input('$-name', new Context())), undefined);
18
19
  assert.deepStrictEqual(inspect(parser, input('$-name-0', new Context())), undefined);
19
20
  assert.deepStrictEqual(inspect(parser, input('$group-0', new Context())), undefined);
21
+ assert.deepStrictEqual(inspect(parser, input('$-0]', new Context())), undefined);
22
+ assert.deepStrictEqual(inspect(parser, input('[$-0', new Context())), undefined);
20
23
  assert.deepStrictEqual(inspect(parser, input(' [$-0]', new Context())), undefined);
21
24
  });
22
25
 
@@ -5,7 +5,7 @@ import { label } from '../../inline/extension/label';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
7
  export const figbase: ExtensionParser.FigbaseParser = block(fmap(
8
- validate(/\[?\$-(?:[0-9]+\.)*0\]?(?:$|[ \n])/y,
8
+ validate(/\[?\$-(?:[0-9]+\.)*0\]?[^\S\n]*(?:$|\n)/y,
9
9
  line(union([label]))),
10
10
  ([{ value: el }]) => {
11
11
  const label = el.getAttribute('data-label')!;
@@ -22,8 +22,8 @@ describe('Unit: parser/block/paragraph', () => {
22
22
  assert.deepStrictEqual(inspect(parser, input('a\\ \n', new Context())), [['<p>a</p>'], '']);
23
23
  assert.deepStrictEqual(inspect(parser, input('a\\\n', new Context())), [['<p>a</p>'], '']);
24
24
  assert.deepStrictEqual(inspect(parser, input('a\\\nb', new Context())), [['<p>a<br>b</p>'], '']);
25
- assert.deepStrictEqual(inspect(parser, input('a&NewLine;b', new Context())), [['<p>a b</p>'], '']);
26
- assert.deepStrictEqual(inspect(parser, input('&Tab;&NewLine;', new Context())), [['<p>&amp;Tab;</p>'], '']);
25
+ assert.deepStrictEqual(inspect(parser, input('a&NewLine;b', new Context())), [['<p>a<span class="invalid">&amp;NewLine;</span>b</p>'], '']);
26
+ assert.deepStrictEqual(inspect(parser, input('&Tab;&nbsp;', new Context())), [['<p>&amp;Tab;</p>'], '']);
27
27
  assert.deepStrictEqual(inspect(parser, input('<wbr>', new Context())), [['<p>&lt;wbr&gt;</p>'], '']);
28
28
  assert.deepStrictEqual(inspect(parser, input('<wbr>\n', new Context())), [['<p>&lt;wbr&gt;</p>'], '']);
29
29
  assert.deepStrictEqual(inspect(parser, input('<wbr>\na', new Context())), [['<p>&lt;wbr&gt;<br>a</p>'], '']);
@@ -1,5 +1,6 @@
1
1
  import { ReplyParser } from '../../block';
2
2
  import { List, Node } from '../../../combinator/data/parser';
3
+ import { Flag } from '../../node';
3
4
  import { union, line, focus, open, fmap } from '../../../combinator';
4
5
  import { anchor } from '../../inline/autolink/anchor';
5
6
  import { str } from '../../source';
@@ -33,6 +34,6 @@ export const cite: ReplyParser.CiteParser = line(fmap(
33
34
  ? define(node, { 'data-depth': `${quotes.length + 1}` }, node.innerText.slice(1))
34
35
  : node.slice(1),
35
36
  ]))),
36
- new Node(html('br')),
37
+ new Node(html('br'), Flag.invisible),
37
38
  ]);
38
39
  }));
@@ -1,5 +1,6 @@
1
1
  import { ReplyParser } from '../../block';
2
2
  import { List, Node } from '../../../combinator/data/parser';
3
+ import { Flag } from '../../node';
3
4
  import { union, some, block, validate, rewrite, convert, lazy, fmap } from '../../../combinator';
4
5
  import { math } from '../../inline/math';
5
6
  import { autolink } from '../../inline/autolink';
@@ -21,7 +22,7 @@ export const quote: ReplyParser.QuoteParser = lazy(() => block(fmap(
21
22
  unescsource,
22
23
  ])))),
23
24
  (ns, { source, position }) => new List([
24
- new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br')),
25
+ new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br'), Flag.invisible),
25
26
  new Node(html('span', { class: 'quote' }, defrag(unwrap(ns)))),
26
27
  ].reverse())),
27
28
  false));
@@ -1,5 +1,6 @@
1
1
  import { ReplyParser } from '../block';
2
2
  import { List, Node } from '../../combinator/data/parser';
3
+ import { Flag } from '../node';
3
4
  import { union, some, block, validate, rewrite, fmap } from '../../combinator';
4
5
  import { cite, syntax as csyntax } from './reply/cite';
5
6
  import { quote, syntax as qsyntax } from './reply/quote';
@@ -20,6 +21,6 @@ export const reply: ReplyParser = block(validate(csyntax, fmap(
20
21
  visualize(fmap(some(inline), (ns, { source, position }) =>
21
22
  source[position - 1] === '\n'
22
23
  ? ns
23
- : ns.push(new Node(html('br'))) && ns)))
24
+ : ns.push(new Node(html('br'), Flag.invisible)) && ns)))
24
25
  ])),
25
26
  ns => new List([new Node(html('p', defrag(unwrap(trimBlankNodeEnd(ns)))))]))));
@@ -19,7 +19,10 @@ describe('Unit: parser/inline/annotation', () => {
19
19
  assert.deepStrictEqual(inspect(parser, input('(([))', new Context())), undefined);
20
20
  assert.deepStrictEqual(inspect(parser, input('(([%))', new Context())), undefined);
21
21
  assert.deepStrictEqual(inspect(parser, input('(( ))', new Context())), undefined);
22
- assert.deepStrictEqual(inspect(parser, input('(( (a', new Context())), undefined);
22
+ assert.deepStrictEqual(inspect(parser, input('(( a))', new Context())), undefined);
23
+ assert.deepStrictEqual(inspect(parser, input('(( a ))', new Context())), undefined);
24
+ assert.deepStrictEqual(inspect(parser, input('((\\ a))', new Context())), undefined);
25
+ assert.deepStrictEqual(inspect(parser, input('((<wbr>a))', new Context())), undefined);
23
26
  assert.deepStrictEqual(inspect(parser, input('((\n))', new Context())), undefined);
24
27
  assert.deepStrictEqual(inspect(parser, input('((\na))', new Context())), undefined);
25
28
  assert.deepStrictEqual(inspect(parser, input('((\\\na))', new Context())), undefined);
@@ -35,10 +38,6 @@ describe('Unit: parser/inline/annotation', () => {
35
38
  });
36
39
 
37
40
  it('basic', () => {
38
- assert.deepStrictEqual(inspect(parser, input('(( a))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
39
- assert.deepStrictEqual(inspect(parser, input('(( a ))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
40
- assert.deepStrictEqual(inspect(parser, input('((\\ a))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
41
- assert.deepStrictEqual(inspect(parser, input('((<wbr>a))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
42
41
  assert.deepStrictEqual(inspect(parser, input('((a))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
43
42
  assert.deepStrictEqual(inspect(parser, input('((a ))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
44
43
  assert.deepStrictEqual(inspect(parser, input('((a ))', new Context())), [['<sup class="annotation"><span>a</span></sup>'], '']);
@@ -3,14 +3,14 @@ import { State, Backtrack } from '../context';
3
3
  import { List, Node } from '../../combinator/data/parser';
4
4
  import { union, some, precedence, state, constraint, surround, setBacktrack, lazy } from '../../combinator';
5
5
  import { inline } from '../inline';
6
- import { trimBlankStart, trimBlankNodeEnd } from '../visibility';
6
+ import { beforeNonblank, trimBlankNodeEnd } from '../visibility';
7
7
  import { unwrap } from '../util';
8
8
  import { html, defrag } from 'typed-dom/dom';
9
9
 
10
10
  export const annotation: AnnotationParser = lazy(() => constraint(State.annotation, surround(
11
11
  '((',
12
12
  precedence(1, state(State.annotation,
13
- trimBlankStart(some(union([inline]), ')', [[')', 1]])))),
13
+ beforeNonblank(some(union([inline]), ')', [[')', 1]])))),
14
14
  '))',
15
15
  false,
16
16
  [2, 1 | Backtrack.common, 3 | Backtrack.doublebracket],
@@ -5,14 +5,14 @@ import { union, some, recursion, precedence, surround, lazy } from '../../combin
5
5
  import { inline } from '../inline';
6
6
  import { strong } from './strong';
7
7
  import { str } from '../source';
8
- import { tightStart, afterNonblank } from '../visibility';
8
+ import { beforeNonblank, afterNonblank } from '../visibility';
9
9
  import { unwrap } from '../util';
10
10
  import { html, defrag } from 'typed-dom/dom';
11
11
 
12
12
  export const emphasis: EmphasisParser = lazy(() => surround(
13
- str(/\*(?!\*)/y),
13
+ str('*', (source, position, range) => !source.startsWith('*', position + range)),
14
14
  precedence(0, recursion(Recursion.inline,
15
- tightStart(some(union([
15
+ beforeNonblank(some(union([
16
16
  some(inline, '*', afterNonblank),
17
17
  strong,
18
18
  ]))))),
@@ -5,8 +5,8 @@ import { union, some, recursion, precedence, surround, lazy, bind } from '../../
5
5
  import { inline } from '../inline';
6
6
  import { strong } from './strong';
7
7
  import { emphasis } from './emphasis';
8
- import { str } from '../source';
9
- import { tightStart, afterNonblank } from '../visibility';
8
+ import { strs } from '../source';
9
+ import { beforeNonblank, afterNonblank } from '../visibility';
10
10
  import { unwrap, repeat } from '../util';
11
11
  import { html, defrag } from 'typed-dom/dom';
12
12
 
@@ -25,8 +25,8 @@ const subemphasis: Parser.IntermediateParser<EmphasisParser> = lazy(() => some(u
25
25
  export const emstrong: EmStrongParser = lazy(() =>
26
26
  precedence(0, recursion(Recursion.inline, repeat('***', surround(
27
27
  '',
28
- tightStart(some(union([some(inline, '*', afterNonblank)]))),
29
- str(/\*{1,3}/y),
28
+ beforeNonblank(some(union([some(inline, '*', afterNonblank)]))),
29
+ strs('*', 3),
30
30
  false, [],
31
31
  ([, bs, cs], context): Result<Parser.Node<EmStrongParser>, Parser.Context<EmStrongParser>> => {
32
32
  assert(cs.length === 1);
@@ -6,7 +6,7 @@ import { inline } from '../../inline';
6
6
  import { indexee, identity } from './indexee';
7
7
  import { unsafehtmlentity } from '../htmlentity';
8
8
  import { txt, str } from '../../source';
9
- import { tightStart, trimBlankNodeEnd } from '../../visibility';
9
+ import { beforeNonblank, trimBlankNodeEnd } from '../../visibility';
10
10
  import { unwrap } from '../../util';
11
11
  import { html, define, defrag } from 'typed-dom/dom';
12
12
 
@@ -15,7 +15,7 @@ import IndexParser = ExtensionParser.IndexParser;
15
15
  export const index: IndexParser = lazy(() => constraint(State.index, fmap(indexee(surround(
16
16
  str('[#'),
17
17
  precedence(1, state(State.linkers,
18
- tightStart(
18
+ beforeNonblank(
19
19
  some(inits([
20
20
  inline,
21
21
  signature,
@@ -1,6 +1,6 @@
1
1
  import { ExtensionParser } from '../../inline';
2
2
  import { List, Node } from '../../../combinator/data/parser';
3
- import { union, focus, surround } from '../../../combinator';
3
+ import { union, validate, focus, surround } from '../../../combinator';
4
4
  import { signature } from './index';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
@@ -10,10 +10,10 @@ import { html } from 'typed-dom/dom';
10
10
  // テキストまたはインデクスを付けて同期が必要な機会を減らすのが
11
11
  // 継続的編集において最も簡便となる。
12
12
 
13
- export const indexer: ExtensionParser.IndexerParser = surround(
13
+ export const indexer: ExtensionParser.IndexerParser = validate(' [|', surround(
14
14
  / \[(?=\|\S)/y,
15
15
  union([
16
16
  signature,
17
17
  focus(/\|(?=\])/y, () => new List([new Node(html('span', { class: 'indexer', 'data-index': '' }))])),
18
18
  ]),
19
- /\][^\S\n]*(?:$|\n)/y);
19
+ /\][^\S\n]*(?:$|\n)/y));
@@ -4,7 +4,7 @@ import { List, Node } from '../../../combinator/data/parser';
4
4
  import { union, some, recursion, precedence, surround, lazy } from '../../../combinator';
5
5
  import { inline } from '../../inline';
6
6
  import { str } from '../../source';
7
- import { tightStart } from '../../visibility';
7
+ import { beforeNonblank } from '../../visibility';
8
8
  import { invalid } from '../../util';
9
9
  import { html } from 'typed-dom/dom';
10
10
 
@@ -16,7 +16,7 @@ export const placeholder: ExtensionParser.PlaceholderParser = lazy(() => surroun
16
16
  // ^はabbrで使用済みだが^:などのようにして分離使用可能
17
17
  str(/\[[:^|]/y),
18
18
  precedence(1, recursion(Recursion.inline,
19
- tightStart(some(union([inline]), ']', [[']', 1]])))),
19
+ beforeNonblank(some(union([inline]), ']', [[']', 1]])))),
20
20
  str(']'),
21
21
  false,
22
22
  [3 | Backtrack.common],
@@ -1,6 +1,7 @@
1
1
  import { HTMLParser } from '../inline';
2
2
  import { Recursion } from '../context';
3
3
  import { List, Node, Context } from '../../combinator/data/parser';
4
+ import { Flag } from '../node';
4
5
  import { union, some, recursion, precedence, validate, surround, open, match, lazy } from '../../combinator';
5
6
  import { inline } from '../inline';
6
7
  import { str } from '../source';
@@ -28,7 +29,7 @@ export const html: HTMLParser = lazy(() => validate(/<[a-z]+(?=[ >])/yi,
28
29
  open(str(/ ?/y), str('>'), true),
29
30
  true, [],
30
31
  ([as, bs = new List(), cs], context) =>
31
- new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), context))]),
32
+ new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs).import(cs))], new List(), new List(), context), as.head!.value === '<wbr' ? Flag.invisible : Flag.none)]),
32
33
  ([as, bs = new List()], context) =>
33
34
  new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs))], new List(), new List(), context))])),
34
35
  match(
@@ -33,11 +33,11 @@ describe('Unit: parser/inline/htmlentity', () => {
33
33
  assert.deepStrictEqual(inspect(parser, input('&#X22;', new Context())), [['&'], '#X22;']);
34
34
  assert.deepStrictEqual(inspect(parser, input('&#XD06;', new Context())), [['&'], '#XD06;']);
35
35
  assert.deepStrictEqual(inspect(parser, input('&#xcab;', new Context())), [['&'], '#xcab;']);
36
+ assert.deepStrictEqual(inspect(parser, input('&NewLine;', new Context())), [['<span class="invalid">&amp;NewLine;</span>'], '']);
36
37
  assert.deepStrictEqual(inspect(parser, input(' &amp;', new Context())), undefined);
37
38
  });
38
39
 
39
40
  it('entity', () => {
40
- assert.deepStrictEqual(inspect(parser, input('&NewLine;', new Context())), [[' '], '']);
41
41
  assert.deepStrictEqual(inspect(parser, input('&nbsp;', new Context())), [['\u00A0'], '']);
42
42
  assert.deepStrictEqual(inspect(parser, input('&amp;', new Context())), [['&'], '']);
43
43
  assert.deepStrictEqual(inspect(parser, input('&copy;', new Context())), [['©'], '']);
@@ -1,6 +1,7 @@
1
1
  import { HTMLEntityParser, UnsafeHTMLEntityParser } from '../inline';
2
2
  import { Backtrack } from '../context';
3
3
  import { List, Node } from '../../combinator/data/parser';
4
+ import { Flag, isInvisibleHTMLEntityName } from '../node';
4
5
  import { union, surround, fmap } from '../../combinator';
5
6
  import { str } from '../source';
6
7
  import { invalid } from '../util';
@@ -11,15 +12,19 @@ export const unsafehtmlentity: UnsafeHTMLEntityParser = surround(
11
12
  false,
12
13
  [3 | Backtrack.unescapable],
13
14
  ([as, bs, cs]) =>
14
- new List([new Node(parser(as.head!.value + bs.head!.value + cs.head!.value))]),
15
+ new List([
16
+ new Node(
17
+ parser(as.head!.value + bs.head!.value + cs.head!.value),
18
+ isInvisibleHTMLEntityName(bs.head!.value) ? Flag.invisible : Flag.none)
19
+ ]),
15
20
  ([as, bs]) =>
16
21
  new List([new Node(as.head!.value + (bs?.head?.value ?? ''))]));
17
22
 
18
23
  export const htmlentity: HTMLEntityParser = fmap(
19
24
  union([unsafehtmlentity]),
20
- ([{ value }]) => new List([
21
- length === 1 || value.at(-1) !== ';'
22
- ? new Node(value)
25
+ ([{ value, flags }]) => new List([
26
+ value.length === 1 || value.at(-1) !== ';'
27
+ ? new Node(value, flags)
23
28
  : new Node(html('span', {
24
29
  class: 'invalid',
25
30
  ...invalid('htmlentity', 'syntax', 'Invalid HTML entity'),
@@ -27,7 +32,7 @@ export const htmlentity: HTMLEntityParser = fmap(
27
32
  ]));
28
33
 
29
34
  const parser = (el => (entity: string): string => {
30
- if (entity === '&NewLine;') return ' ';
35
+ if (entity === '&NewLine;') return entity;
31
36
  el.innerHTML = entity;
32
37
  return el.textContent!;
33
38
  })(html('span'));
@@ -3,7 +3,7 @@ import { Recursion, Command } from '../context';
3
3
  import { List, Node } from '../../combinator/data/parser';
4
4
  import { union, some, recursion, precedence, surround, lazy } from '../../combinator';
5
5
  import { inline } from '../inline';
6
- import { tightStart, afterNonblank } from '../visibility';
6
+ import { beforeNonblank, afterNonblank } from '../visibility';
7
7
  import { unwrap, repeat } from '../util';
8
8
  import { html, defrag } from 'typed-dom/dom';
9
9
 
@@ -13,7 +13,7 @@ import { html, defrag } from 'typed-dom/dom';
13
13
  export const italic: ItalicParser = lazy(() =>
14
14
  precedence(0, recursion(Recursion.inline, repeat('///', surround(
15
15
  '',
16
- tightStart(some(union([inline]), '///', afterNonblank)),
16
+ beforeNonblank(some(union([inline]), '///', afterNonblank)),
17
17
  '///',
18
18
  false, [],
19
19
  ([, bs], { buffer }) => buffer.import(bs),
@@ -76,10 +76,15 @@ describe('Unit: parser/inline/link', () => {
76
76
  assert.deepStrictEqual(inspect(parser, input('[ ]{ }', new Context())), undefined);
77
77
  assert.deepStrictEqual(inspect(parser, input('[ ]{b}', new Context())), undefined);
78
78
  assert.deepStrictEqual(inspect(parser, input('[ ]{b}', new Context())), undefined);
79
+ assert.deepStrictEqual(inspect(parser, input('[ a]{b}', new Context())), undefined);
80
+ assert.deepStrictEqual(inspect(parser, input('[ a ]{b}', new Context())), undefined);
81
+ assert.deepStrictEqual(inspect(parser, input('[\\ a]{b}', new Context())), undefined);
82
+ assert.deepStrictEqual(inspect(parser, input('[ \\ a]{b}', new Context())), undefined);
79
83
  assert.deepStrictEqual(inspect(parser, input('[\n]{b}', new Context())), undefined);
80
84
  assert.deepStrictEqual(inspect(parser, input('[\\ ]{b}', new Context())), undefined);
81
85
  assert.deepStrictEqual(inspect(parser, input('[\\\n]{b}', new Context())), undefined);
82
86
  assert.deepStrictEqual(inspect(parser, input('[&Tab;]{b}', new Context())), undefined);
87
+ assert.deepStrictEqual(inspect(parser, input('[&zwj;]{b}', new Context())), undefined);
83
88
  assert.deepStrictEqual(inspect(parser, input('[[]{b}', new Context())), undefined);
84
89
  assert.deepStrictEqual(inspect(parser, input('[]]{b}', new Context())), undefined);
85
90
  assert.deepStrictEqual(inspect(parser, input('[a]{}', new Context())), undefined);
@@ -131,10 +136,6 @@ describe('Unit: parser/inline/link', () => {
131
136
  assert.deepStrictEqual(inspect(parser, input('[]{^/b}', new Context({ host: new URL('/0.a0', location.origin) }))), [[`<a class="url" href="/b">^/b</a>`], '']);
132
137
  assert.deepStrictEqual(inspect(parser, input('[]{^/b}', new Context({ host: new URL('/0.0', location.origin) }))), [[`<a class="url" href="/0.0/b">^/b</a>`], '']);
133
138
  assert.deepStrictEqual(inspect(parser, input('[]{^/b}', new Context({ host: new URL('/0.0,0.0,0z', location.origin) }))), [[`<a class="url" href="/0.0,0.0,0z/b">^/b</a>`], '']);
134
- assert.deepStrictEqual(inspect(parser, input('[ a]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
135
- assert.deepStrictEqual(inspect(parser, input('[ a ]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
136
- assert.deepStrictEqual(inspect(parser, input('[\\ a]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
137
- assert.deepStrictEqual(inspect(parser, input('[ \\ a]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
138
139
  assert.deepStrictEqual(inspect(parser, input('[a ]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
139
140
  assert.deepStrictEqual(inspect(parser, input('[a ]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
140
141
  assert.deepStrictEqual(inspect(parser, input('[a]{b}', new Context())), [['<a class="link" href="b">a</a>'], '']);
@@ -5,7 +5,7 @@ import { union, inits, sequence, subsequence, some, consume, precedence, state,
5
5
  import { inline, media, shortmedia } from '../inline';
6
6
  import { attributes } from './html';
7
7
  import { str } from '../source';
8
- import { trimBlankStart, trimBlankNodeEnd } from '../visibility';
8
+ import { beforeNonblank, trimBlankNodeEnd } from '../visibility';
9
9
  import { unwrap, invalid, stringify } from '../util';
10
10
  import { ReadonlyURL } from 'spica/url';
11
11
  import { html, define, defrag } from 'typed-dom/dom';
@@ -20,7 +20,7 @@ export const textlink: LinkParser.TextLinkParser = lazy(() => bind(
20
20
  constraint(State.link, state(State.linkers, dup(surround(
21
21
  '[',
22
22
  precedence(1,
23
- trimBlankStart(some(union([inline]), ']', [[']', 1]]))),
23
+ beforeNonblank(some(union([inline]), ']', [[']', 1]]))),
24
24
  ']',
25
25
  true,
26
26
  [3 | Backtrack.common | Backtrack.link, 2 | Backtrack.ruby],
@@ -43,7 +43,9 @@ export const textlink: LinkParser.TextLinkParser = lazy(() => bind(
43
43
  bs && as.import(bs).push(new Node(Command.Cancel)) && as)),
44
44
  ]),
45
45
  ([{ value: content }, { value: params = undefined } = {}], context) => {
46
- if (context.state & State.link) return new List([new Node(context.source.slice(context.position - context.range, context.position))]);
46
+ if (context.state & State.link) return new List([
47
+ new Node(context.source.slice(context.position - context.range, context.position).replace(/\\($|.)/g, '$1'))
48
+ ]);
47
49
  if (content.last!.value === Command.Separator) {
48
50
  content.pop();
49
51
  if (params === undefined) {
@@ -88,7 +90,7 @@ export const medialink: LinkParser.MediaLinkParser = lazy(() => constraint(State
88
90
  new List([new Node(parse(content, params as List<Node<string>>, context))])))));
89
91
 
90
92
  export const uri: LinkParser.ParameterParser.UriParser = union([
91
- open(/ /y, str(/\S+/y)),
93
+ open(' ', str(/\S+/y)),
92
94
  str(/[^\s{}]+/y),
93
95
  ]);
94
96
 
@@ -4,14 +4,14 @@ import { List, Node } from '../../combinator/data/parser';
4
4
  import { union, some, recursion, precedence, state, surround, lazy } from '../../combinator';
5
5
  import { inline } from '../inline';
6
6
  import { identity, signature } from './extension/indexee';
7
- import { tightStart, afterNonblank } from '../visibility';
7
+ import { beforeNonblank, afterNonblank } from '../visibility';
8
8
  import { unwrap, repeat } from '../util';
9
9
  import { html, define, defrag } from 'typed-dom/dom';
10
10
 
11
11
  export const mark: MarkParser = lazy(() =>
12
12
  precedence(0, recursion(Recursion.inline, repeat('==', surround(
13
13
  '',
14
- tightStart(state(State.mark, some(union([inline]), '==', afterNonblank))),
14
+ beforeNonblank(state(State.mark, some(union([inline]), '==', afterNonblank))),
15
15
  '==',
16
16
  false, [],
17
17
  ([, bs], { buffer }) => buffer.import(bs),
@@ -41,10 +41,15 @@ describe('Unit: parser/inline/media', () => {
41
41
  assert.deepStrictEqual(inspect(parser, input('![ ]{}', new Context())), undefined);
42
42
  assert.deepStrictEqual(inspect(parser, input('![ ]{b}', new Context())), undefined);
43
43
  assert.deepStrictEqual(inspect(parser, input('![ ]{b}', new Context())), undefined);
44
+ assert.deepStrictEqual(inspect(parser, input('![ a]{b}', new Context())), undefined);
45
+ assert.deepStrictEqual(inspect(parser, input('![ a ]{b}', new Context())), undefined);
46
+ assert.deepStrictEqual(inspect(parser, input('![\\ a]{b}', new Context())), undefined);
47
+ assert.deepStrictEqual(inspect(parser, input('![ \\ a]{b}', new Context())), undefined);
44
48
  assert.deepStrictEqual(inspect(parser, input('![\n]{b}', new Context())), undefined);
45
49
  assert.deepStrictEqual(inspect(parser, input('![\\ ]{b}', new Context())), undefined);
46
50
  assert.deepStrictEqual(inspect(parser, input('![\\\n]{b}', new Context())), undefined);
47
51
  assert.deepStrictEqual(inspect(parser, input('![&Tab;]{b}', new Context())), undefined);
52
+ assert.deepStrictEqual(inspect(parser, input('![&zwj;]{b}', new Context())), undefined);
48
53
  assert.deepStrictEqual(inspect(parser, input('![&a;]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="&amp;a;"></a>'], '']);
49
54
  assert.deepStrictEqual(inspect(parser, input('![[]{b}', new Context())), undefined);
50
55
  assert.deepStrictEqual(inspect(parser, input('![]]{b}', new Context())), undefined);
@@ -79,8 +84,6 @@ describe('Unit: parser/inline/media', () => {
79
84
  assert.deepStrictEqual(inspect(parser, input('![]{?/../}', new Context())), [[`<a href="?/../" target="_blank"><img class="media" data-src="?/../" alt="?/../"></a>`], '']);
80
85
  assert.deepStrictEqual(inspect(parser, input('![]{#/../}', new Context())), [[`<a href="#/../" target="_blank"><img class="media" data-src="#/../" alt="#/../"></a>`], '']);
81
86
  assert.deepStrictEqual(inspect(parser, input('![]{^/b}', new Context())), [[`<a href="/b" target="_blank"><img class="media" data-src="/b" alt="^/b"></a>`], '']);
82
- assert.deepStrictEqual(inspect(parser, input('![ a]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
83
- assert.deepStrictEqual(inspect(parser, input('![ a ]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
84
87
  assert.deepStrictEqual(inspect(parser, input('![a ]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
85
88
  assert.deepStrictEqual(inspect(parser, input('![a ]{b}', new Context())), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
86
89
  assert.deepStrictEqual(inspect(parser, input('![a b]{c}', new Context())), [['<a href="c" target="_blank"><img class="media" data-src="c" alt="a b"></a>'], '']);