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.
- package/CHANGELOG.md +4 -0
- package/design.md +48 -2
- package/dist/index.js +215 -121
- package/markdown.d.ts +6 -17
- package/package.json +1 -1
- package/src/combinator/control/manipulation/surround.ts +17 -13
- package/src/combinator/data/parser/context/delimiter.ts +2 -2
- package/src/combinator/data/parser/context.ts +4 -3
- package/src/combinator/data/parser/some.ts +3 -3
- package/src/combinator/data/parser.ts +5 -3
- package/src/parser/api/parse.test.ts +34 -30
- package/src/parser/block/dlist.ts +1 -1
- package/src/parser/block/heading.ts +2 -2
- package/src/parser/block/mediablock.ts +2 -2
- package/src/parser/block/pagebreak.ts +1 -1
- package/src/parser/block/reply.ts +1 -1
- package/src/parser/block.ts +3 -1
- package/src/parser/header.ts +1 -1
- package/src/parser/inline/annotation.ts +4 -5
- package/src/parser/inline/autolink/account.ts +1 -1
- package/src/parser/inline/autolink/anchor.ts +1 -1
- package/src/parser/inline/autolink/channel.ts +1 -1
- package/src/parser/inline/autolink/email.ts +1 -1
- package/src/parser/inline/autolink/hashnum.ts +1 -1
- package/src/parser/inline/autolink/hashtag.ts +1 -1
- package/src/parser/inline/autolink/url.test.ts +8 -2
- package/src/parser/inline/autolink/url.ts +5 -6
- package/src/parser/inline/bracket.ts +25 -3
- package/src/parser/inline/code.ts +2 -2
- package/src/parser/inline/extension/index.ts +42 -26
- package/src/parser/inline/extension/indexee.ts +6 -3
- package/src/parser/inline/extension/label.ts +1 -1
- package/src/parser/inline/html.test.ts +22 -19
- package/src/parser/inline/html.ts +82 -78
- package/src/parser/inline/link.ts +12 -9
- package/src/parser/inline/mark.ts +1 -1
- package/src/parser/inline/math.test.ts +1 -0
- package/src/parser/inline/math.ts +18 -9
- package/src/parser/inline/media.test.ts +3 -3
- package/src/parser/inline/media.ts +14 -6
- package/src/parser/inline/reference.ts +67 -10
- package/src/parser/inline/ruby.ts +6 -5
- package/src/parser/inline/shortmedia.ts +1 -1
- package/src/parser/inline.ts +4 -19
- package/src/parser/segment.test.ts +3 -2
- package/src/parser/segment.ts +2 -2
- package/src/parser/source/escapable.ts +1 -1
- package/src/parser/source/text.ts +2 -2
- package/src/parser/source/unescapable.ts +1 -1
- package/src/parser/util.ts +14 -4
- package/src/parser/visibility.ts +1 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { CodeParser } from '../inline';
|
|
2
|
-
import { validate,
|
|
2
|
+
import { validate, isBacktrack, setBacktrack, match } from '../../combinator';
|
|
3
3
|
import { Backtrack } from '../context';
|
|
4
4
|
import { html } from 'typed-dom/dom';
|
|
5
5
|
|
|
6
6
|
export const code: CodeParser = validate(
|
|
7
7
|
({ source, context }) =>
|
|
8
8
|
source[0] === '`' &&
|
|
9
|
-
!
|
|
9
|
+
!isBacktrack(context, [1 | Backtrack.bracket], source),
|
|
10
10
|
match(
|
|
11
11
|
/^(`+)(?!`)([^\n]*?)(?:((?<!`)\1(?!`))|$|\n)/,
|
|
12
12
|
([whole, , body, closer]) => ({ source, context }) =>
|
|
@@ -5,50 +5,66 @@ import { union, inits, some, precedence, state, constraint, validate, surround,
|
|
|
5
5
|
import { inline } from '../../inline';
|
|
6
6
|
import { indexee, identity } from './indexee';
|
|
7
7
|
import { unsafehtmlentity } from '../htmlentity';
|
|
8
|
-
import {
|
|
8
|
+
import { txt, str } from '../../source';
|
|
9
9
|
import { tightStart, trimBlankNodeEnd } from '../../visibility';
|
|
10
|
-
import { unshift } from 'spica/array';
|
|
10
|
+
import { unshift, push } from 'spica/array';
|
|
11
11
|
import { html, define, defrag } from 'typed-dom/dom';
|
|
12
12
|
|
|
13
13
|
import IndexParser = ExtensionParser.IndexParser;
|
|
14
14
|
|
|
15
|
-
export const index: IndexParser = lazy(() => constraint(State.index,
|
|
16
|
-
'[#',
|
|
17
|
-
precedence(1, state(State.linkers
|
|
15
|
+
export const index: IndexParser = lazy(() => constraint(State.index, fmap(indexee(surround(
|
|
16
|
+
str('[#'),
|
|
17
|
+
precedence(1, state(State.linkers,
|
|
18
18
|
tightStart(
|
|
19
19
|
some(inits([
|
|
20
20
|
inline,
|
|
21
21
|
signature,
|
|
22
22
|
]), ']', [[']', 1]])))),
|
|
23
|
-
']',
|
|
23
|
+
str(']'),
|
|
24
24
|
false,
|
|
25
|
-
([,
|
|
26
|
-
context.linebreak ===
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
([as, bs, cs], rest, context) => {
|
|
26
|
+
if (context.linebreak === 0 && trimBlankNodeEnd(bs).length > 0) {
|
|
27
|
+
return [[html('a', { 'data-index': dataindex(bs) }, defrag(bs))], rest];
|
|
28
|
+
}
|
|
29
|
+
return (context.state! & State.linkers) === State.linkers
|
|
30
|
+
? [push(push(unshift(as, bs), cs), ['']), rest]
|
|
31
|
+
: undefined;
|
|
32
|
+
},
|
|
33
|
+
([as, bs], rest, context) => {
|
|
34
|
+
return (context.state! & State.linkers) === State.linkers
|
|
35
|
+
? [push(unshift(as, bs), ['']), rest]
|
|
36
|
+
: undefined;
|
|
37
|
+
},
|
|
31
38
|
[3 | Backtrack.bracket])),
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
ns => {
|
|
40
|
+
if (ns.length === 1) {
|
|
41
|
+
const el = ns[0] as HTMLElement;
|
|
42
|
+
return [
|
|
43
|
+
define(el, {
|
|
44
|
+
id: el.id ? null : undefined,
|
|
45
|
+
class: 'index',
|
|
46
|
+
href: el.id ? `#${el.id}` : undefined,
|
|
47
|
+
})
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
assert(ns.at(-1) === '');
|
|
52
|
+
ns.pop();
|
|
53
|
+
return ns;
|
|
54
|
+
}
|
|
55
|
+
})));
|
|
40
56
|
|
|
41
57
|
export const signature: IndexParser.SignatureParser = lazy(() => validate('|', surround(
|
|
42
58
|
str(/^\|(?!\\?\s)/),
|
|
43
|
-
tightStart(some(union([inline]), ']')),
|
|
59
|
+
tightStart(some(union([inline]), ']', [[']', 1]])),
|
|
44
60
|
/^(?=])/,
|
|
45
61
|
false,
|
|
46
62
|
(_, rest, context) => {
|
|
47
63
|
//context.offset ??= 0;
|
|
48
64
|
//context.offset += rest.length;
|
|
49
|
-
const
|
|
65
|
+
const text = eval(sig({ source: context.recent![1], context }), []).join('');
|
|
50
66
|
//context.offset -= rest.length;
|
|
51
|
-
const index = identity('index', undefined,
|
|
67
|
+
const index = identity('index', undefined, text)?.slice(7);
|
|
52
68
|
return index
|
|
53
69
|
? [[html('span', { class: 'indexer', 'data-index': index })], rest]
|
|
54
70
|
: undefined;
|
|
@@ -57,7 +73,7 @@ export const signature: IndexParser.SignatureParser = lazy(() => validate('|', s
|
|
|
57
73
|
|
|
58
74
|
export function dataindex(ns: readonly (string | HTMLElement)[]): string | undefined {
|
|
59
75
|
if (ns.length === 0) return;
|
|
60
|
-
for (let i = ns.length
|
|
76
|
+
for (let i = ns.length; i--;) {
|
|
61
77
|
const node = ns[i];
|
|
62
78
|
if (typeof node === 'string') return;
|
|
63
79
|
if (i === ns.length - 1 && ['UL', 'OL'].includes(node.tagName)) continue;
|
|
@@ -66,7 +82,7 @@ export function dataindex(ns: readonly (string | HTMLElement)[]): string | undef
|
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
const
|
|
85
|
+
const sig: IndexParser.SignatureParser.InternalParser = some(union([
|
|
70
86
|
unsafehtmlentity,
|
|
71
|
-
|
|
87
|
+
txt,
|
|
72
88
|
]));
|
|
@@ -5,7 +5,10 @@ import { define } from 'typed-dom/dom';
|
|
|
5
5
|
|
|
6
6
|
export function indexee<P extends Parser<unknown, MarkdownParser.Context>>(parser: P): P;
|
|
7
7
|
export function indexee(parser: Parser<HTMLElement, MarkdownParser.Context>): Parser<HTMLElement> {
|
|
8
|
-
return fmap(parser, (
|
|
8
|
+
return fmap(parser, (ns, _, { id }) =>
|
|
9
|
+
ns.length === 1
|
|
10
|
+
? [define(ns[0], { id: identity('index', id, ns[0]), 'data-index': null })]
|
|
11
|
+
: ns);
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
const MAX = 60;
|
|
@@ -130,8 +133,8 @@ export function signature(source: Element | DocumentFragment): string {
|
|
|
130
133
|
case 'label':
|
|
131
134
|
el.replaceWith(`[$${el.getAttribute('data-label')!.replace('$', '')}]`);
|
|
132
135
|
continue;
|
|
133
|
-
case 'remark':
|
|
134
136
|
case 'checkbox':
|
|
137
|
+
case 'remark':
|
|
135
138
|
case 'annotation':
|
|
136
139
|
case 'reference':
|
|
137
140
|
el.remove();
|
|
@@ -163,8 +166,8 @@ export function text(source: Element | DocumentFragment): string {
|
|
|
163
166
|
case 'math':
|
|
164
167
|
el.replaceWith(el.getAttribute('data-src')!);
|
|
165
168
|
continue;
|
|
166
|
-
case 'remark':
|
|
167
169
|
case 'checkbox':
|
|
170
|
+
case 'remark':
|
|
168
171
|
case 'annotation':
|
|
169
172
|
case 'reference':
|
|
170
173
|
el.remove();
|
|
@@ -11,7 +11,7 @@ export const segment: ExtensionParser.LabelParser.SegmentParser = clear(union([
|
|
|
11
11
|
body,
|
|
12
12
|
]));
|
|
13
13
|
|
|
14
|
-
export const label: ExtensionParser.LabelParser = constraint(State.label,
|
|
14
|
+
export const label: ExtensionParser.LabelParser = constraint(State.label, fmap(
|
|
15
15
|
union([
|
|
16
16
|
surround('[', body, ']', false, undefined, undefined, [1 | Backtrack.bracket, 1]),
|
|
17
17
|
body,
|
|
@@ -23,6 +23,8 @@ describe('Unit: parser/inline/html', () => {
|
|
|
23
23
|
assert.deepStrictEqual(inspect(parser('<a,b>')), undefined);
|
|
24
24
|
assert.deepStrictEqual(inspect(parser('<a, b>')), undefined);
|
|
25
25
|
assert.deepStrictEqual(inspect(parser('<T>')), [['<span class="invalid"><T></span>'], '']);
|
|
26
|
+
assert.deepStrictEqual(inspect(parser('<wbr/>')), undefined);
|
|
27
|
+
assert.deepStrictEqual(inspect(parser('<wbr />')), [['<span class="invalid"><wbr /></span>'], '']);
|
|
26
28
|
assert.deepStrictEqual(inspect(parser('<bdi>')), [['<span class="invalid"><bdi></span>'], '']);
|
|
27
29
|
assert.deepStrictEqual(inspect(parser('<bdi>z')), [['<span class="invalid"><bdi>z</span>'], '']);
|
|
28
30
|
assert.deepStrictEqual(inspect(parser('<bdi></bdi>')), [['<span class="invalid"><bdi></bdi></span>'], '']);
|
|
@@ -41,23 +43,29 @@ describe('Unit: parser/inline/html', () => {
|
|
|
41
43
|
assert.deepStrictEqual(inspect(parser('<BDI>a</bdi>')), [['<span class="invalid"><BDI></span>'], 'a</bdi>']);
|
|
42
44
|
assert.deepStrictEqual(inspect(parser('</bdi>')), undefined);
|
|
43
45
|
assert.deepStrictEqual(inspect(parser('<bdi/>')), undefined);
|
|
46
|
+
assert.deepStrictEqual(inspect(parser('<bdi />')), [['<span class="invalid"><bdi /></span>'], '']);
|
|
44
47
|
assert.deepStrictEqual(inspect(parser('<b><b><b>a</b></b></b>')), [['<span class="invalid"><b><span class="invalid"><b><span class="invalid"><b>a</b></span></b></span></b></span>'], '']);
|
|
45
48
|
assert.deepStrictEqual(inspect(parser('<bdi><bdi><bdi>a</bdi></bdi></bdi>')), [['<bdi><bdi><bdi>a</bdi></bdi></bdi>'], '']);
|
|
46
|
-
assert.deepStrictEqual(inspect(parser('<x a="*b*"')),
|
|
49
|
+
assert.deepStrictEqual(inspect(parser('<x a="*b*"')), [['<span class="invalid"><x a="*b*"</span>'], '']);
|
|
47
50
|
assert.deepStrictEqual(inspect(parser('<x a="*b*">')), [['<span class="invalid"><x a="*b*"></span>'], '']);
|
|
48
51
|
assert.deepStrictEqual(inspect(parser('<x a="*b*">c')), [['<span class="invalid"><x a="*b*"></span>'], 'c']);
|
|
49
|
-
assert.deepStrictEqual(inspect(parser('<bdi a="*b*"')),
|
|
52
|
+
assert.deepStrictEqual(inspect(parser('<bdi a="*b*"')), [['<span class="invalid"><bdi a="*b*"</span>'], '']);
|
|
50
53
|
assert.deepStrictEqual(inspect(parser('<bdi a="*b*">')), [['<span class="invalid"><bdi a="*b*"></span>'], '']);
|
|
51
54
|
assert.deepStrictEqual(inspect(parser('<bdi a="*b*">c')), [['<span class="invalid"><bdi a="*b*">c</span>'], '']);
|
|
52
|
-
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*')),
|
|
53
|
-
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>')),
|
|
54
|
-
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>d</bdi>')),
|
|
55
|
-
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *>*c*')),
|
|
56
|
-
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *>*c*</bdi>')),
|
|
55
|
+
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*')), [['<span class="invalid"><bdi a b="*" *c*</span>'], '']);
|
|
56
|
+
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>')), [['<span class="invalid"><bdi a b="*" *c*></span>'], '']);
|
|
57
|
+
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>d</bdi>')), [['<span class="invalid"><bdi a b="*" *c*>d</bdi></span>'], '']);
|
|
58
|
+
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *>*c*')), [['<span class="invalid"><bdi a b="*" *><em>c</em></span>'], '']);
|
|
59
|
+
assert.deepStrictEqual(inspect(parser('<bdi a b="*" *>*c*</bdi>')), [['<span class="invalid"><bdi a b="*" *><em>c</em></bdi></span>'], '']);
|
|
57
60
|
assert.deepStrictEqual(inspect(parser(' <bdi>a</bdi>')), undefined);
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
it('basic', () => {
|
|
64
|
+
assert.deepStrictEqual(inspect(parser('<wbr>')), [['<wbr>'], '']);
|
|
65
|
+
assert.deepStrictEqual(inspect(parser('<wbr >')), [['<wbr>'], '']);
|
|
66
|
+
assert.deepStrictEqual(inspect(parser('<wbr>a')), [['<wbr>'], 'a']);
|
|
67
|
+
assert.deepStrictEqual(inspect(parser('<bdi >a</bdi>')), [['<bdi>a</bdi>'], '']);
|
|
68
|
+
assert.deepStrictEqual(inspect(parser('<bdi >a</bdi>')), [['<bdi>a</bdi>'], '']);
|
|
61
69
|
assert.deepStrictEqual(inspect(parser('<bdi> a</bdi>')), [['<bdi> a</bdi>'], '']);
|
|
62
70
|
assert.deepStrictEqual(inspect(parser('<bdi> a </bdi>')), [['<bdi> a </bdi>'], '']);
|
|
63
71
|
assert.deepStrictEqual(inspect(parser('<bdi> a </bdi>')), [['<bdi> a </bdi>'], '']);
|
|
@@ -71,7 +79,6 @@ describe('Unit: parser/inline/html', () => {
|
|
|
71
79
|
assert.deepStrictEqual(inspect(parser('<bdi>a\n </bdi>')), [['<bdi>a </bdi>'], '']);
|
|
72
80
|
assert.deepStrictEqual(inspect(parser('<bdi>a\n<wbr></bdi>')), [['<bdi>a<wbr></bdi>'], '']);
|
|
73
81
|
assert.deepStrictEqual(inspect(parser('<bdi>a\nb</bdi>')), [['<bdi>a<br>b</bdi>'], '']);
|
|
74
|
-
assert.deepStrictEqual(inspect(parser('<wbr>a')), [['<wbr>'], 'a']);
|
|
75
82
|
});
|
|
76
83
|
|
|
77
84
|
it('nest', () => {
|
|
@@ -87,31 +94,27 @@ describe('Unit: parser/inline/html', () => {
|
|
|
87
94
|
assert.deepStrictEqual(inspect(parser('<bdi>a<a>b</a>c</bdi>')), [['<bdi>a<span class="invalid"><a>b</a></span>c</bdi>'], '']);
|
|
88
95
|
assert.deepStrictEqual(inspect(parser('<img>')), [['<span class="invalid"><img></span>'], '']);
|
|
89
96
|
assert.deepStrictEqual(inspect(parser('<bdi><img></bdi>')), [['<bdi><span class="invalid"><img></span></bdi>'], '']);
|
|
90
|
-
assert.deepStrictEqual(inspect(parser('<img />')), undefined);
|
|
91
|
-
assert.deepStrictEqual(inspect(parser('<bdi><img /></bdi>')), [['<bdi><img /></bdi>'], '']);
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
it('attribute', () => {
|
|
95
100
|
assert.deepStrictEqual(inspect(parser('<bdi\n>a</bdi>')), undefined);
|
|
96
|
-
assert.deepStrictEqual(inspect(parser('<bdi >a</bdi>')), [['<bdi
|
|
97
|
-
assert.deepStrictEqual(inspect(parser('<bdi
|
|
98
|
-
assert.deepStrictEqual(inspect(parser('<bdi >a</bdi>')), [['<bdi>a</bdi>'], '']);
|
|
99
|
-
assert.deepStrictEqual(inspect(parser('<bdi __proto__>a</bdi>')), undefined);
|
|
101
|
+
assert.deepStrictEqual(inspect(parser('<bdi \n>a</bdi>')), [['<span class="invalid"><bdi <br>>a</bdi></span>'], '']);
|
|
102
|
+
assert.deepStrictEqual(inspect(parser('<bdi __proto__>a</bdi>')), [['<span class="invalid"><bdi __proto__>a</bdi></span>'], '']);
|
|
100
103
|
assert.deepStrictEqual(inspect(parser('<bdi constructor>a</bdi>')), [['<span class="invalid"><bdi constructor>a</bdi></span>'], '']);
|
|
101
104
|
assert.deepStrictEqual(inspect(parser('<bdi toString>a</bdi>')), [['<span class="invalid"><bdi toString>a</bdi></span>'], '']);
|
|
102
105
|
assert.deepStrictEqual(inspect(parser('<bdi X>a</bdi>')), [['<span class="invalid"><bdi X>a</bdi></span>'], '']);
|
|
103
106
|
assert.deepStrictEqual(inspect(parser('<bdi x>a</bdi>')), [['<span class="invalid"><bdi x>a</bdi></span>'], '']);
|
|
104
107
|
assert.deepStrictEqual(inspect(parser('<bdo>a</bdo>')), [['<span class="invalid"><bdo>a</bdo></span>'], '']);
|
|
105
108
|
assert.deepStrictEqual(inspect(parser('<bdo >a</bdo>')), [['<span class="invalid"><bdo >a</bdo></span>'], '']);
|
|
106
|
-
assert.deepStrictEqual(inspect(parser('<bdo __proto__>a</bdo>')),
|
|
109
|
+
assert.deepStrictEqual(inspect(parser('<bdo __proto__>a</bdo>')), [['<span class="invalid"><bdo __proto__>a</bdo></span>'], '']);
|
|
107
110
|
assert.deepStrictEqual(inspect(parser('<bdo constructor>a</bdo>')), [['<span class="invalid"><bdo constructor>a</bdo></span>'], '']);
|
|
108
111
|
assert.deepStrictEqual(inspect(parser('<bdo toString>a</bdo>')), [['<span class="invalid"><bdo toString>a</bdo></span>'], '']);
|
|
109
112
|
assert.deepStrictEqual(inspect(parser('<bdo X>a</bdo>')), [['<span class="invalid"><bdo X>a</bdo></span>'], '']);
|
|
110
113
|
assert.deepStrictEqual(inspect(parser('<bdo x>a</bdo>')), [['<span class="invalid"><bdo x>a</bdo></span>'], '']);
|
|
111
114
|
assert.deepStrictEqual(inspect(parser('<bdo dir>a</bdo>')), [['<span class="invalid"><bdo dir>a</bdo></span>'], '']);
|
|
112
|
-
assert.deepStrictEqual(inspect(parser('<bdo dir=>a</bdo>')),
|
|
113
|
-
assert.deepStrictEqual(inspect(parser('<bdo dir=rtl>a</bdo>')),
|
|
114
|
-
assert.deepStrictEqual(inspect(parser('<bdo dir=">a</bdo>')),
|
|
115
|
+
assert.deepStrictEqual(inspect(parser('<bdo dir=>a</bdo>')), [['<span class="invalid"><bdo dir=>a</bdo></span>'], '']);
|
|
116
|
+
assert.deepStrictEqual(inspect(parser('<bdo dir=rtl>a</bdo>')), [['<span class="invalid"><bdo dir=rtl>a</bdo></span>'], '']);
|
|
117
|
+
assert.deepStrictEqual(inspect(parser('<bdo dir=">a</bdo>')), [['<span class="invalid"><bdo dir=">a</bdo></span>'], '']);
|
|
115
118
|
assert.deepStrictEqual(inspect(parser('<bdo dir="">a</bdo>')), [['<span class="invalid"><bdo dir="">a</bdo></span>'], '']);
|
|
116
119
|
assert.deepStrictEqual(inspect(parser('<bdo dir="rtl" dir="rtl">a</bdo>')), [['<span class="invalid"><bdo dir="rtl" dir="rtl">a</bdo></span>'], '']);
|
|
117
120
|
assert.deepStrictEqual(inspect(parser('<bdo diR="rtl">a</bdo>')), [['<span class="invalid"><bdo diR="rtl">a</bdo></span>'], '']);
|
|
@@ -120,7 +123,7 @@ describe('Unit: parser/inline/html', () => {
|
|
|
120
123
|
assert.deepStrictEqual(inspect(parser('<bdo dir="rtl" >a</bdo>')), [['<bdo dir="rtl">a</bdo>'], '']);
|
|
121
124
|
assert.deepStrictEqual(inspect(parser('<bdo dir="rtl">a</bdo>')), [['<bdo dir="rtl">a</bdo>'], '']);
|
|
122
125
|
assert.deepStrictEqual(inspect(parser('<wbr\n>')), undefined);
|
|
123
|
-
assert.deepStrictEqual(inspect(parser('<wbr >')), [['<wbr>'], '']);
|
|
126
|
+
assert.deepStrictEqual(inspect(parser('<wbr \n>')), [['<span class="invalid"><wbr </span>'], '\n>']);
|
|
124
127
|
assert.deepStrictEqual(inspect(parser('<wbr constructor>')), [['<span class="invalid"><wbr constructor></span>'], '']);
|
|
125
128
|
assert.deepStrictEqual(inspect(parser('<wbr X>')), [['<span class="invalid"><wbr X></span>'], '']);
|
|
126
129
|
assert.deepStrictEqual(inspect(parser('<wbr x>')), [['<span class="invalid"><wbr x></span>'], '']);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HTMLParser } from '../inline';
|
|
2
|
-
import { Recursion
|
|
2
|
+
import { Recursion } from '../context';
|
|
3
3
|
import { union, subsequence, some, recursion, precedence, validate, focus, surround, open, match, lazy } from '../../combinator';
|
|
4
4
|
import { inline } from '../inline';
|
|
5
5
|
import { str } from '../source';
|
|
@@ -9,8 +9,9 @@ import { memoize } from 'spica/memoize';
|
|
|
9
9
|
import { unshift, push, splice } from 'spica/array';
|
|
10
10
|
import { html as h, defrag } from 'typed-dom/dom';
|
|
11
11
|
|
|
12
|
-
const tags = Object.freeze(['bdo', 'bdi']);
|
|
12
|
+
const tags = Object.freeze(['wbr', 'bdo', 'bdi']);
|
|
13
13
|
const attrspecs = {
|
|
14
|
+
wbr: {},
|
|
14
15
|
bdo: {
|
|
15
16
|
dir: Object.freeze(['ltr', 'rtl']),
|
|
16
17
|
},
|
|
@@ -24,48 +25,110 @@ export const html: HTMLParser = lazy(() => validate(/^<[a-z]+(?=[^\S\n]|>)/i,
|
|
|
24
25
|
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
|
|
25
26
|
str(/^<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[^\S\n]|>)/i),
|
|
26
27
|
some(union([attribute])),
|
|
27
|
-
str(/^[^\S\n]
|
|
28
|
+
open(str(/^[^\S\n]*/), str('>'), true),
|
|
29
|
+
true,
|
|
28
30
|
([as, bs = [], cs], rest) =>
|
|
29
|
-
as[0].slice(1)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
undefined,
|
|
33
|
-
[3 | Backtrack.bracket]),
|
|
31
|
+
[[elem(as[0].slice(1), false, push(unshift(as, bs), cs), [], [])], rest],
|
|
32
|
+
([as, bs = []], rest) =>
|
|
33
|
+
[[elem(as[0].slice(1), false, unshift(as, bs), [], [])], rest]),
|
|
34
34
|
match(
|
|
35
35
|
new RegExp(String.raw`^<(${TAGS.join('|')})(?=[^\S\n]|>)`),
|
|
36
36
|
memoize(
|
|
37
37
|
([, tag]) =>
|
|
38
38
|
surround<HTMLParser.TagParser, string>(
|
|
39
39
|
surround(
|
|
40
|
-
str(`<${tag}`), some(attribute), str(/^[^\S\n]
|
|
41
|
-
|
|
40
|
+
str(`<${tag}`), some(attribute), open(str(/^[^\S\n]*/), str('>'), true),
|
|
41
|
+
true,
|
|
42
|
+
([as, bs = [], cs], rest) => [push(unshift(as, bs), cs), rest],
|
|
43
|
+
([as, bs = []], rest) => [unshift(as, bs), rest]),
|
|
42
44
|
precedence(3, recursion(Recursion.inline,
|
|
43
45
|
subsequence([
|
|
44
46
|
focus(/^[^\S\n]*\n/, some(inline)),
|
|
45
47
|
some(open(/^\n?/, some(inline, blankWith('\n', `</${tag}>`), [[blankWith('\n', `</${tag}>`), 3]]), true)),
|
|
46
48
|
]))),
|
|
47
|
-
str(`</${tag}>`),
|
|
49
|
+
str(`</${tag}>`),
|
|
50
|
+
true,
|
|
48
51
|
([as, bs = [], cs], rest) =>
|
|
49
|
-
[[elem(tag, as, bs, cs)], rest],
|
|
52
|
+
[[elem(tag, true, as, bs, cs)], rest],
|
|
50
53
|
([as, bs = []], rest) =>
|
|
51
|
-
[[elem(tag, as, bs, [])], rest]),
|
|
54
|
+
[[elem(tag, true, as, bs, [])], rest]),
|
|
52
55
|
([, tag]) => tag,
|
|
53
56
|
new Map())),
|
|
54
57
|
surround(
|
|
55
58
|
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
|
|
56
|
-
str(/^<[a-z]
|
|
59
|
+
str(/^<[a-z]{1,16}(?=[^\S\n]|>)/i),
|
|
57
60
|
some(union([attribute])),
|
|
58
|
-
str(/^[^\S\n]
|
|
61
|
+
open(str(/^[^\S\n]*/), str('>'), true),
|
|
62
|
+
true,
|
|
59
63
|
([as, bs = [], cs], rest) =>
|
|
60
|
-
[[elem(as[0].slice(1), push(unshift(as, bs), cs), [], [])], rest],
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
[[elem(as[0].slice(1), false, push(unshift(as, bs), cs), [], [])], rest],
|
|
65
|
+
([as, bs = []], rest) =>
|
|
66
|
+
[[elem(as[0].slice(1), false, unshift(as, bs), [], [])], rest]),
|
|
63
67
|
])));
|
|
64
68
|
|
|
65
69
|
export const attribute: HTMLParser.AttributeParser = union([
|
|
66
|
-
str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="[
|
|
70
|
+
str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|>)/i),
|
|
71
|
+
str(/^[^\S\n]+[^\s<>]+/),
|
|
67
72
|
]);
|
|
68
73
|
|
|
74
|
+
function elem(tag: string, content: boolean, as: string[], bs: (HTMLElement | string)[], cs: string[]): HTMLElement {
|
|
75
|
+
assert(as.length > 0);
|
|
76
|
+
assert(as[0][0] === '<');
|
|
77
|
+
if (!tags.includes(tag)) return ielem('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
|
|
78
|
+
if (content) {
|
|
79
|
+
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
|
|
80
|
+
if (bs.length === 0) return ielem('content', `Missing the content`, as, bs, cs);
|
|
81
|
+
if (!isLooseNodeStart(bs)) return ielem('content', `Missing the visible content in the same line`, as, bs, cs);
|
|
82
|
+
}
|
|
83
|
+
const attrs = attributes('html', attrspecs[tag], as.slice(1, as.at(-1) === '>' ? -2 : as.length));
|
|
84
|
+
if (/(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')) return ielem('attribute', 'Invalid HTML attribute', as, bs, cs)
|
|
85
|
+
if (as.at(-1) !== '>') return ielem('tag', `Missing the closing bracket ">"`, as, bs, cs);
|
|
86
|
+
return h(tag as 'span', attrs, defrag(bs));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ielem(type: string, message: string, as: (HTMLElement | string)[], bs: (HTMLElement | string)[], cs: (HTMLElement | string)[]): HTMLElement {
|
|
90
|
+
return h('span',
|
|
91
|
+
{ class: 'invalid', ...invalid('html', type, message) },
|
|
92
|
+
defrag(push(unshift(as, bs), cs)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const requiredAttributes = memoize(
|
|
96
|
+
(spec: Readonly<Record<string, readonly (string | undefined)[] | undefined>>) =>
|
|
97
|
+
Object.entries(spec).flatMap(([k, v]) => v && Object.isFrozen(v) ? [k] : []),
|
|
98
|
+
new WeakMap());
|
|
99
|
+
|
|
100
|
+
export function attributes(
|
|
101
|
+
syntax: string,
|
|
102
|
+
spec: Readonly<Record<string, readonly (string | undefined)[] | undefined>> | undefined,
|
|
103
|
+
params: string[],
|
|
104
|
+
): Record<string, string | undefined> {
|
|
105
|
+
assert(spec instanceof Object === false);
|
|
106
|
+
assert(!spec?.['__proto__']);
|
|
107
|
+
assert(!spec?.toString);
|
|
108
|
+
let invalidation = false;
|
|
109
|
+
const attrs: Record<string, string | undefined> = {};
|
|
110
|
+
for (let i = 0; i < params.length; ++i) {
|
|
111
|
+
const param = params[i].trimStart();
|
|
112
|
+
const name = param.split('=', 1)[0];
|
|
113
|
+
const value = param !== name
|
|
114
|
+
? param.slice(name.length + 2, -1).replace(/\\(.?)/g, '$1')
|
|
115
|
+
: undefined;
|
|
116
|
+
invalidation ||= !spec || name in attrs;
|
|
117
|
+
if (spec && name in spec && !spec[name]) continue;
|
|
118
|
+
spec?.[name]?.includes(value) || spec?.[name]?.length === 0 && value !== undefined
|
|
119
|
+
? attrs[name] = value ?? ''
|
|
120
|
+
: invalidation ||= !!spec;
|
|
121
|
+
assert(!(name in {} && attrs.hasOwnProperty(name)));
|
|
122
|
+
splice(params, i--, 1);
|
|
123
|
+
}
|
|
124
|
+
invalidation ||= !!spec && !requiredAttributes(spec).every(name => name in attrs);
|
|
125
|
+
if (invalidation) {
|
|
126
|
+
attrs['class'] = 'invalid';
|
|
127
|
+
Object.assign(attrs, invalid(syntax, 'argument', 'Invalid argument'));
|
|
128
|
+
}
|
|
129
|
+
return attrs;
|
|
130
|
+
}
|
|
131
|
+
|
|
69
132
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
|
|
70
133
|
// [...document.querySelectorAll('tbody > tr > td:first-child')].map(el => el.textContent.slice(1, -1))
|
|
71
134
|
const TAGS = Object.freeze([
|
|
@@ -206,62 +269,3 @@ const TAGS = Object.freeze([
|
|
|
206
269
|
"tt",
|
|
207
270
|
"xmp",
|
|
208
271
|
]);
|
|
209
|
-
|
|
210
|
-
function elem(tag: string, as: string[], bs: (HTMLElement | string)[], cs: string[]): HTMLElement {
|
|
211
|
-
assert(as.length > 0);
|
|
212
|
-
assert(as[0][0] === '<' && as.at(-1)!.slice(-1) === '>');
|
|
213
|
-
if (!tags.includes(tag)) return ielem('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
|
|
214
|
-
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
|
|
215
|
-
if (bs.length === 0) return ielem('content', `Missing the content`, as, bs, cs);
|
|
216
|
-
if (!isLooseNodeStart(bs)) return ielem('content', `Missing the visible content in the same line`, as, bs, cs);
|
|
217
|
-
const attrs = attributes('html', [], attrspecs[tag], as.slice(1, -1));
|
|
218
|
-
return /(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')
|
|
219
|
-
? ielem('attribute', 'Invalid HTML attribute', as, bs, cs)
|
|
220
|
-
: h(tag as 'span', attrs, defrag(bs));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function ielem(type: string, message: string, as: (HTMLElement | string)[], bs: (HTMLElement | string)[], cs: (HTMLElement | string)[]): HTMLElement {
|
|
224
|
-
return h('span',
|
|
225
|
-
{ class: 'invalid', ...invalid('html', type, message) },
|
|
226
|
-
defrag(push(unshift(as, bs), cs)));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const requiredAttributes = memoize(
|
|
230
|
-
(spec: Readonly<Record<string, readonly (string | undefined)[] | undefined>>) =>
|
|
231
|
-
Object.entries(spec).flatMap(([k, v]) => v && Object.isFrozen(v) ? [k] : []),
|
|
232
|
-
new WeakMap());
|
|
233
|
-
|
|
234
|
-
export function attributes(
|
|
235
|
-
syntax: string,
|
|
236
|
-
classes: readonly string[],
|
|
237
|
-
spec: Readonly<Record<string, readonly (string | undefined)[] | undefined>> | undefined,
|
|
238
|
-
params: string[],
|
|
239
|
-
): Record<string, string | undefined> {
|
|
240
|
-
assert(spec instanceof Object === false);
|
|
241
|
-
assert(!spec?.['__proto__']);
|
|
242
|
-
assert(!spec?.toString);
|
|
243
|
-
let invalidation = false;
|
|
244
|
-
const attrs: Record<string, string | undefined> = {};
|
|
245
|
-
for (let i = 0; i < params.length; ++i) {
|
|
246
|
-
const param = params[i].trim();
|
|
247
|
-
const name = param.split('=', 1)[0];
|
|
248
|
-
const value = param !== name
|
|
249
|
-
? param.slice(name.length + 2, -1).replace(/\\(.?)/g, '$1')
|
|
250
|
-
: undefined;
|
|
251
|
-
invalidation ||= !spec || name in attrs;
|
|
252
|
-
if (spec && name in spec && !spec[name]) continue;
|
|
253
|
-
spec?.[name]?.includes(value) || spec?.[name]?.length === 0 && value !== undefined
|
|
254
|
-
? attrs[name] = value ?? ''
|
|
255
|
-
: invalidation ||= !!spec;
|
|
256
|
-
assert(!(name in {} && attrs.hasOwnProperty(name)));
|
|
257
|
-
splice(params, i--, 1);
|
|
258
|
-
}
|
|
259
|
-
invalidation ||= !!spec && !requiredAttributes(spec).every(name => name in attrs);
|
|
260
|
-
if (invalidation) {
|
|
261
|
-
attrs['class'] = classes.length === 0
|
|
262
|
-
? 'invalid'
|
|
263
|
-
: `${classes.join(' ')}${classes.includes('invalid') ? '' : ' invalid'}`;
|
|
264
|
-
Object.assign(attrs, invalid(syntax, 'argument', 'Invalid argument'));
|
|
265
|
-
}
|
|
266
|
-
return attrs;
|
|
267
|
-
}
|
|
@@ -16,8 +16,8 @@ const optspec = {
|
|
|
16
16
|
} as const;
|
|
17
17
|
Object.setPrototypeOf(optspec, null);
|
|
18
18
|
|
|
19
|
-
export const textlink: LinkParser.TextLinkParser = lazy(() => constraint(State.link,
|
|
20
|
-
precedence(1, state(State.linkers
|
|
19
|
+
export const textlink: LinkParser.TextLinkParser = lazy(() => constraint(State.link, creation(10,
|
|
20
|
+
precedence(1, state(State.linkers,
|
|
21
21
|
bind(subsequence([
|
|
22
22
|
dup(surround(
|
|
23
23
|
'[',
|
|
@@ -25,11 +25,13 @@ export const textlink: LinkParser.TextLinkParser = lazy(() => constraint(State.l
|
|
|
25
25
|
']',
|
|
26
26
|
true,
|
|
27
27
|
([, ns = []], rest, context) =>
|
|
28
|
-
context.linebreak ===
|
|
28
|
+
context.linebreak === 0
|
|
29
29
|
? [push(ns, [Command.Escape]), rest]
|
|
30
30
|
: undefined,
|
|
31
31
|
undefined,
|
|
32
|
-
[3 | Backtrack.link, 3 | Backtrack.bracket])),
|
|
32
|
+
[3 | Backtrack.link, 2 | Backtrack.ruby, 3 | Backtrack.bracket])),
|
|
33
|
+
// `{ `と`{`で個別にバックトラックが発生し+1nされる。
|
|
34
|
+
// 自己再帰的にパースしてもオプションの不要なパースによる計算量の増加により相殺される。
|
|
33
35
|
dup(surround(
|
|
34
36
|
/^{(?![{}])/,
|
|
35
37
|
inits([uri, some(option)]),
|
|
@@ -41,7 +43,8 @@ export const textlink: LinkParser.TextLinkParser = lazy(() => constraint(State.l
|
|
|
41
43
|
if (content.at(-1) === Command.Escape) {
|
|
42
44
|
content.pop();
|
|
43
45
|
if (params === undefined) {
|
|
44
|
-
|
|
46
|
+
const head = context.recent!.reduce((a, b) => a + b.length, rest.length);
|
|
47
|
+
return void setBacktrack(context, [2 | Backtrack.link], head);
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
else {
|
|
@@ -54,7 +57,7 @@ export const textlink: LinkParser.TextLinkParser = lazy(() => constraint(State.l
|
|
|
54
57
|
return [[parse(defrag(content), params, context)], rest];
|
|
55
58
|
}))))));
|
|
56
59
|
|
|
57
|
-
export const medialink: LinkParser.MediaLinkParser = lazy(() => constraint(State.link | State.media,
|
|
60
|
+
export const medialink: LinkParser.MediaLinkParser = lazy(() => constraint(State.link | State.media, validate(['[', '{'], creation(10,
|
|
58
61
|
state(State.linkers,
|
|
59
62
|
bind(reverse(sequence([
|
|
60
63
|
dup(surround(
|
|
@@ -90,8 +93,8 @@ export const uri: LinkParser.ParameterParser.UriParser = union([
|
|
|
90
93
|
|
|
91
94
|
export const option: LinkParser.ParameterParser.OptionParser = union([
|
|
92
95
|
fmap(str(/^[^\S\n]+nofollow(?=[^\S\n]|})/), () => [` rel="nofollow"`]),
|
|
93
|
-
str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|})/),
|
|
94
|
-
|
|
96
|
+
str(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|})/i),
|
|
97
|
+
str(/^[^\S\n]+[^\s{}]+/),
|
|
95
98
|
]);
|
|
96
99
|
|
|
97
100
|
function parse(
|
|
@@ -119,7 +122,7 @@ function parse(
|
|
|
119
122
|
context.host?.origin || location.origin);
|
|
120
123
|
return el.classList.contains('invalid')
|
|
121
124
|
? el
|
|
122
|
-
: define(el, attributes('link',
|
|
125
|
+
: define(el, attributes('link', optspec, params));
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
function elem(
|
|
@@ -8,7 +8,7 @@ import { repeat } from '../util';
|
|
|
8
8
|
import { push } from 'spica/array';
|
|
9
9
|
import { html, define, defrag } from 'typed-dom/dom';
|
|
10
10
|
|
|
11
|
-
export const mark: MarkParser = lazy(() => constraint(State.linkers & ~State.mark,
|
|
11
|
+
export const mark: MarkParser = lazy(() => constraint(State.linkers & ~State.mark, validate('==',
|
|
12
12
|
precedence(0, state(State.mark, repeat('==', surround(
|
|
13
13
|
'',
|
|
14
14
|
recursion(Recursion.inline,
|
|
@@ -83,6 +83,7 @@ describe('Unit: parser/inline/math', () => {
|
|
|
83
83
|
assert.deepStrictEqual(inspect(parser('$\\huge0$')), [['<span class="invalid" translate="no">$\\huge0$</span>'], '']);
|
|
84
84
|
assert.deepStrictEqual(inspect(parser('$\\Begin{}$')), [['<span class="invalid" translate="no">$\\Begin{}$</span>'], '']);
|
|
85
85
|
assert.deepStrictEqual(inspect(parser('${\\begin}$')), [['<span class="invalid" translate="no">${\\begin}$</span>'], '']);
|
|
86
|
+
assert.deepStrictEqual(inspect(parser('$http://host$')), undefined);
|
|
86
87
|
assert.deepStrictEqual(inspect(parser(' ${a}$')), undefined);
|
|
87
88
|
});
|
|
88
89
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MathParser } from '../inline';
|
|
2
|
-
import { Recursion } from '../context';
|
|
2
|
+
import { Backtrack, Recursion } from '../context';
|
|
3
3
|
import { union, some, recursion, precedence, validate, focus, rewrite, surround, lazy } from '../../combinator';
|
|
4
|
-
import { escsource,
|
|
4
|
+
import { escsource, str } from '../source';
|
|
5
5
|
import { invalid } from '../util';
|
|
6
6
|
import { html } from 'typed-dom/dom';
|
|
7
7
|
|
|
@@ -9,14 +9,23 @@ const forbiddenCommand = /\\(?:begin|tiny|huge|large)(?![a-z])/i;
|
|
|
9
9
|
|
|
10
10
|
export const math: MathParser = lazy(() => validate('$', rewrite(
|
|
11
11
|
union([
|
|
12
|
-
surround(
|
|
12
|
+
surround(
|
|
13
|
+
/^\$(?={)/,
|
|
14
|
+
precedence(5, bracket),
|
|
15
|
+
'$',
|
|
16
|
+
false, undefined, undefined, [3 | Backtrack.bracket]),
|
|
13
17
|
surround(
|
|
14
18
|
/^\$(?![\s{}])/,
|
|
15
19
|
precedence(2, some(union([
|
|
16
|
-
bracket,
|
|
17
|
-
focus(
|
|
20
|
+
precedence(5, bracket),
|
|
21
|
+
some(focus(
|
|
22
|
+
/^(?:[ ([](?!\$)|\\[\\{}$#]?|[!%&')\x2A-\x5A\]^_\x61-\x7A|~])/,
|
|
23
|
+
escsource,
|
|
24
|
+
false),
|
|
25
|
+
'://'),
|
|
18
26
|
]))),
|
|
19
|
-
/^\$(?![0-9A-Za-z])
|
|
27
|
+
/^\$(?![0-9A-Za-z])/,
|
|
28
|
+
false, undefined, undefined, [3 | Backtrack.bracket]),
|
|
20
29
|
]),
|
|
21
30
|
({ source, context: { caches: { math: cache } = {} } }) => [[
|
|
22
31
|
cache?.get(source)?.cloneNode(true) ||
|
|
@@ -26,8 +35,8 @@ export const math: MathParser = lazy(() => validate('$', rewrite(
|
|
|
26
35
|
: {
|
|
27
36
|
class: 'invalid',
|
|
28
37
|
translate: 'no',
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
...invalid('math', 'content',
|
|
39
|
+
`"${source.match(forbiddenCommand)![0]}" command is forbidden`),
|
|
31
40
|
},
|
|
32
41
|
source)
|
|
33
42
|
], ''])));
|
|
@@ -37,7 +46,7 @@ const bracket: MathParser.BracketParser = lazy(() => surround(
|
|
|
37
46
|
recursion(Recursion.terminal,
|
|
38
47
|
some(union([
|
|
39
48
|
bracket,
|
|
40
|
-
some(escsource, /^[{}
|
|
49
|
+
some(escsource, /^[{}$#\n]/),
|
|
41
50
|
]))),
|
|
42
51
|
str('}'),
|
|
43
52
|
true));
|