securemark 0.296.0 → 0.296.2

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 (44) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/dist/index.js +196 -187
  3. package/package.json +1 -1
  4. package/src/combinator/control/constraint/block.ts +2 -2
  5. package/src/combinator/control/constraint/contract.ts +4 -3
  6. package/src/combinator/control/constraint/line.ts +5 -5
  7. package/src/combinator/control/manipulation/fence.ts +4 -4
  8. package/src/combinator/control/manipulation/scope.ts +1 -1
  9. package/src/combinator/control/manipulation/surround.ts +31 -15
  10. package/src/combinator/data/delimiter.ts +82 -6
  11. package/src/combinator/data/parser/context.ts +1 -34
  12. package/src/parser/api/normalize.ts +5 -1
  13. package/src/parser/block/reply/cite.ts +1 -1
  14. package/src/parser/block/reply/quote.ts +1 -1
  15. package/src/parser/block/reply.ts +1 -1
  16. package/src/parser/inline/annotation.ts +3 -3
  17. package/src/parser/inline/deletion.ts +1 -1
  18. package/src/parser/inline/emphasis.ts +4 -4
  19. package/src/parser/inline/emstrong.test.ts +1 -0
  20. package/src/parser/inline/emstrong.ts +3 -3
  21. package/src/parser/inline/extension/index.ts +2 -3
  22. package/src/parser/inline/extension/placeholder.ts +2 -2
  23. package/src/parser/inline/html.ts +3 -3
  24. package/src/parser/inline/htmlentity.ts +2 -2
  25. package/src/parser/inline/insertion.ts +1 -1
  26. package/src/parser/inline/italic.test.ts +1 -0
  27. package/src/parser/inline/italic.ts +2 -2
  28. package/src/parser/inline/link.test.ts +0 -1
  29. package/src/parser/inline/link.ts +3 -3
  30. package/src/parser/inline/mark.test.ts +1 -0
  31. package/src/parser/inline/mark.ts +4 -4
  32. package/src/parser/inline/media.test.ts +0 -1
  33. package/src/parser/inline/media.ts +1 -1
  34. package/src/parser/inline/reference.ts +3 -3
  35. package/src/parser/inline/ruby.test.ts +5 -0
  36. package/src/parser/inline/ruby.ts +2 -2
  37. package/src/parser/inline/strong.ts +4 -4
  38. package/src/parser/node.ts +4 -4
  39. package/src/parser/source/escapable.ts +2 -1
  40. package/src/parser/source/str.ts +4 -4
  41. package/src/parser/source/text.ts +1 -1
  42. package/src/parser/source/unescapable.ts +2 -1
  43. package/src/parser/util.ts +7 -4
  44. package/src/parser/visibility.ts +41 -89
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.296.0",
3
+ "version": "0.296.2",
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
1
  import { Parser, failsafe } from '../../data/parser';
2
- import { isBlankline } from './line';
2
+ import { isEmptyline } from './line';
3
3
 
4
4
  export function block<P extends Parser>(parser: P, separation?: boolean): P;
5
5
  export function block<N>(parser: Parser<N>, separation = true): Parser<N> {
@@ -10,7 +10,7 @@ export function block<N>(parser: Parser<N>, separation = true): Parser<N> {
10
10
  if (position === source.length) return;
11
11
  const result = parser(input);
12
12
  if (result === undefined) return;
13
- if (separation && !isBlankline(source, context.position)) return;
13
+ if (separation && !isEmptyline(source, context.position)) return;
14
14
  assert(context.position === source.length || source[context.position - 1] === '\n');
15
15
  return context.position === source.length || source[context.position - 1] === '\n'
16
16
  ? result
@@ -1,5 +1,6 @@
1
1
  import { Parser, Input, List, Node, Context } from '../../data/parser';
2
- import { matcher, bind } from '../../../combinator';
2
+ import { tester } from '../../data/delimiter';
3
+ import { bind } from '../monad/bind';
3
4
 
4
5
  //export function contract<P extends Parser>(patterns: string | RegExp | (string | RegExp)[], parser: P, cond: (nodes: readonly Data<P>[], rest: string) => boolean): P;
5
6
  //export function contract<N>(patterns: string | RegExp | (string | RegExp)[], parser: Parser<N>, cond: (nodes: readonly N[], rest: string) => boolean): Parser<N> {
@@ -10,8 +11,8 @@ export function validate<P extends Parser>(pattern: string | RegExp, parser: P):
10
11
  export function validate<P extends Parser>(cond: ((input: Input<Parser.Context<P>>) => boolean), parser: P): P;
11
12
  export function validate<N>(pattern: string | RegExp | ((input: Input<Context>) => boolean), parser: Parser<N>): Parser<N> {
12
13
  if (typeof pattern === 'function') return guard(pattern, parser);
13
- const match = matcher(pattern, false);
14
- return input => match(input) ? parser(input) : undefined;
14
+ const test = tester(pattern, false);
15
+ return input => test(input) && parser(input);
15
16
  }
16
17
 
17
18
  function guard<P extends Parser>(f: (input: Input<Parser.Context<P>>) => boolean, parser: P): P;
@@ -14,7 +14,7 @@ export function line<N>(parser: Parser<N>): Parser<N> {
14
14
  context.source = source;
15
15
  context.offset -= position;
16
16
  if (result === undefined) return;
17
- if (context.position < position + line.length && !isBlankline(source, context.position)) return;
17
+ if (context.position < position + line.length && !isEmptyline(source, context.position)) return;
18
18
  context.position = position + line.length;
19
19
  return result;
20
20
  });
@@ -27,10 +27,10 @@ export function firstline(source: string, position: number): string {
27
27
  : source.slice(position, i + 1);
28
28
  }
29
29
 
30
- const blankline = /[^\S\n]*(?:$|\n)/y;
31
- export function isBlankline(source: string, position: number): boolean {
32
- blankline.lastIndex = position;
30
+ const emptyline = /[^\S\n]*(?:$|\n)/y;
31
+ export function isEmptyline(source: string, position: number): boolean {
32
+ emptyline.lastIndex = position;
33
33
  return source.length === position
34
34
  || source[position] === '\n'
35
- || blankline.test(source);
35
+ || emptyline.test(source);
36
36
  }
@@ -1,6 +1,6 @@
1
1
  import { Parser, List, Node, Context, failsafe } from '../../data/parser';
2
2
  import { consume } from '../../../combinator';
3
- import { firstline, isBlankline } from '../constraint/line';
3
+ import { firstline, isEmptyline } from '../constraint/line';
4
4
  import { push } from 'spica/array';
5
5
 
6
6
  export function fence<C extends Context, D extends Parser<unknown, C>[]>(opener: RegExp, limit: number, separation = true): Parser<string, C, D> {
@@ -20,20 +20,20 @@ export function fence<C extends Context, D extends Parser<unknown, C>[]>(opener:
20
20
  context.position += matches[0].length;
21
21
  // Prevent annoying parsing in editing.
22
22
  const secondline = firstline(source, context.position);
23
- if (isBlankline(secondline, 0) && firstline(source, context.position + secondline.length).trimEnd() !== delim) return;
23
+ if (isEmptyline(secondline, 0) && firstline(source, context.position + secondline.length).trimEnd() !== delim) return;
24
24
  let block = '';
25
25
  let closer = '';
26
26
  let overflow = '';
27
27
  for (let count = 1; ; ++count) {
28
28
  if (context.position === source.length) break;
29
29
  const line = firstline(source, context.position);
30
- if ((closer || count > limit + 1) && isBlankline(line, 0)) break;
30
+ if ((closer || count > limit + 1) && isEmptyline(line, 0)) break;
31
31
  if(closer) {
32
32
  overflow += line;
33
33
  }
34
34
  if (!closer && count <= limit + 1 && line.slice(0, delim.length) === delim && line.trimEnd() === delim) {
35
35
  closer = line;
36
- if (isBlankline(source, context.position + line.length)) {
36
+ if (isEmptyline(source, context.position + line.length)) {
37
37
  context.position += line.length;
38
38
  break;
39
39
  }
@@ -1,5 +1,5 @@
1
1
  import { Parser, input, failsafe } from '../../data/parser';
2
- import { matcher } from '../../../combinator';
2
+ import { matcher } from '../../data/delimiter';
3
3
 
4
4
  export function focus<P extends Parser>(scope: string | RegExp, parser: P, slice?: boolean): P;
5
5
  export function focus<N>(scope: string | RegExp, parser: Parser<N>, slice = true): Parser<N> {
@@ -1,50 +1,64 @@
1
1
  import { Parser, Result, List, Node, Context, failsafe } from '../../data/parser';
2
- import { matcher, clear } from '../../../combinator';
2
+ import { tester } from '../../data/delimiter';
3
3
 
4
4
  export function surround<P extends Parser, S = string>(
5
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: Parser.IntermediateParser<P>, closer: string | RegExp | Parser<S, Parser.Context<P>>,
5
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
6
+ parser: Parser.IntermediateParser<P>,
7
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
6
8
  optional?: false,
7
9
  backtracks?: readonly number[],
8
10
  f?: (rss: [List<Node<S>>, List<Node<Parser.SubNode<P>>>, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
9
11
  g?: (rss: [List<Node<S>>, List<Node<Parser.SubNode<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
10
12
  ): P;
11
13
  export function surround<P extends Parser, S = string>(
12
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: Parser.IntermediateParser<P>, closer: string | RegExp | Parser<S, Parser.Context<P>>,
14
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
15
+ parser: Parser.IntermediateParser<P>,
16
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
13
17
  optional?: boolean,
14
18
  backtracks?: readonly number[],
15
19
  f?: (rss: [List<Node<S>>, List<Node<Parser.SubNode<P>>> | undefined, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
16
20
  g?: (rss: [List<Node<S>>, List<Node<Parser.SubNode<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
17
21
  ): P;
18
22
  export function surround<P extends Parser, S = string>(
19
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: P, closer: string | RegExp | Parser<S, Parser.Context<P>>,
23
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
24
+ parser: P,
25
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
20
26
  optional?: false,
21
27
  backtracks?: readonly number[],
22
28
  f?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>>, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
23
29
  g?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
24
30
  ): P;
25
31
  export function surround<P extends Parser, S = string>(
26
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: P, closer: string | RegExp | Parser<S, Parser.Context<P>>,
32
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
33
+ parser: P,
34
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
27
35
  optional?: boolean,
28
36
  backtracks?: readonly number[],
29
37
  f?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
30
38
  g?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
31
39
  ): P;
32
40
  export function surround<P extends Parser<string>, S = string>(
33
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: string | RegExp | P, closer: string | RegExp | Parser<S, Parser.Context<P>>,
41
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
42
+ parser: string | RegExp | P,
43
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
34
44
  optional?: false,
35
45
  backtracks?: readonly number[],
36
46
  f?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>>, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
37
47
  g?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
38
48
  ): P;
39
49
  export function surround<P extends Parser<string>, S = string>(
40
- opener: string | RegExp | Parser<S, Parser.Context<P>>, parser: string | RegExp | P, closer: string | RegExp | Parser<S, Parser.Context<P>>,
50
+ opener: string | RegExp | Parser<S, Parser.Context<P>>,
51
+ parser: string | RegExp | P,
52
+ closer: string | RegExp | Parser<S, Parser.Context<P>>,
41
53
  optional?: boolean,
42
54
  backtracks?: readonly number[],
43
55
  f?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined, List<Node<S>>], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
44
56
  g?: (rss: [List<Node<S>>, List<Node<Parser.Node<P>>> | undefined], context: Parser.Context<P>) => Result<Parser.Node<P>, Parser.Context<P>, Parser.SubParsers<P>>,
45
57
  ): P;
46
58
  export function surround<N>(
47
- opener: string | RegExp | Parser<N>, parser: string | RegExp | Parser<N>, closer: string | RegExp | Parser<N>,
59
+ opener: string | RegExp | Parser<N>,
60
+ parser: string | RegExp | Parser<N>,
61
+ closer: string | RegExp | Parser<N>,
48
62
  optional: boolean = false,
49
63
  backtracks: readonly number[] = [],
50
64
  f?: (rss: [List<Node<N>>, List<Node<N>>, List<Node<N>>], context: Context) => Result<N>,
@@ -53,19 +67,19 @@ export function surround<N>(
53
67
  switch (typeof opener) {
54
68
  case 'string':
55
69
  case 'object':
56
- opener = clear(matcher(opener, true));
70
+ opener = tester(opener, true);
57
71
  }
58
72
  assert(opener);
59
73
  switch (typeof parser) {
60
74
  case 'string':
61
75
  case 'object':
62
- parser = clear(matcher(parser, true));
76
+ parser = tester(parser, true);
63
77
  }
64
78
  assert(parser);
65
79
  switch (typeof closer) {
66
80
  case 'string':
67
81
  case 'object':
68
- closer = clear(matcher(closer, true));
82
+ closer = tester(closer, true);
69
83
  }
70
84
  assert(closer);
71
85
  const [blen, rbs, wbs] = reduce(backtracks);
@@ -76,7 +90,7 @@ export function surround<N>(
76
90
  const { linebreak } = context;
77
91
  context.linebreak = 0;
78
92
  const nodesO = opener(input);
79
- if (!nodesO) {
93
+ if (nodesO === undefined) {
80
94
  return void revert(context, linebreak);
81
95
  }
82
96
  if (rbs && isBacktrack(context, rbs, position, blen)) {
@@ -84,14 +98,14 @@ export function surround<N>(
84
98
  }
85
99
  const nodesM = context.position < source.length ? parser(input) : undefined;
86
100
  context.range = context.position - position;
87
- if (!nodesM && !optional) {
101
+ if (nodesM === undefined && !optional) {
88
102
  wbs && setBacktrack(context, wbs, position);
89
103
  const result = g?.([nodesO, nodesM], context);
90
104
  return result || void revert(context, linebreak);
91
105
  }
92
106
  const nodesC = nodesM || optional ? closer(input) : undefined;
93
107
  context.range = context.position - position;
94
- if (!nodesC) {
108
+ if (nodesC === undefined) {
95
109
  wbs && setBacktrack(context, wbs, position);
96
110
  const result = g?.([nodesO, nodesM], context);
97
111
  return result || void revert(context, linebreak);
@@ -102,7 +116,9 @@ export function surround<N>(
102
116
  context.range = context.position - position;
103
117
  const result = f
104
118
  ? f([nodesO, nodesM!, nodesC], context)
105
- : nodesO.import(nodesM ?? new List()).import(nodesC);
119
+ : nodesM
120
+ ? nodesO.import(nodesM).import(nodesC)
121
+ : nodesO.import(nodesC);
106
122
  if (result) {
107
123
  context.linebreak ||= linebreak;
108
124
  }
@@ -1,5 +1,5 @@
1
- import { Input, Context } from './parser';
2
- import { matcher } from '../../combinator';
1
+ import { Parser, Input, List, Node, Context } from './parser';
2
+ import { consume } from './parser/context';
3
3
 
4
4
  interface Delimiter {
5
5
  readonly memory: Delimiter[];
@@ -33,11 +33,11 @@ export class Delimiters {
33
33
  return () => undefined;
34
34
  case 'string':
35
35
  case 'object':
36
- const match = matcher(pattern, false);
37
- const verify = after ? matcher(after, false) : undefined;
36
+ const test = tester(pattern, false);
37
+ const verify = after ? tester(after, false) : undefined;
38
38
  return verify
39
- ? input => match(input) !== undefined && verify(input) !== undefined || undefined
40
- : input => match(input) !== undefined || undefined;
39
+ ? input => test(input) !== undefined && verify(input) !== undefined || undefined
40
+ : input => test(input) !== undefined || undefined;
41
41
  }
42
42
  }
43
43
  private readonly tree: Record<number, Delimiter[]> = {};
@@ -150,3 +150,79 @@ export class Delimiters {
150
150
  return false;
151
151
  }
152
152
  }
153
+
154
+ export function matcher(pattern: string | RegExp, advance: boolean, after?: Parser<string>): Parser<string> {
155
+ assert(pattern instanceof RegExp ? !pattern.flags.match(/[gm]/) && pattern.sticky && !pattern.source.startsWith('^') : true);
156
+ const count = typeof pattern === 'object'
157
+ ? /[^^\\*+][*+]/.test(pattern.source)
158
+ : false;
159
+ switch (typeof pattern) {
160
+ case 'string':
161
+ if (pattern === '') return () => new List([new Node(pattern)]);
162
+ return input => {
163
+ const { context } = input;
164
+ const { source, position } = context;
165
+ if (!source.startsWith(pattern, position)) return;
166
+ if (advance) {
167
+ context.position += pattern.length;
168
+ }
169
+ const next = after?.(input);
170
+ return after
171
+ ? next && new List([new Node(pattern)]).import(next)
172
+ : new List([new Node(pattern)]);
173
+ };
174
+ case 'object':
175
+ assert(pattern.sticky);
176
+ return input => {
177
+ const { context } = input;
178
+ const { source, position } = context;
179
+ pattern.lastIndex = position;
180
+ if (!pattern.test(source)) return;
181
+ const src = source.slice(position, pattern.lastIndex);
182
+ count && consume(src.length, context);
183
+ if (advance) {
184
+ context.position += src.length;
185
+ }
186
+ const next = after?.(input);
187
+ return after
188
+ ? next && new List([new Node(src)]).import(next)
189
+ : new List([new Node(src)]);
190
+ };
191
+ }
192
+ }
193
+
194
+ export function tester(pattern: string | RegExp, advance: boolean, after?: Parser<unknown>): Parser<never> {
195
+ assert(pattern instanceof RegExp ? !pattern.flags.match(/[gm]/) && pattern.sticky && !pattern.source.startsWith('^') : true);
196
+ const count = typeof pattern === 'object'
197
+ ? /[^^\\*+][*+]/.test(pattern.source)
198
+ : false;
199
+ switch (typeof pattern) {
200
+ case 'string':
201
+ if (pattern === '') return () => new List();
202
+ return input => {
203
+ const { context } = input;
204
+ const { source, position } = context;
205
+ if (!source.startsWith(pattern, position)) return;
206
+ if (advance) {
207
+ context.position += pattern.length;
208
+ }
209
+ if (after && after(input) === undefined) return;
210
+ return new List();
211
+ };
212
+ case 'object':
213
+ assert(pattern.sticky);
214
+ return input => {
215
+ const { context } = input;
216
+ const { source, position } = context;
217
+ pattern.lastIndex = position;
218
+ if (!pattern.test(source)) return;
219
+ const len = pattern.lastIndex - position;
220
+ count && consume(len, context);
221
+ if (advance) {
222
+ context.position += len;
223
+ }
224
+ if (after && after(input) === undefined) return;
225
+ return new List();
226
+ };
227
+ }
228
+ }
@@ -1,4 +1,4 @@
1
- import { Parser, Result, List, Node, Context, Options } from '../../data/parser';
1
+ import { Parser, Result, Context, Options } from '../../data/parser';
2
2
  import { min } from 'spica/alias';
3
3
  import { clone } from 'spica/assign';
4
4
 
@@ -158,36 +158,3 @@ export function constraint<N>(state: number, positive: boolean | Parser<N>, pars
158
158
  : undefined;
159
159
  };
160
160
  }
161
-
162
- export function matcher(pattern: string | RegExp, advance: boolean, verify?: (source: string, position: number, range: number) => boolean): Parser<string> {
163
- assert(pattern instanceof RegExp ? !pattern.flags.match(/[gm]/) && pattern.sticky && !pattern.source.startsWith('^') : true);
164
- const count = typeof pattern === 'object'
165
- ? /[^^\\*+][*+]/.test(pattern.source)
166
- : false;
167
- switch (typeof pattern) {
168
- case 'string':
169
- return ({ context }) => {
170
- const { source, position } = context;
171
- if (!source.startsWith(pattern, position)) return;
172
- if (verify?.(source, position, pattern.length) === false) return;
173
- if (advance) {
174
- context.position += pattern.length;
175
- }
176
- return new List([new Node(pattern)]);
177
- };
178
- case 'object':
179
- assert(pattern.sticky);
180
- return ({ context }) => {
181
- const { source, position } = context;
182
- pattern.lastIndex = position;
183
- if (!pattern.test(source)) return;
184
- const src = source.slice(position, pattern.lastIndex);
185
- count && consume(src.length, context);
186
- if (verify?.(source, position, src.length) === false) return;
187
- if (advance) {
188
- context.position += src.length;
189
- }
190
- return new List([new Node(src)]);
191
- };
192
- }
193
- }
@@ -24,7 +24,7 @@ function sanitize(source: string): string {
24
24
 
25
25
  // https://dev.w3.org/html5/html-author/charref
26
26
  // https://en.wikipedia.org/wiki/Whitespace_character
27
- export const invisibleHTMLEntityNames = [
27
+ const invisibleHTMLEntityNames = [
28
28
  'Tab',
29
29
  'NewLine',
30
30
  'NonBreakingSpace',
@@ -63,6 +63,10 @@ const parser = (el => (entity: string): string => {
63
63
  el.innerHTML = entity;
64
64
  return el.textContent!;
65
65
  })(html('span'));
66
+ export const invisibleBlankHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
67
+ .filter(name => parser(`&${name};`).trimStart() === '');
68
+ export const invisibleGraphHTMLEntityNames: readonly string[] = invisibleHTMLEntityNames
69
+ .filter(name => parser(`&${name};`).trimStart() !== '');
66
70
  const unreadableEscapeHTMLEntityNames = invisibleHTMLEntityNames.filter(name => ![
67
71
  'Tab',
68
72
  'NewLine',
@@ -34,6 +34,6 @@ export const cite: ReplyParser.CiteParser = line(fmap(
34
34
  ? define(node, { 'data-depth': `${quotes.length + 1}` }, node.innerText.slice(1))
35
35
  : node.slice(1),
36
36
  ]))),
37
- new Node(html('br'), Flag.invisible),
37
+ new Node(html('br'), Flag.blank),
38
38
  ]);
39
39
  }));
@@ -22,7 +22,7 @@ export const quote: ReplyParser.QuoteParser = lazy(() => block(fmap(
22
22
  unescsource,
23
23
  ])))),
24
24
  (ns, { source, position }) => new List([
25
- new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br'), Flag.invisible),
25
+ new Node(source[position - 1] === '\n' ? ns.pop()!.value as HTMLBRElement : html('br'), Flag.blank),
26
26
  new Node(html('span', { class: 'quote' }, defrag(unwrap(ns)))),
27
27
  ].reverse())),
28
28
  false));
@@ -21,6 +21,6 @@ export const reply: ReplyParser = block(validate(csyntax, fmap(
21
21
  visualize(fmap(some(inline), (ns, { source, position }) =>
22
22
  source[position - 1] === '\n'
23
23
  ? ns
24
- : ns.push(new Node(html('br'), Flag.invisible)) && ns)))
24
+ : ns.push(new Node(html('br'), Flag.blank)) && ns)))
25
25
  ])),
26
26
  ns => new List([new Node(html('p', defrag(unwrap(trimBlankNodeEnd(ns)))))]))));
@@ -1,16 +1,16 @@
1
1
  import { AnnotationParser } from '../inline';
2
2
  import { State, Backtrack } from '../context';
3
3
  import { List, Node } from '../../combinator/data/parser';
4
- import { union, some, precedence, state, constraint, surround, setBacktrack, lazy } from '../../combinator';
4
+ import { union, some, precedence, state, constraint, surround, close, setBacktrack, lazy } from '../../combinator';
5
5
  import { inline } from '../inline';
6
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
+ close('((', beforeNonblank),
12
12
  precedence(1, state(State.annotation,
13
- beforeNonblank(some(union([inline]), ')', [[')', 1]])))),
13
+ some(union([inline]), ')', [[')', 1]]))),
14
14
  '))',
15
15
  false,
16
16
  [2, 1 | Backtrack.common, 3 | Backtrack.doublebracket],
@@ -8,7 +8,7 @@ import { unwrap, repeat } from '../util';
8
8
  import { html, defrag } from 'typed-dom/dom';
9
9
 
10
10
  export const deletion: DeletionParser = lazy(() =>
11
- precedence(0, recursion(Recursion.inline, repeat('~~', surround(
11
+ precedence(0, recursion(Recursion.inline, repeat('~~', '', surround(
12
12
  '',
13
13
  some(union([
14
14
  some(inline, blankWith('\n', '~~')),
@@ -5,17 +5,17 @@ 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 { beforeNonblank, afterNonblank } from '../visibility';
8
+ import { beforeNonblankWith, 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('*', (source, position, range) => !source.startsWith('*', position + range)),
13
+ str('*', beforeNonblankWith(/(?!\*)/)),
14
14
  precedence(0, recursion(Recursion.inline,
15
- beforeNonblank(some(union([
15
+ some(union([
16
16
  some(inline, '*', afterNonblank),
17
17
  strong,
18
- ]))))),
18
+ ])))),
19
19
  str('*'),
20
20
  false, [],
21
21
  ([, bs]) => new List([new Node(html('em', defrag(unwrap(bs))))]),
@@ -114,6 +114,7 @@ describe('Unit: parser/inline/emstrong', () => {
114
114
  assert.deepStrictEqual(inspect(parser, input('******a*b ***', new Context())), [['*****', '<em>a</em>', 'b ', '***'], '']);
115
115
  assert.deepStrictEqual(inspect(parser, input('******a*b ****', new Context())), [['*****', '<em>a</em>', 'b ', '****'], '']);
116
116
  assert.deepStrictEqual(inspect(parser, input('******a*b *****', new Context())), [['*****', '<em>a</em>', 'b ', '*****'], '']);
117
+ assert.deepStrictEqual(inspect(parser, input('******a*** b***', new Context())), [['<em><strong><em><strong>a</strong></em> b</strong></em>'], '']);
117
118
  });
118
119
 
119
120
  });
@@ -23,9 +23,9 @@ const subemphasis: Parser.IntermediateParser<EmphasisParser> = lazy(() => some(u
23
23
  // 可能な限り早く閉じるよう解析しなければならない。
24
24
  // このため終端記号の後ろを見て終端を中止し同じ構文を再帰的に適用してはならない。
25
25
  export const emstrong: EmStrongParser = lazy(() =>
26
- precedence(0, recursion(Recursion.inline, repeat('***', surround(
26
+ precedence(0, recursion(Recursion.inline, repeat('***', beforeNonblank, surround(
27
27
  '',
28
- beforeNonblank(some(union([some(inline, '*', afterNonblank)]))),
28
+ some(union([some(inline, '*', afterNonblank)])),
29
29
  strs('*', 3),
30
30
  false, [],
31
31
  ([, bs, cs], context): Result<Parser.Node<EmStrongParser>, Parser.Context<EmStrongParser>> => {
@@ -33,7 +33,7 @@ export const emstrong: EmStrongParser = lazy(() =>
33
33
  const { buffer } = context;
34
34
  switch (cs.head!.value) {
35
35
  case '***':
36
- return bs;
36
+ return buffer.import(bs);
37
37
  case '**':
38
38
  return bind<EmphasisParser>(
39
39
  subemphasis,
@@ -13,13 +13,12 @@ import { html, define, defrag } from 'typed-dom/dom';
13
13
  import IndexParser = ExtensionParser.IndexParser;
14
14
 
15
15
  export const index: IndexParser = lazy(() => constraint(State.index, fmap(indexee(surround(
16
- str('[#'),
16
+ str('[#', beforeNonblank),
17
17
  precedence(1, state(State.linkers,
18
- beforeNonblank(
19
18
  some(inits([
20
19
  inline,
21
20
  signature,
22
- ]), ']', [[']', 1]])))),
21
+ ]), ']', [[']', 1]]))),
23
22
  str(']'),
24
23
  false,
25
24
  [3 | Backtrack.common],
@@ -14,9 +14,9 @@ import { html } from 'typed-dom/dom';
14
14
 
15
15
  export const placeholder: ExtensionParser.PlaceholderParser = lazy(() => surround(
16
16
  // ^はabbrで使用済みだが^:などのようにして分離使用可能
17
- str(/\[[:^|]/y),
17
+ str(/\[[:^|]/y, beforeNonblank),
18
18
  precedence(1, recursion(Recursion.inline,
19
- beforeNonblank(some(union([inline]), ']', [[']', 1]])))),
19
+ some(union([inline]), ']', [[']', 1]]))),
20
20
  str(']'),
21
21
  false,
22
22
  [3 | Backtrack.common],
@@ -5,7 +5,7 @@ import { Flag } from '../node';
5
5
  import { union, some, recursion, precedence, validate, surround, open, match, lazy } from '../../combinator';
6
6
  import { inline } from '../inline';
7
7
  import { str } from '../source';
8
- import { isLooseNodeStart, blankWith } from '../visibility';
8
+ import { isNonblankFirstLine, blankWith } from '../visibility';
9
9
  import { invalid, unwrap } from '../util';
10
10
  import { memoize } from 'spica/memoize';
11
11
  import { html as h, defrag } from 'typed-dom/dom';
@@ -29,7 +29,7 @@ export const html: HTMLParser = lazy(() => validate(/<[a-z]+(?=[ >])/yi,
29
29
  open(str(/ ?/y), str('>'), true),
30
30
  true, [],
31
31
  ([as, bs = new List(), cs], 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
+ 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.blank : Flag.none)]),
33
33
  ([as, bs = new List()], context) =>
34
34
  new List([new Node(elem(as.head!.value.slice(1), false, [...unwrap(as.import(bs))], new List(), new List(), context))])),
35
35
  match(
@@ -85,7 +85,7 @@ function elem(tag: string, content: boolean, as: readonly string[], bs: List<Nod
85
85
  if (content) {
86
86
  if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, context);
87
87
  if (bs.length === 0) return ielem('content', `Missing the content`, context);
88
- if (!isLooseNodeStart(bs)) return ielem('content', `Missing the visible content in the same line`, context);
88
+ if (!isNonblankFirstLine(bs)) return ielem('content', `Missing the visible content in the same line`, context);
89
89
  }
90
90
  const [attrs] = attributes('html', attrspecs[tag], as.slice(1, as.at(-1) === '>' ? -1 : as.length));
91
91
  if (/(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')) return ielem('attribute', 'Invalid HTML attribute', context)
@@ -1,7 +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
+ import { Flag, isBlankHTMLEntityName } from '../node';
5
5
  import { union, surround, fmap } from '../../combinator';
6
6
  import { str } from '../source';
7
7
  import { invalid } from '../util';
@@ -15,7 +15,7 @@ export const unsafehtmlentity: UnsafeHTMLEntityParser = surround(
15
15
  new List([
16
16
  new Node(
17
17
  parser(as.head!.value + bs.head!.value + cs.head!.value),
18
- isInvisibleHTMLEntityName(bs.head!.value) ? Flag.invisible : Flag.none)
18
+ isBlankHTMLEntityName(bs.head!.value) ? Flag.blank : Flag.none)
19
19
  ]),
20
20
  ([as, bs]) =>
21
21
  new List([new Node(as.head!.value + (bs?.head?.value ?? ''))]));
@@ -8,7 +8,7 @@ import { unwrap, repeat } from '../util';
8
8
  import { html, defrag } from 'typed-dom/dom';
9
9
 
10
10
  export const insertion: InsertionParser = lazy(() =>
11
- precedence(0, recursion(Recursion.inline, repeat('++', surround(
11
+ precedence(0, recursion(Recursion.inline, repeat('++', '', surround(
12
12
  '',
13
13
  some(union([
14
14
  some(inline, blankWith('\n', '++')),
@@ -57,6 +57,7 @@ describe('Unit: parser/inline/italic', () => {
57
57
  assert.deepStrictEqual(inspect(parser, input('//////a/////b', new Context())), [['///', '<i>a</i>', '//b'], '']);
58
58
  assert.deepStrictEqual(inspect(parser, input('//////a//////', new Context())), [['<i><i>a</i></i>'], '']);
59
59
  assert.deepStrictEqual(inspect(parser, input('//////a///b///', new Context())), [['<i><i>a</i>b</i>'], '']);
60
+ assert.deepStrictEqual(inspect(parser, input('//////a/// b///', new Context())), [['<i><i>a</i> b</i>'], '']);
60
61
  assert.deepStrictEqual(inspect(parser, input('///a ///b//////', new Context())), [['<i>a <i>b</i></i>'], '']);
61
62
  assert.deepStrictEqual(inspect(parser, input('///- ///b//////', new Context())), [['<i>- <i>b</i></i>'], '']);
62
63
  assert.deepStrictEqual(inspect(parser, input('///a\\ ///b//////', new Context())), [['<i>a <i>b</i></i>'], '']);
@@ -11,9 +11,9 @@ import { html, defrag } from 'typed-dom/dom';
11
11
  // 斜体は単語に使うとかえって見づらく読み飛ばしやすくなるため使わないべきであり
12
12
  // ある程度の長さのある文に使うのが望ましい。
13
13
  export const italic: ItalicParser = lazy(() =>
14
- precedence(0, recursion(Recursion.inline, repeat('///', surround(
14
+ precedence(0, recursion(Recursion.inline, repeat('///', beforeNonblank, surround(
15
15
  '',
16
- beforeNonblank(some(union([inline]), '///', afterNonblank)),
16
+ some(union([inline]), '///', afterNonblank),
17
17
  '///',
18
18
  false, [],
19
19
  ([, bs], { buffer }) => buffer.import(bs),