securemark 0.288.0 → 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.
@@ -3,6 +3,7 @@ import { Recursion } from '../context';
3
3
  import { union, some, recursion, block, focus, rewrite, convert, lazy, fmap } from '../../combinator';
4
4
  import { autolink } from '../autolink';
5
5
  import { contentline } from '../source';
6
+ import { invalid } from '../util';
6
7
  import { html, define, defrag } from 'typed-dom/dom';
7
8
 
8
9
  export const sidefence: SidefenceParser = lazy(() => block(fmap(focus(
@@ -11,9 +12,7 @@ export const sidefence: SidefenceParser = lazy(() => block(fmap(focus(
11
12
  ([el]) => [
12
13
  define(el, {
13
14
  class: 'invalid',
14
- 'data-invalid-syntax': 'sidefence',
15
- 'data-invalid-type': 'syntax',
16
- 'data-invalid-message': 'Reserved syntax',
15
+ ...invalid('sidefence', 'syntax', 'Reserved syntax'),
17
16
  }),
18
17
  ])));
19
18
 
@@ -3,6 +3,7 @@ import { union, sequence, some, block, line, validate, focus, rewrite, surround,
3
3
  import { inline, media, medialink, shortmedia } from '../inline';
4
4
  import { contentline } from '../source';
5
5
  import { trimBlank } from '../visibility';
6
+ import { invalid } from '../util';
6
7
  import { duffReduce } from 'spica/duff';
7
8
  import { push } from 'spica/array';
8
9
  import { html, defrag } from 'typed-dom/dom';
@@ -31,9 +32,7 @@ const row = <P extends CellParser | AlignParser>(parser: P, optional: boolean):
31
32
  rewrite(contentline, ({ source }) => [[
32
33
  html('tr', {
33
34
  class: 'invalid',
34
- 'data-invalid-syntax': 'table-row',
35
- 'data-invalid-type': 'syntax',
36
- 'data-invalid-message': 'Missing the start symbol of the table row',
35
+ ...invalid('table-row', 'syntax', 'Missing the start symbol of the table row'),
37
36
  }, [html('td', source.replace('\n', ''))])
38
37
  ], '']));
39
38
 
@@ -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, rewrite, open, trim, fallback, lazy, fmap } from '../../combinator';
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
- invalid),
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'
@@ -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
 
@@ -31,9 +32,7 @@ export const header: MarkdownParser.HeaderParser = lazy(() => validate(
31
32
  html('pre', {
32
33
  class: 'invalid',
33
34
  translate: 'no',
34
- 'data-invalid-syntax': 'header',
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
  ]))),
@@ -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
- 'data-invalid-syntax': 'extension',
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 invalid('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
220
- if (cs.length === 0) return invalid('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
221
- if (bs.length === 0) return invalid('content', `Missing the content`, as, bs, cs);
222
- if (!isLooseNodeStart(bs)) return invalid('content', `Missing the visible content in the same line`, as, bs, cs);
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 'data-invalid-syntax' in attrs
225
- ? invalid('attribute', 'Invalid HTML attribute', as, bs, cs)
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 invalid(type: string, message: string, as: (HTMLElement | string)[], bs: (HTMLElement | string)[], cs: (HTMLElement | string)[]): HTMLElement {
230
- return h('span', {
231
- class: 'invalid',
232
- 'data-invalid-syntax': 'html',
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 invalid = false;
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
- invalid ||= !spec || name in attrs;
258
+ invalidation ||= !spec || name in attrs;
261
259
  if (spec && name in spec && !spec[name]) continue;
262
- spec?.[name]?.includes(value) || value !== undefined && spec?.[name]?.length === 0
260
+ spec?.[name]?.includes(value) || spec?.[name]?.length === 0 && value !== undefined
263
261
  ? attrs[name] = value ?? ''
264
- : invalid ||= !!spec;
262
+ : invalidation ||= !!spec;
265
263
  assert(!(name in {} && attrs.hasOwnProperty(name)));
266
264
  splice(params, i--, 1);
267
265
  }
268
- invalid ||= !!spec && !requiredAttributes(spec).every(name => name in attrs);
269
- if (invalid) {
270
- attrs['class'] = (classes.includes('invalid') ? classes : unshift(classes, ['invalid'])).join(' ');
271
- attrs['data-invalid-syntax'] = syntax;
272
- attrs['data-invalid-type'] = 'argument';
273
- attrs['data-invalid-message'] = 'Invalid argument';
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
- 'data-invalid-syntax': 'htmlentity',
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="&quot;">"</a>`], '']);
113
+ assert.deepStrictEqual(inspect(parser('[]{"}"}')), [[`<a class="url" href="&quot;">"</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.className === 'invalid'
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
- 'data-invalid-syntax': 'link',
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
- 'data-invalid-syntax': 'math',
29
- 'data-invalid-type': 'content',
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="media invalid" data-src="javascript:alert" alt="">'], '']);
11
- assert.deepStrictEqual(inspect(parser('![]{vbscript:alert}')), [['<img class="media invalid" data-src="vbscript:alert" alt="">'], '']);
12
- assert.deepStrictEqual(inspect(parser('![]{data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K}')), [['<img class="media invalid" data-src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K" alt="">'], '']);
13
- assert.deepStrictEqual(inspect(parser('![]{any:alert}')), [['<img class="media invalid" data-src="any:alert" alt="">'], '']);
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="&quot;" target="_blank"><img class="media" data-src="&quot;" 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="&quot;"></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('![&Tab;]{b}')), undefined);
45
- assert.deepStrictEqual(inspect(parser('![&a;]{b}')), [['<img class="media invalid" data-src="b" alt="&amp;a;">'], '']);
47
+ assert.deepStrictEqual(inspect(parser('![&a;]{b}')), [['<img class="invalid" data-src="b" alt="&amp;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="media invalid" data-src="ttp://host" alt="">'], '']);
52
- assert.deepStrictEqual(inspect(parser('![]{tel:1234567890}')), [['<img class="media invalid" data-src="tel:1234567890" alt="">'], '']);
53
- //assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0%1]}')), [['<img class="media invalid" alt="">'], '']);
54
- //assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0/96]}')), [['<img class="media invalid" alt="">'], '']);
55
- assert.deepStrictEqual(inspect(parser('![]{.}')), [['<img class="media invalid" data-src="." alt="">'], '']);
56
- assert.deepStrictEqual(inspect(parser('![]{..}')), [['<img class="media invalid" data-src=".." alt="">'], '']);
57
- assert.deepStrictEqual(inspect(parser('![]{../}')), [['<img class="media invalid" data-src="../" alt="">'], '']);
58
- assert.deepStrictEqual(inspect(parser('![]{/../b}')), [['<img class="media invalid" data-src="/../b" alt="">'], '']);
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="&quot;" target="_blank"><img class="media" data-src="&quot;" alt=""></a>'], '']);
75
+ assert.deepStrictEqual(inspect(parser('![]{"}"}')), [['<a href="&quot;" target="_blank"><img class="media" data-src="&quot;" 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 { markInvalid } from '../util';
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?.setAttribute('alt', text);
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
- markInvalid(target, 'media', 'argument',
102
- 'Dot-segments cannot be used in media paths; use subresource paths instead');
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
- markInvalid(target, 'media', 'argument', 'Invalid protocol');
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, { alt: target.getAttribute('alt')?.replace(CmdRegExp.Escape, '') });
112
- markInvalid(target, 'media', 'content',
113
- `Cannot use invalid HTML entitiy "${alt.match(/&[0-9A-Za-z]+;/)![0]}"`);
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
- 'data-invalid-syntax': 'ruby',
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
  }
@@ -75,6 +75,18 @@ export function repeat<T extends HTMLElement | string>(symbol: string, parser: P
75
75
  };
76
76
  }
77
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
+
78
90
  export function markInvalid<T extends Element>(
79
91
  el: T,
80
92
  syntax: string,