securemark 0.287.1 → 0.288.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/dist/index.js +7369 -7448
- package/markdown.d.ts +11 -34
- package/package.json +1 -1
- package/src/combinator/control/manipulation/convert.ts +4 -4
- package/src/combinator/control/manipulation/trim.ts +3 -3
- package/src/parser/autolink.test.ts +4 -2
- package/src/parser/autolink.ts +2 -1
- package/src/parser/block/blockquote.test.ts +2 -0
- package/src/parser/block/blockquote.ts +4 -4
- package/src/parser/block/codeblock.test.ts +2 -0
- package/src/parser/block/codeblock.ts +9 -6
- package/src/parser/block/extension/aside.ts +7 -8
- package/src/parser/block/extension/example.ts +7 -8
- package/src/parser/block/extension/fig.ts +2 -1
- package/src/parser/block/extension/figure.ts +78 -75
- package/src/parser/block/extension/message.ts +7 -8
- package/src/parser/block/extension/placeholder.ts +6 -3
- package/src/parser/block/extension/table.ts +15 -26
- package/src/parser/block/heading.ts +2 -3
- package/src/parser/block/ilist.ts +17 -7
- package/src/parser/block/mathblock.ts +8 -6
- package/src/parser/block/mediablock.ts +5 -8
- package/src/parser/block/olist.ts +5 -7
- package/src/parser/block/paragraph.test.ts +4 -5
- package/src/parser/block/reply/cite.test.ts +8 -10
- package/src/parser/block/reply/cite.ts +20 -11
- package/src/parser/block/reply/quote.test.ts +18 -17
- package/src/parser/block/reply/quote.ts +16 -30
- package/src/parser/block/reply.test.ts +14 -1
- package/src/parser/block/reply.ts +12 -24
- package/src/parser/block/sidefence.test.ts +2 -0
- package/src/parser/block/sidefence.ts +4 -5
- package/src/parser/block/table.ts +2 -3
- package/src/parser/block/ulist.ts +3 -17
- package/src/parser/header.ts +3 -4
- package/src/parser/inline/autolink/account.ts +2 -1
- package/src/parser/inline/autolink/anchor.ts +2 -1
- package/src/parser/inline/autolink/hashnum.ts +2 -1
- package/src/parser/inline/autolink/hashtag.ts +2 -1
- package/src/parser/inline/autolink/url.ts +4 -2
- package/src/parser/inline/extension/placeholder.ts +2 -3
- package/src/parser/inline/html.ts +21 -23
- package/src/parser/inline/htmlentity.ts +2 -3
- package/src/parser/inline/link.test.ts +4 -0
- package/src/parser/inline/link.ts +3 -5
- package/src/parser/inline/math.ts +3 -3
- package/src/parser/inline/media.test.ts +17 -13
- package/src/parser/inline/media.ts +14 -8
- package/src/parser/inline/reference.ts +2 -6
- package/src/parser/inline/ruby.ts +2 -3
- package/src/parser/inline/shortmedia.ts +4 -2
- package/src/parser/inline/template.ts +1 -1
- package/src/parser/inline.test.ts +2 -0
- package/src/parser/source/str.ts +2 -1
- package/src/parser/util.ts +18 -3
- package/src/parser/visibility.ts +4 -2
- package/src/util/quote.test.ts +2 -2
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { UListParser } from '../block';
|
|
2
|
-
import { Parser } from '../../combinator/data/parser';
|
|
3
2
|
import { Recursion } from '../context';
|
|
4
|
-
import { union, inits, subsequence, some, recursion, block, line, validate, indent, focus,
|
|
3
|
+
import { union, inits, subsequence, some, recursion, block, line, validate, indent, focus, open, trim, fallback, lazy, fmap } from '../../combinator';
|
|
5
4
|
import { olist_ } from './olist';
|
|
6
|
-
import { ilist_ } from './ilist';
|
|
5
|
+
import { ilist_, ilistitem } from './ilist';
|
|
7
6
|
import { inline, indexer, indexee, dataindex } from '../inline';
|
|
8
|
-
import { contentline } from '../source';
|
|
9
7
|
import { lineable } from '../util';
|
|
10
8
|
import { visualize, trimBlank } from '../visibility';
|
|
11
9
|
import { unshift } from 'spica/array';
|
|
@@ -26,7 +24,7 @@ export const ulist_: UListParser = lazy(() => block(fmap(validate(
|
|
|
26
24
|
]), true)),
|
|
27
25
|
indent(union([ulist_, olist_, ilist_])),
|
|
28
26
|
]),
|
|
29
|
-
|
|
27
|
+
ilistitem),
|
|
30
28
|
ns => [html('li', { 'data-index': dataindex(ns) }, defrag(fillFirstLine(ns)))])),
|
|
31
29
|
])))),
|
|
32
30
|
es => [format(html('ul', es))])));
|
|
@@ -37,18 +35,6 @@ export const checkbox = focus(
|
|
|
37
35
|
html('span', { class: 'checkbox' }, source[1].trimStart() ? '☑' : '☐'),
|
|
38
36
|
], ''], false);
|
|
39
37
|
|
|
40
|
-
export const invalid = rewrite(
|
|
41
|
-
inits([contentline, indent<Parser<string>>(({ source }) => [[source], ''])]),
|
|
42
|
-
({ source }) => [[
|
|
43
|
-
'',
|
|
44
|
-
html('span', {
|
|
45
|
-
class: 'invalid',
|
|
46
|
-
'data-invalid-syntax': 'list',
|
|
47
|
-
'data-invalid-type': 'syntax',
|
|
48
|
-
'data-invalid-message': 'Fix the indent or the head of the list item',
|
|
49
|
-
}, source.replace('\n', ''))
|
|
50
|
-
], '']);
|
|
51
|
-
|
|
52
38
|
export function fillFirstLine(ns: (HTMLElement | string)[]): (HTMLElement | string)[] {
|
|
53
39
|
return ns.length === 1
|
|
54
40
|
&& typeof ns[0] === 'object'
|
package/src/parser/header.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { MarkdownParser } from '../../markdown';
|
|
|
2
2
|
import { union, inits, some, block, line, validate, focus, rewrite, clear, convert, lazy, fmap } from '../combinator';
|
|
3
3
|
import { segment } from './segment';
|
|
4
4
|
import { str } from './source';
|
|
5
|
+
import { invalid } from './util';
|
|
5
6
|
import { normalize } from './api/normalize';
|
|
6
7
|
import { html, defrag } from 'typed-dom/dom';
|
|
7
8
|
|
|
@@ -26,14 +27,12 @@ export const header: MarkdownParser.HeaderParser = lazy(() => validate(
|
|
|
26
27
|
...es,
|
|
27
28
|
])),
|
|
28
29
|
]),
|
|
29
|
-
])), false)),
|
|
30
|
+
]), false), false)),
|
|
30
31
|
({ source }) => [[
|
|
31
32
|
html('pre', {
|
|
32
33
|
class: 'invalid',
|
|
33
34
|
translate: 'no',
|
|
34
|
-
'
|
|
35
|
-
'data-invalid-type': 'syntax',
|
|
36
|
-
'data-invalid-message': 'Invalid syntax',
|
|
35
|
+
...invalid('header', 'syntax', 'Invalid syntax'),
|
|
37
36
|
}, normalize(source)),
|
|
38
37
|
], ''],
|
|
39
38
|
]))),
|
|
@@ -21,7 +21,8 @@ export const account: AutolinkParser.AccountParser = lazy(() => rewrite(
|
|
|
21
21
|
? `https://${source.slice(1).replace('/', '/@')}`
|
|
22
22
|
: `/${source}`
|
|
23
23
|
} }`,
|
|
24
|
-
unsafelink
|
|
24
|
+
unsafelink,
|
|
25
|
+
false),
|
|
25
26
|
([el]) => [define(el, { class: 'account' })]))),
|
|
26
27
|
({ source }) => [[source], ''],
|
|
27
28
|
])));
|
|
@@ -24,7 +24,8 @@ export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>',
|
|
|
24
24
|
? `/@${source.slice(2).replace('/', '/timeline?at=')}`
|
|
25
25
|
: `?at=${source.slice(2)}`
|
|
26
26
|
} }`,
|
|
27
|
-
unsafelink
|
|
27
|
+
unsafelink,
|
|
28
|
+
false),
|
|
28
29
|
([el]) => [define(el, { class: 'anchor' })]))),
|
|
29
30
|
({ source }) => [[source], ''],
|
|
30
31
|
]))));
|
|
@@ -11,7 +11,8 @@ export const hashnum: AutolinkParser.HashnumParser = lazy(() => rewrite(
|
|
|
11
11
|
union([
|
|
12
12
|
constraint(State.autolink, false, state(State.autolink, fmap(convert(
|
|
13
13
|
source => `[${source}]{ ${source.slice(1)} }`,
|
|
14
|
-
unsafelink
|
|
14
|
+
unsafelink,
|
|
15
|
+
false),
|
|
15
16
|
([el]) => [define(el, { class: 'hashnum', href: null })]))),
|
|
16
17
|
({ source }) => [[source], ''],
|
|
17
18
|
])));
|
|
@@ -20,7 +20,8 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => rewrite(
|
|
|
20
20
|
union([
|
|
21
21
|
constraint(State.autolink, false, state(State.autolink, fmap(convert(
|
|
22
22
|
source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`,
|
|
23
|
-
unsafelink
|
|
23
|
+
unsafelink,
|
|
24
|
+
false),
|
|
24
25
|
([el]) => [define(el, { class: 'hashtag' })]))),
|
|
25
26
|
({ source }) => [[source], ''],
|
|
26
27
|
])));
|
|
@@ -16,7 +16,8 @@ export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'ht
|
|
|
16
16
|
union([
|
|
17
17
|
constraint(State.autolink, false, state(State.autolink, convert(
|
|
18
18
|
url => `{ ${url} }`,
|
|
19
|
-
unsafelink
|
|
19
|
+
unsafelink,
|
|
20
|
+
false))),
|
|
20
21
|
({ source }) => [[source], ''],
|
|
21
22
|
]))));
|
|
22
23
|
|
|
@@ -29,7 +30,8 @@ export const lineurl: AutolinkParser.UrlParser.LineUrlParser = lazy(() => open(
|
|
|
29
30
|
union([
|
|
30
31
|
constraint(State.autolink, false, state(State.autolink, convert(
|
|
31
32
|
url => `{ ${url} }`,
|
|
32
|
-
unsafelink
|
|
33
|
+
unsafelink,
|
|
34
|
+
false))),
|
|
33
35
|
({ source }) => [[source], ''],
|
|
34
36
|
]),
|
|
35
37
|
]))));
|
|
@@ -4,6 +4,7 @@ import { union, some, recursion, precedence, surround, lazy } from '../../../com
|
|
|
4
4
|
import { inline } from '../../inline';
|
|
5
5
|
import { str } from '../../source';
|
|
6
6
|
import { tightStart } from '../../visibility';
|
|
7
|
+
import { invalid } from '../../util';
|
|
7
8
|
import { unshift } from 'spica/array';
|
|
8
9
|
import { html, defrag } from 'typed-dom/dom';
|
|
9
10
|
|
|
@@ -19,9 +20,7 @@ export const placeholder: ExtensionParser.PlaceholderParser = lazy(() => surroun
|
|
|
19
20
|
([, bs], rest) => [[
|
|
20
21
|
html('span', {
|
|
21
22
|
class: 'invalid',
|
|
22
|
-
|
|
23
|
-
'data-invalid-type': 'syntax',
|
|
24
|
-
'data-invalid-message': `Invalid start symbol or linebreak`,
|
|
23
|
+
...invalid('extension', 'syntax', `Invalid start symbol or linebreak`),
|
|
25
24
|
}, defrag(bs)),
|
|
26
25
|
], rest],
|
|
27
26
|
([as, bs], rest) => [unshift(as, bs), rest], [3 | Backtrack.bracket]));
|
|
@@ -4,6 +4,7 @@ import { union, subsequence, some, recursion, precedence, validate, focus, surro
|
|
|
4
4
|
import { inline } from '../inline';
|
|
5
5
|
import { str } from '../source';
|
|
6
6
|
import { isLooseNodeStart, blankWith } from '../visibility';
|
|
7
|
+
import { invalid } from '../util';
|
|
7
8
|
import { memoize } from 'spica/memoize';
|
|
8
9
|
import { Clock } from 'spica/clock';
|
|
9
10
|
import { unshift, push, splice } from 'spica/array';
|
|
@@ -216,23 +217,20 @@ const TAGS = Object.freeze([
|
|
|
216
217
|
function elem(tag: string, as: string[], bs: (HTMLElement | string)[], cs: string[]): HTMLElement {
|
|
217
218
|
assert(as.length > 0);
|
|
218
219
|
assert(as[0][0] === '<' && as.at(-1)!.slice(-1) === '>');
|
|
219
|
-
if (!tags.includes(tag)) return
|
|
220
|
-
if (cs.length === 0) return
|
|
221
|
-
if (bs.length === 0) return
|
|
222
|
-
if (!isLooseNodeStart(bs)) return
|
|
220
|
+
if (!tags.includes(tag)) return ielem('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
|
|
221
|
+
if (cs.length === 0) return ielem('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
|
|
222
|
+
if (bs.length === 0) return ielem('content', `Missing the content`, as, bs, cs);
|
|
223
|
+
if (!isLooseNodeStart(bs)) return ielem('content', `Missing the visible content in the same line`, as, bs, cs);
|
|
223
224
|
const attrs = attributes('html', [], attrspecs[tag], as.slice(1, -1));
|
|
224
|
-
return
|
|
225
|
-
?
|
|
225
|
+
return /(?<!\S)invalid(?!\S)/.test(attrs['class'] ?? '')
|
|
226
|
+
? ielem('attribute', 'Invalid HTML attribute', as, bs, cs)
|
|
226
227
|
: h(tag as 'span', attrs, defrag(bs));
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
function
|
|
230
|
-
return h('span',
|
|
231
|
-
class: 'invalid',
|
|
232
|
-
|
|
233
|
-
'data-invalid-type': type,
|
|
234
|
-
'data-invalid-message': message,
|
|
235
|
-
}, defrag(push(unshift(as, bs), cs)));
|
|
230
|
+
function ielem(type: string, message: string, as: (HTMLElement | string)[], bs: (HTMLElement | string)[], cs: (HTMLElement | string)[]): HTMLElement {
|
|
231
|
+
return h('span',
|
|
232
|
+
{ class: 'invalid', ...invalid('html', type, message) },
|
|
233
|
+
defrag(push(unshift(as, bs), cs)));
|
|
236
234
|
}
|
|
237
235
|
|
|
238
236
|
const requiredAttributes = memoize(
|
|
@@ -249,7 +247,7 @@ export function attributes(
|
|
|
249
247
|
assert(spec instanceof Object === false);
|
|
250
248
|
assert(!spec?.['__proto__']);
|
|
251
249
|
assert(!spec?.toString);
|
|
252
|
-
let
|
|
250
|
+
let invalidation = false;
|
|
253
251
|
const attrs: Record<string, string | undefined> = {};
|
|
254
252
|
for (let i = 0; i < params.length; ++i) {
|
|
255
253
|
const param = params[i].trim();
|
|
@@ -257,20 +255,20 @@ export function attributes(
|
|
|
257
255
|
const value = param !== name
|
|
258
256
|
? param.slice(name.length + 2, -1).replace(/\\(.?)/g, '$1')
|
|
259
257
|
: undefined;
|
|
260
|
-
|
|
258
|
+
invalidation ||= !spec || name in attrs;
|
|
261
259
|
if (spec && name in spec && !spec[name]) continue;
|
|
262
|
-
spec?.[name]?.includes(value) ||
|
|
260
|
+
spec?.[name]?.includes(value) || spec?.[name]?.length === 0 && value !== undefined
|
|
263
261
|
? attrs[name] = value ?? ''
|
|
264
|
-
:
|
|
262
|
+
: invalidation ||= !!spec;
|
|
265
263
|
assert(!(name in {} && attrs.hasOwnProperty(name)));
|
|
266
264
|
splice(params, i--, 1);
|
|
267
265
|
}
|
|
268
|
-
|
|
269
|
-
if (
|
|
270
|
-
attrs['class'] =
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
attrs
|
|
266
|
+
invalidation ||= !!spec && !requiredAttributes(spec).every(name => name in attrs);
|
|
267
|
+
if (invalidation) {
|
|
268
|
+
attrs['class'] = classes.length === 0
|
|
269
|
+
? 'invalid'
|
|
270
|
+
: `${classes.join(' ')}${classes.includes('invalid') ? '' : ' invalid'}`;
|
|
271
|
+
Object.assign(attrs, invalid(syntax, 'argument', 'Invalid argument'));
|
|
274
272
|
}
|
|
275
273
|
return attrs;
|
|
276
274
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HTMLEntityParser, UnsafeHTMLEntityParser } from '../inline';
|
|
2
2
|
import { Command } from '../context';
|
|
3
3
|
import { union, validate, focus, fmap } from '../../combinator';
|
|
4
|
+
import { invalid } from '../util';
|
|
4
5
|
import { html } from 'typed-dom/dom';
|
|
5
6
|
import { reduce } from 'spica/memoize';
|
|
6
7
|
|
|
@@ -14,9 +15,7 @@ export const htmlentity: HTMLEntityParser = fmap(
|
|
|
14
15
|
text[0] === Command.Escape
|
|
15
16
|
? html('span', {
|
|
16
17
|
class: 'invalid',
|
|
17
|
-
'
|
|
18
|
-
'data-invalid-type': 'syntax',
|
|
19
|
-
'data-invalid-message': 'Invalid HTML entity',
|
|
18
|
+
...invalid('htmlentity', 'syntax', 'Invalid HTML entity'),
|
|
20
19
|
}, text.slice(1))
|
|
21
20
|
: text,
|
|
22
21
|
]);
|
|
@@ -64,6 +64,8 @@ describe('Unit: parser/inline/link', () => {
|
|
|
64
64
|
assert.deepStrictEqual(inspect(parser('[]{ }')), undefined);
|
|
65
65
|
assert.deepStrictEqual(inspect(parser('[]{ }')), undefined);
|
|
66
66
|
assert.deepStrictEqual(inspect(parser('[]{{}')), undefined);
|
|
67
|
+
assert.deepStrictEqual(inspect(parser('[]{}}')), undefined);
|
|
68
|
+
assert.deepStrictEqual(inspect(parser('[]{{}}')), undefined);
|
|
67
69
|
assert.deepStrictEqual(inspect(parser('[]{{b}}')), undefined);
|
|
68
70
|
assert.deepStrictEqual(inspect(parser('[]{b\nb}')), undefined);
|
|
69
71
|
assert.deepStrictEqual(inspect(parser('[]{b\\\nb}')), undefined);
|
|
@@ -107,6 +109,8 @@ describe('Unit: parser/inline/link', () => {
|
|
|
107
109
|
assert.deepStrictEqual(inspect(parser('[]{ b }')), [['<a class="url" href="b">b</a>'], '']);
|
|
108
110
|
assert.deepStrictEqual(inspect(parser('[]{ b }')), [['<a class="url" href="b">b</a>'], '']);
|
|
109
111
|
assert.deepStrictEqual(inspect(parser('[]{ b }')), [['<a class="url" href="b">b</a>'], '']);
|
|
112
|
+
assert.deepStrictEqual(inspect(parser('[]{"}')), [[`<a class="url" href=""">"</a>`], '']);
|
|
113
|
+
assert.deepStrictEqual(inspect(parser('[]{"}"}')), [[`<a class="url" href=""">"</a>`], '"}']);
|
|
110
114
|
assert.deepStrictEqual(inspect(parser('[]{\\}')), [[`<a class="url" href="\\">\\</a>`], '']);
|
|
111
115
|
assert.deepStrictEqual(inspect(parser('[]{\\ }')), [[`<a class="url" href="\\">\\</a>`], '']);
|
|
112
116
|
assert.deepStrictEqual(inspect(parser('[]{\\b}')), [[`<a class="url" href="\\b">\\b</a>`], '']);
|
|
@@ -6,7 +6,7 @@ import { inline, media, shortmedia } from '../inline';
|
|
|
6
6
|
import { attributes } from './html';
|
|
7
7
|
import { linebreak, unescsource, str } from '../source';
|
|
8
8
|
import { trimBlankStart, trimBlankNodeEnd } from '../visibility';
|
|
9
|
-
import { stringify } from '../util';
|
|
9
|
+
import { invalid, stringify } from '../util';
|
|
10
10
|
import { ReadonlyURL } from 'spica/url';
|
|
11
11
|
import { html, define, defrag } from 'typed-dom/dom';
|
|
12
12
|
|
|
@@ -94,7 +94,7 @@ function parse(
|
|
|
94
94
|
content,
|
|
95
95
|
uri,
|
|
96
96
|
context.host?.origin || location.origin);
|
|
97
|
-
return el.
|
|
97
|
+
return el.classList.contains('invalid')
|
|
98
98
|
? el
|
|
99
99
|
: define(el, attributes('link', [], optspec, params));
|
|
100
100
|
}
|
|
@@ -166,9 +166,7 @@ function elem(
|
|
|
166
166
|
return html('a',
|
|
167
167
|
{
|
|
168
168
|
class: 'invalid',
|
|
169
|
-
|
|
170
|
-
'data-invalid-type': type ??= 'argument',
|
|
171
|
-
'data-invalid-message': message ??= 'Invalid protocol',
|
|
169
|
+
...invalid('link', type ??= 'argument', message ??= 'Invalid protocol'),
|
|
172
170
|
},
|
|
173
171
|
content.length === 0
|
|
174
172
|
? INSECURE_URI
|
|
@@ -2,6 +2,7 @@ import { MathParser } from '../inline';
|
|
|
2
2
|
import { Recursion } from '../context';
|
|
3
3
|
import { union, some, recursion, precedence, validate, focus, rewrite, surround, lazy } from '../../combinator';
|
|
4
4
|
import { escsource, unescsource, str } from '../source';
|
|
5
|
+
import { invalid } from '../util';
|
|
5
6
|
import { html } from 'typed-dom/dom';
|
|
6
7
|
|
|
7
8
|
const forbiddenCommand = /\\(?:begin|tiny|huge|large)(?![a-z])/i;
|
|
@@ -25,9 +26,8 @@ export const math: MathParser = lazy(() => validate('$', rewrite(
|
|
|
25
26
|
: {
|
|
26
27
|
class: 'invalid',
|
|
27
28
|
translate: 'no',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'data-invalid-message': `"${source.match(forbiddenCommand)![0]}" command is forbidden`,
|
|
29
|
+
...invalid('math', 'content',
|
|
30
|
+
`"${source.match(forbiddenCommand)![0]}" command is forbidden`),
|
|
31
31
|
},
|
|
32
32
|
source)
|
|
33
33
|
], ''])));
|
|
@@ -7,10 +7,10 @@ describe('Unit: parser/inline/media', () => {
|
|
|
7
7
|
const parser = (source: string) => some(media)({ source, context: {} });
|
|
8
8
|
|
|
9
9
|
it('xss', () => {
|
|
10
|
-
assert.deepStrictEqual(inspect(parser('![]{javascript:alert}')), [['<img class="
|
|
11
|
-
assert.deepStrictEqual(inspect(parser('![]{vbscript:alert}')), [['<img class="
|
|
12
|
-
assert.deepStrictEqual(inspect(parser('![]{data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K}')), [['<img class="
|
|
13
|
-
assert.deepStrictEqual(inspect(parser('![]{any:alert}')), [['<img class="
|
|
10
|
+
assert.deepStrictEqual(inspect(parser('![]{javascript:alert}')), [['<img class="invalid" data-src="javascript:alert" alt="">'], '']);
|
|
11
|
+
assert.deepStrictEqual(inspect(parser('![]{vbscript:alert}')), [['<img class="invalid" data-src="vbscript:alert" alt="">'], '']);
|
|
12
|
+
assert.deepStrictEqual(inspect(parser('![]{data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K}')), [['<img class="invalid" data-src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K" alt="">'], '']);
|
|
13
|
+
assert.deepStrictEqual(inspect(parser('![]{any:alert}')), [['<img class="invalid" data-src="any:alert" alt="">'], '']);
|
|
14
14
|
assert.deepStrictEqual(inspect(parser('![]{"}')), [['<a href=""" target="_blank"><img class="media" data-src=""" alt=""></a>'], '']);
|
|
15
15
|
assert.deepStrictEqual(inspect(parser('![]{\\}')), [['<a href="\\" target="_blank"><img class="media" data-src="\\" alt=""></a>'], '']);
|
|
16
16
|
assert.deepStrictEqual(inspect(parser('![\\"]{/}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="""></a>'], '']);
|
|
@@ -30,6 +30,8 @@ describe('Unit: parser/inline/media', () => {
|
|
|
30
30
|
assert.deepStrictEqual(inspect(parser('![]{ }')), undefined);
|
|
31
31
|
assert.deepStrictEqual(inspect(parser('![]]{/}')), undefined);
|
|
32
32
|
assert.deepStrictEqual(inspect(parser('![]{{}')), undefined);
|
|
33
|
+
assert.deepStrictEqual(inspect(parser('![]{}}')), undefined);
|
|
34
|
+
assert.deepStrictEqual(inspect(parser('![]{{}}')), undefined);
|
|
33
35
|
assert.deepStrictEqual(inspect(parser('![]{{b}}')), undefined);
|
|
34
36
|
assert.deepStrictEqual(inspect(parser('![]{b\nc}')), undefined);
|
|
35
37
|
assert.deepStrictEqual(inspect(parser('![]{a\\\nc}')), undefined);
|
|
@@ -42,20 +44,20 @@ describe('Unit: parser/inline/media', () => {
|
|
|
42
44
|
assert.deepStrictEqual(inspect(parser('![\\ ]{b}')), undefined);
|
|
43
45
|
assert.deepStrictEqual(inspect(parser('![\\\n]{b}')), undefined);
|
|
44
46
|
assert.deepStrictEqual(inspect(parser('![	]{b}')), undefined);
|
|
45
|
-
assert.deepStrictEqual(inspect(parser('![&a;]{b}')), [['<img class="
|
|
47
|
+
assert.deepStrictEqual(inspect(parser('![&a;]{b}')), [['<img class="invalid" data-src="b" alt="&a;">'], '']);
|
|
46
48
|
assert.deepStrictEqual(inspect(parser('![[]{b}')), undefined);
|
|
47
49
|
assert.deepStrictEqual(inspect(parser('![]]{b}')), undefined);
|
|
48
50
|
assert.deepStrictEqual(inspect(parser('![a]{}')), undefined);
|
|
49
51
|
assert.deepStrictEqual(inspect(parser('![a\nb]{b}')), undefined);
|
|
50
52
|
assert.deepStrictEqual(inspect(parser('![a\\\nb]{b}')), undefined);
|
|
51
|
-
assert.deepStrictEqual(inspect(parser('![]{ttp://host}')), [['<img class="
|
|
52
|
-
assert.deepStrictEqual(inspect(parser('![]{tel:1234567890}')), [['<img class="
|
|
53
|
-
//assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0%1]}')), [['<img class="
|
|
54
|
-
//assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0/96]}')), [['<img class="
|
|
55
|
-
assert.deepStrictEqual(inspect(parser('![]{.}')), [['<img class="
|
|
56
|
-
assert.deepStrictEqual(inspect(parser('![]{..}')), [['<img class="
|
|
57
|
-
assert.deepStrictEqual(inspect(parser('![]{../}')), [['<img class="
|
|
58
|
-
assert.deepStrictEqual(inspect(parser('![]{/../b}')), [['<img class="
|
|
53
|
+
assert.deepStrictEqual(inspect(parser('![]{ttp://host}')), [['<img class="invalid" data-src="ttp://host" alt="">'], '']);
|
|
54
|
+
assert.deepStrictEqual(inspect(parser('![]{tel:1234567890}')), [['<img class="invalid" data-src="tel:1234567890" alt="">'], '']);
|
|
55
|
+
//assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0%1]}')), [['<img class="invalid" alt="">'], '']);
|
|
56
|
+
//assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0/96]}')), [['<img class="invalid" alt="">'], '']);
|
|
57
|
+
assert.deepStrictEqual(inspect(parser('![]{.}')), [['<img class="invalid" data-src="." alt="">'], '']);
|
|
58
|
+
assert.deepStrictEqual(inspect(parser('![]{..}')), [['<img class="invalid" data-src=".." alt="">'], '']);
|
|
59
|
+
assert.deepStrictEqual(inspect(parser('![]{../}')), [['<img class="invalid" data-src="../" alt="">'], '']);
|
|
60
|
+
assert.deepStrictEqual(inspect(parser('![]{/../b}')), [['<img class="invalid" data-src="/../b" alt="">'], '']);
|
|
59
61
|
assert.deepStrictEqual(inspect(parser(' ![]{b}')), undefined);
|
|
60
62
|
assert.deepStrictEqual(inspect(parser('[]{/}')), undefined);
|
|
61
63
|
});
|
|
@@ -69,6 +71,8 @@ describe('Unit: parser/inline/media', () => {
|
|
|
69
71
|
assert.deepStrictEqual(inspect(parser('![]{ b }')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt=""></a>'], '']);
|
|
70
72
|
assert.deepStrictEqual(inspect(parser('![]{ b }')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt=""></a>'], '']);
|
|
71
73
|
assert.deepStrictEqual(inspect(parser('![]{ b }')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt=""></a>'], '']);
|
|
74
|
+
assert.deepStrictEqual(inspect(parser('![]{"}')), [['<a href=""" target="_blank"><img class="media" data-src=""" alt=""></a>'], '']);
|
|
75
|
+
assert.deepStrictEqual(inspect(parser('![]{"}"}')), [['<a href=""" target="_blank"><img class="media" data-src=""" alt=""></a>'], '"}']);
|
|
72
76
|
assert.deepStrictEqual(inspect(parser('![]{\\}')), [['<a href="\\" target="_blank"><img class="media" data-src="\\" alt=""></a>'], '']);
|
|
73
77
|
assert.deepStrictEqual(inspect(parser('![]{\\ }')), [['<a href="\\" target="_blank"><img class="media" data-src="\\" alt=""></a>'], '']);
|
|
74
78
|
assert.deepStrictEqual(inspect(parser('![]{\\b}')), [['<a href="\\b" target="_blank"><img class="media" data-src="\\b" alt=""></a>'], '']);
|
|
@@ -5,7 +5,7 @@ import { unsafelink, uri, option as linkoption, resolve } from './link';
|
|
|
5
5
|
import { attributes } from './html';
|
|
6
6
|
import { unsafehtmlentity } from './htmlentity';
|
|
7
7
|
import { txt, linebreak, str } from '../source';
|
|
8
|
-
import {
|
|
8
|
+
import { invalid } from '../util';
|
|
9
9
|
import { ReadonlyURL } from 'spica/url';
|
|
10
10
|
import { push } from 'spica/array';
|
|
11
11
|
import { html, define } from 'typed-dom/dom';
|
|
@@ -51,7 +51,7 @@ export const media: MediaParser = lazy(() => constraint(State.media, false, vali
|
|
|
51
51
|
|| (cache = context.caches?.media?.get(url.href)?.cloneNode(true))
|
|
52
52
|
|| html('img', { class: 'media', 'data-src': url.source, alt: text });
|
|
53
53
|
assert(!el.matches('.invalid'));
|
|
54
|
-
cache?.hasAttribute('alt') && cache
|
|
54
|
+
cache?.hasAttribute('alt') && cache.setAttribute('alt', text);
|
|
55
55
|
if (!sanitize(el, url, text)) return [[el], rest];
|
|
56
56
|
assert(!el.matches('.invalid'));
|
|
57
57
|
define(el, attributes('media', push([], el.classList), optspec, params));
|
|
@@ -98,19 +98,25 @@ function sanitize(target: HTMLElement, uri: ReadonlyURL, alt: string): boolean {
|
|
|
98
98
|
case 'https:':
|
|
99
99
|
assert(uri.host);
|
|
100
100
|
if (/\/\.\.?(?:\/|$)/.test('/' + uri.source.slice(0, uri.source.search(/[?#]|$/)))) {
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
define(target, {
|
|
102
|
+
class: 'invalid',
|
|
103
|
+
...invalid('media', 'argument',
|
|
104
|
+
'Dot-segments cannot be used in media paths; use subresource paths instead')
|
|
105
|
+
});
|
|
103
106
|
return false;
|
|
104
107
|
}
|
|
105
108
|
break;
|
|
106
109
|
default:
|
|
107
|
-
|
|
110
|
+
define(target, { class: 'invalid', ...invalid('media', 'argument', 'Invalid protocol') });
|
|
108
111
|
return false;
|
|
109
112
|
}
|
|
110
113
|
if (alt.includes(Command.Escape)) {
|
|
111
|
-
define(target, {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
define(target, {
|
|
115
|
+
class: 'invalid',
|
|
116
|
+
alt: target.getAttribute('alt')?.replace(CmdRegExp.Escape, ''),
|
|
117
|
+
...invalid('media', 'argument',
|
|
118
|
+
`Cannot use invalid HTML entitiy "${alt.match(/&[0-9A-Za-z]+;/)![0]}"`)
|
|
119
|
+
});
|
|
114
120
|
return false;
|
|
115
121
|
}
|
|
116
122
|
return true;
|
|
@@ -6,6 +6,7 @@ import { str } from '../source';
|
|
|
6
6
|
import { blank, trimBlankStart, trimBlankNodeEnd } from '../visibility';
|
|
7
7
|
import { html, defrag } from 'typed-dom/dom';
|
|
8
8
|
import { unshift } from 'spica/array';
|
|
9
|
+
import { invalid } from '../util';
|
|
9
10
|
|
|
10
11
|
export const reference: ReferenceParser = lazy(() => constraint(State.reference, false, surround(
|
|
11
12
|
'[[',
|
|
@@ -38,12 +39,7 @@ const abbr: ReferenceParser.AbbrParser = surround(
|
|
|
38
39
|
function attributes(ns: (string | HTMLElement)[]): Record<string, string | undefined> {
|
|
39
40
|
switch (ns[0]) {
|
|
40
41
|
case '':
|
|
41
|
-
return {
|
|
42
|
-
class: 'invalid',
|
|
43
|
-
'data-invalid-syntax': 'reference',
|
|
44
|
-
'data-invalid-type': 'syntax',
|
|
45
|
-
'data-invalid-message': 'Invalid abbreviation',
|
|
46
|
-
};
|
|
42
|
+
return { class: 'invalid', ...invalid('reference', 'syntax', 'Invalid abbreviation') };
|
|
47
43
|
case '\n':
|
|
48
44
|
const abbr = ns[1] as string;
|
|
49
45
|
ns[0] = ns[1] = '';
|
|
@@ -5,6 +5,7 @@ import { sequence, surround, dup, lazy, fmap } from '../../combinator';
|
|
|
5
5
|
import { unsafehtmlentity } from './htmlentity';
|
|
6
6
|
import { text as txt, str } from '../source';
|
|
7
7
|
import { isTightNodeStart } from '../visibility';
|
|
8
|
+
import { invalid } from '../util';
|
|
8
9
|
import { unshift, push } from 'spica/array';
|
|
9
10
|
import { html, defrag } from 'typed-dom/dom';
|
|
10
11
|
|
|
@@ -104,9 +105,7 @@ function attributes(texts: string[], rubies: string[]): Record<string, string> {
|
|
|
104
105
|
ss[i] = ss[i].replace(CmdRegExp.Escape, '');
|
|
105
106
|
attrs ??= {
|
|
106
107
|
class: 'invalid',
|
|
107
|
-
|
|
108
|
-
'data-invalid-type': ss === texts ? 'content' : 'argument',
|
|
109
|
-
'data-invalid-message': 'Invalid HTML entity',
|
|
108
|
+
...invalid('ruby', ss === texts ? 'content' : 'argument', 'Invalid HTML entity'),
|
|
110
109
|
};
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -9,7 +9,8 @@ export const shortmedia: ShortMediaParser = constraint(State.media, false, rewri
|
|
|
9
9
|
open('!', url),
|
|
10
10
|
convert(
|
|
11
11
|
source => `!{ ${source.slice(1)} }`,
|
|
12
|
-
union([media])
|
|
12
|
+
union([media]),
|
|
13
|
+
false)));
|
|
13
14
|
|
|
14
15
|
export const lineshortmedia: ShortMediaParser.LineShortMediaParser = open(
|
|
15
16
|
linebreak,
|
|
@@ -17,4 +18,5 @@ export const lineshortmedia: ShortMediaParser.LineShortMediaParser = open(
|
|
|
17
18
|
/^!https?:\/\/\S+(?=[^\S\n]*(?:$|\n))/,
|
|
18
19
|
convert(
|
|
19
20
|
source => `!{ ${source.slice(1)} }`,
|
|
20
|
-
union([media])
|
|
21
|
+
union([media]),
|
|
22
|
+
false)));
|
|
@@ -11,7 +11,7 @@ export const template: TemplateParser = lazy(() => surround(
|
|
|
11
11
|
'}}',
|
|
12
12
|
true,
|
|
13
13
|
([, ns = []], rest) => [[html('span', { class: 'template' }, `{{${ns.join('')}}}`)], rest],
|
|
14
|
-
undefined, [3 | Backtrack.linedoublebracket, 1 | Backtrack.lineescbracket]
|
|
14
|
+
undefined, [3 | Backtrack.linedoublebracket, 1 | Backtrack.lineescbracket]));
|
|
15
15
|
|
|
16
16
|
const bracket: TemplateParser.BracketParser = lazy(() => union([
|
|
17
17
|
surround(str('('), recursion(Recursion.terminal, some(union([bracket, escsource]), ')')), str(')'), true,
|
|
@@ -169,6 +169,8 @@ describe('Unit: parser/inline', () => {
|
|
|
169
169
|
assert.deepStrictEqual(inspect(parser('_http://host_')), [['_', '<a class="url" href="http://host" target="_blank">http://host</a>', '_'], '']);
|
|
170
170
|
assert.deepStrictEqual(inspect(parser('*http://host*')), [['<em><a class="url" href="http://host" target="_blank">http://host</a></em>'], '']);
|
|
171
171
|
assert.deepStrictEqual(inspect(parser('(http://host)')), [['<span class="paren">(<a class="url" href="http://host" target="_blank">http://host</a>)</span>'], '']);
|
|
172
|
+
assert.deepStrictEqual(inspect(parser('"http://host"')), [['"', '<a class="url" href="http://host" target="_blank">http://host</a>', '"'], '']);
|
|
173
|
+
assert.deepStrictEqual(inspect(parser('"http://host""')), [['"', '<a class="url" href="http://host" target="_blank">http://host</a>', '"', '"'], '']);
|
|
172
174
|
assert.deepStrictEqual(inspect(parser(' http://host')), [[' ', '<a class="url" href="http://host" target="_blank">http://host</a>'], '']);
|
|
173
175
|
assert.deepStrictEqual(inspect(parser('あhttp://hostい')), [['あ', '<a class="url" href="http://host" target="_blank">http://host</a>', 'い'], '']);
|
|
174
176
|
});
|
package/src/parser/source/str.ts
CHANGED
|
@@ -21,7 +21,8 @@ export function str(pattern: string | RegExp, not?: string): Parser<string, Cont
|
|
|
21
21
|
const m = source.match(pattern);
|
|
22
22
|
count && m && consume(m[0].length, context);
|
|
23
23
|
if (m && not && source.slice(m[0].length, m[0].length + not.length) === not) return;
|
|
24
|
-
|
|
24
|
+
//assert(!m || m[0]);
|
|
25
|
+
return m
|
|
25
26
|
? [[m[0]], source.slice(m[0].length)]
|
|
26
27
|
: undefined;
|
|
27
28
|
};
|
package/src/parser/util.ts
CHANGED
|
@@ -4,9 +4,12 @@ import { Parser, Result, Ctx, Tree, Context, eval, exec } from '../combinator/da
|
|
|
4
4
|
import { convert } from '../combinator';
|
|
5
5
|
import { define } from 'typed-dom/dom';
|
|
6
6
|
|
|
7
|
-
export function lineable<P extends Parser<HTMLElement | string>>(parser: P): P;
|
|
8
|
-
export function lineable<T extends HTMLElement | string>(parser: Parser<T
|
|
9
|
-
return convert(
|
|
7
|
+
export function lineable<P extends Parser<HTMLElement | string>>(parser: P, fillTrailingLinebreak?: boolean): P;
|
|
8
|
+
export function lineable<T extends HTMLElement | string>(parser: Parser<T>, fillTrailingLinebreak = false): Parser<T> {
|
|
9
|
+
return convert(
|
|
10
|
+
source => `\r${source}${fillTrailingLinebreak && source.at(-1) !== '\n' ? '\n' : ''}`,
|
|
11
|
+
parser,
|
|
12
|
+
!fillTrailingLinebreak);
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export function repeat<P extends Parser<HTMLElement | string>>(symbol: string, parser: P, cons: (nodes: Tree<P>[], context: Context<P>) => Tree<P>[], termination?: (acc: Tree<P>[][], rest: string, prefix: number, postfix: number, state: boolean) => Result<string | Tree<P>>): P;
|
|
@@ -72,6 +75,18 @@ export function repeat<T extends HTMLElement | string>(symbol: string, parser: P
|
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
export function invalid(
|
|
79
|
+
syntax: string,
|
|
80
|
+
type: string,
|
|
81
|
+
message: string,
|
|
82
|
+
): Record<string, string> {
|
|
83
|
+
return {
|
|
84
|
+
'data-invalid-syntax': syntax,
|
|
85
|
+
'data-invalid-type': type,
|
|
86
|
+
'data-invalid-message': message,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
export function markInvalid<T extends Element>(
|
|
76
91
|
el: T,
|
|
77
92
|
syntax: string,
|
package/src/parser/visibility.ts
CHANGED
|
@@ -23,7 +23,8 @@ export function visualize<T extends HTMLElement | string>(parser: Parser<T>): Pa
|
|
|
23
23
|
return union([
|
|
24
24
|
convert(
|
|
25
25
|
source => source.replace(blank.line, line => line.replace(/[\\&<]/g, `${Command.Escape}$&`)),
|
|
26
|
-
verify(parser, (ns, rest) => !rest && hasVisible(ns))
|
|
26
|
+
verify(parser, (ns, rest) => !rest && hasVisible(ns)),
|
|
27
|
+
false),
|
|
27
28
|
some(union([linebreak, unescsource])),
|
|
28
29
|
]);
|
|
29
30
|
}
|
|
@@ -162,7 +163,8 @@ export function trimBlankStart<P extends Parser<unknown>>(parser: P): P;
|
|
|
162
163
|
export function trimBlankStart<T>(parser: Parser<T>): Parser<T> {
|
|
163
164
|
return convert(
|
|
164
165
|
source => source.replace(blank.start, ''),
|
|
165
|
-
parser
|
|
166
|
+
parser,
|
|
167
|
+
true);
|
|
166
168
|
}
|
|
167
169
|
export function trimBlankEnd<P extends Parser<HTMLElement | string>>(parser: P): P;
|
|
168
170
|
export function trimBlankEnd<T extends HTMLElement | string>(parser: Parser<T>): Parser<T> {
|
package/src/util/quote.test.ts
CHANGED
|
@@ -5,10 +5,10 @@ describe('Unit: util/quote', () => {
|
|
|
5
5
|
describe('quote', () => {
|
|
6
6
|
it('basic', () => {
|
|
7
7
|
const range = document.createRange();
|
|
8
|
-
const el = parse('>>1\n>
|
|
8
|
+
const el = parse('>>1\n> a\n>>?\n> 2\n>>4 `b` ${c}$\n [e](f) ').firstElementChild!;
|
|
9
9
|
range.setStart(el.firstChild!.firstChild!, 0);
|
|
10
10
|
range.setEnd(el.lastChild!.lastChild!.lastChild!, 1);
|
|
11
|
-
assert(quote('3', range)
|
|
11
|
+
assert.deepStrictEqual(quote('3', range), `>>>1\n>> a\n>>>?\n>> 2\n>>3\n> >>4 \`b\` \${c}$\n> e`);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
it('adjustment', () => {
|