securemark 0.222.0 → 0.225.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.
@@ -1,10 +1,11 @@
1
1
  import { link } from './link';
2
2
  import { some } from '../../combinator';
3
3
  import { inspect } from '../../debug.test';
4
+ import { MarkdownParser } from '../../../markdown';
4
5
 
5
6
  describe('Unit: parser/inline/link', () => {
6
7
  describe('link', () => {
7
- const parser = (source: string) => some(link)(source, {});
8
+ const parser = (source: string, context: MarkdownParser.Context = {}) => some(link)(source, context);
8
9
 
9
10
  it('xss', () => {
10
11
  assert.deepStrictEqual(inspect(parser('[]{javascript:alert}')), [['<a class="invalid">javascript:alert</a>'], '']);
@@ -50,33 +51,40 @@ describe('Unit: parser/inline/link', () => {
50
51
  assert.deepStrictEqual(inspect(parser('[]{ }')), undefined);
51
52
  assert.deepStrictEqual(inspect(parser('[]{ }')), undefined);
52
53
  assert.deepStrictEqual(inspect(parser('[]{{}')), undefined);
53
- assert.deepStrictEqual(inspect(parser('[]{{a}}')), undefined);
54
- assert.deepStrictEqual(inspect(parser('[]{a\nb}')), undefined);
55
- assert.deepStrictEqual(inspect(parser('[]{a\\\nb}')), undefined);
56
- assert.deepStrictEqual(inspect(parser('[]{ a}')), undefined);
57
- assert.deepStrictEqual(inspect(parser('[]{ a\n}')), undefined);
54
+ assert.deepStrictEqual(inspect(parser('[]{{b}}')), undefined);
55
+ assert.deepStrictEqual(inspect(parser('[]{b\nb}')), undefined);
56
+ assert.deepStrictEqual(inspect(parser('[]{b\\\nb}')), undefined);
57
+ assert.deepStrictEqual(inspect(parser('[]{ b}')), undefined);
58
+ assert.deepStrictEqual(inspect(parser('[]{ b\n}')), undefined);
58
59
  assert.deepStrictEqual(inspect(parser('[ ]{}')), undefined);
59
60
  assert.deepStrictEqual(inspect(parser('[ ]{ }')), undefined);
60
- assert.deepStrictEqual(inspect(parser('[ ]{a}')), undefined);
61
- assert.deepStrictEqual(inspect(parser('[ ]{a}')), undefined);
62
- assert.deepStrictEqual(inspect(parser('[\n]{}')), undefined);
63
- assert.deepStrictEqual(inspect(parser('[\\ ]{}')), undefined);
64
- assert.deepStrictEqual(inspect(parser('[\\\n]{}')), undefined);
65
- assert.deepStrictEqual(inspect(parser('[[]{}')), undefined);
66
- assert.deepStrictEqual(inspect(parser('[]]{}')), undefined);
61
+ assert.deepStrictEqual(inspect(parser('[ ]{b}')), undefined);
62
+ assert.deepStrictEqual(inspect(parser('[ ]{b}')), undefined);
63
+ assert.deepStrictEqual(inspect(parser('[\n]{b}')), undefined);
64
+ assert.deepStrictEqual(inspect(parser('[\\ ]{b}')), undefined);
65
+ assert.deepStrictEqual(inspect(parser('[\\\n]{b}')), undefined);
66
+ assert.deepStrictEqual(inspect(parser('[&Tab;]{b}')), undefined);
67
+ assert.deepStrictEqual(inspect(parser('[[]{b}')), undefined);
68
+ assert.deepStrictEqual(inspect(parser('[]]{b}')), undefined);
67
69
  assert.deepStrictEqual(inspect(parser('[a]{}')), undefined);
68
- assert.deepStrictEqual(inspect(parser('[ a]{b}')), undefined);
69
- assert.deepStrictEqual(inspect(parser('[ a ]{b}')), undefined);
70
- assert.deepStrictEqual(inspect(parser('[a\nb]{#}')), undefined);
71
- assert.deepStrictEqual(inspect(parser('[a\\\nb]{#}')), undefined);
72
- assert.deepStrictEqual(inspect(parser('[<wbr>]{/}')), undefined);
73
- assert.deepStrictEqual(inspect(parser('[[# a #]]{#}')), undefined);
70
+ assert.deepStrictEqual(inspect(parser('[\\ a]{b}')), undefined);
71
+ assert.deepStrictEqual(inspect(parser('[ \\ a]{b}')), undefined);
72
+ assert.deepStrictEqual(inspect(parser('[a\nb]{b}')), undefined);
73
+ assert.deepStrictEqual(inspect(parser('[a\\\nb]{b}')), undefined);
74
+ assert.deepStrictEqual(inspect(parser('[<wbr>]{b}')), undefined);
75
+ assert.deepStrictEqual(inspect(parser('[[# a #]]{b}')), undefined);
74
76
  assert.deepStrictEqual(inspect(parser('[*a\nb*]{/}')), undefined);
75
77
  assert.deepStrictEqual(inspect(parser('[http://host]{http://host}')), undefined);
76
78
  assert.deepStrictEqual(inspect(parser('[]{ttp://host}')), [['<a class="invalid">ttp://host</a>'], '']);
77
79
  //assert.deepStrictEqual(inspect(parser('[]{http://[::ffff:0:0%1]}')), [['<a class="invalid">http://[::ffff:0:0%1]</a>'], '']);
78
80
  //assert.deepStrictEqual(inspect(parser('[]{http://[::ffff:0:0/96]}')), [['<a class="invalid">http://[::ffff:0:0/96]</a>'], '']);
79
- assert.deepStrictEqual(inspect(parser(' []{a}')), undefined);
81
+ assert.deepStrictEqual(inspect(parser('[]{^/.}')), [[`<a class="invalid">^/.</a>`], '']);
82
+ assert.deepStrictEqual(inspect(parser('[]{^/..}')), [[`<a class="invalid">^/..</a>`], '']);
83
+ assert.deepStrictEqual(inspect(parser('[]{^/../}')), [[`<a class="invalid">^/../</a>`], '']);
84
+ assert.deepStrictEqual(inspect(parser('[]{^/../..}')), [[`<a class="invalid">^/../..</a>`], '']);
85
+ assert.deepStrictEqual(inspect(parser('[]{^/../b}')), [[`<a class="invalid">^/../b</a>`], '']);
86
+ assert.deepStrictEqual(inspect(parser('[]{^/../b/..}')), [[`<a class="invalid">^/../b/..</a>`], '']);
87
+ assert.deepStrictEqual(inspect(parser(' []{b}')), undefined);
80
88
  assert.deepStrictEqual(inspect(parser('![]{/}')), undefined);
81
89
  });
82
90
 
@@ -91,15 +99,28 @@ describe('Unit: parser/inline/link', () => {
91
99
  assert.deepStrictEqual(inspect(parser('[]{\\b}')), [[`<a href="\\b">\\b</a>`], '']);
92
100
  assert.deepStrictEqual(inspect(parser('[]{?b=c+d&\\#}')), [['<a href="?b=c+d&amp;\\#">?b=c+d&amp;\\#</a>'], '']);
93
101
  assert.deepStrictEqual(inspect(parser('[]{?&amp;}')), [['<a href="?&amp;amp;">?&amp;amp;</a>'], '']);
94
- assert.deepStrictEqual(inspect(parser('[]{./b}')), [['<a href="./b">./b</a>'], '']);
95
- assert.deepStrictEqual(inspect(parser('[]{^/b}')), [[`<a href="/b">^/b</a>`], '']);
96
102
  assert.deepStrictEqual(inspect(parser('[]{#}')), [['<a href="#">#</a>'], '']);
97
103
  assert.deepStrictEqual(inspect(parser('[]{#b}')), [['<a href="#b">#b</a>'], '']);
104
+ assert.deepStrictEqual(inspect(parser('[]{./b}')), [['<a href="./b">./b</a>'], '']);
105
+ assert.deepStrictEqual(inspect(parser('[]{^/b}')), [[`<a href="/b">^/b</a>`], '']);
106
+ assert.deepStrictEqual(inspect(parser('[]{^/b?/../}')), [[`<a href="/b?/../">^/b?/../</a>`], '']);
107
+ assert.deepStrictEqual(inspect(parser('[]{^/b#/../}')), [[`<a href="/b#/../">^/b#/../</a>`], '']);
108
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/dir', location.origin) })), [[`<a href="/dir/b">^/b</a>`], '']);
109
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/dir/', location.origin) })), [[`<a href="/dir/b">^/b</a>`], '']);
110
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/folder/doc.md', location.origin) })), [[`<a href="/folder/b">^/b</a>`], '']);
111
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/folder/doc.md/', location.origin) })), [[`<a href="/folder/doc.md/b">^/b</a>`], '']);
112
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/.file', location.origin) })), [[`<a href="/b">^/b</a>`], '']);
113
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/0.0a', location.origin) })), [[`<a href="/b">^/b</a>`], '']);
114
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/0.a0', location.origin) })), [[`<a href="/b">^/b</a>`], '']);
115
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/0.0', location.origin) })), [[`<a href="/0.0/b">^/b</a>`], '']);
116
+ assert.deepStrictEqual(inspect(parser('[]{^/b}', { host: new URL('/0.0,0.0,0z', location.origin) })), [[`<a href="/0.0,0.0,0z/b">^/b</a>`], '']);
117
+ assert.deepStrictEqual(inspect(parser('[ a]{b}')), [['<a href="b">a</a>'], '']);
118
+ assert.deepStrictEqual(inspect(parser('[ a ]{b}')), [['<a href="b">a</a>'], '']);
119
+ assert.deepStrictEqual(inspect(parser('[a ]{b}')), [['<a href="b">a</a>'], '']);
120
+ assert.deepStrictEqual(inspect(parser('[a ]{b}')), [['<a href="b">a</a>'], '']);
98
121
  assert.deepStrictEqual(inspect(parser('[a]{b}')), [['<a href="b">a</a>'], '']);
99
122
  assert.deepStrictEqual(inspect(parser('[a]{#}')), [['<a href="#">a</a>'], '']);
100
123
  assert.deepStrictEqual(inspect(parser('[a]{#b}')), [['<a href="#b">a</a>'], '']);
101
- assert.deepStrictEqual(inspect(parser('[a ]{b}')), [['<a href="b">a</a>'], '']);
102
- assert.deepStrictEqual(inspect(parser('[a ]{b}')), [['<a href="b">a</a>'], '']);
103
124
  assert.deepStrictEqual(inspect(parser('[a b]{c}')), [['<a href="c">a b</a>'], '']);
104
125
  assert.deepStrictEqual(inspect(parser(`[]{?#${encodeURIComponent(':/[]{}<>?#=& ')}}`)), [['<a href="?#%3A%2F%5B%5D%7B%7D%3C%3E%3F%23%3D%26%20">?#%3A%2F[]{}&lt;&gt;%3F%23%3D%26%20</a>'], '']);
105
126
  assert.deepStrictEqual(inspect(parser('{b}')), [['<a href="b">b</a>'], '']);
@@ -7,7 +7,7 @@ import { inline, media, shortmedia } from '../inline';
7
7
  import { attributes } from './html';
8
8
  import { autolink } from '../autolink';
9
9
  import { str } from '../source';
10
- import { startTight, trimEnd, stringify } from '../util';
10
+ import { startLoose, trimNode, stringify } from '../util';
11
11
  import { html, define, defrag } from 'typed-dom';
12
12
  import { ReadonlyURL } from 'spica/url';
13
13
 
@@ -28,7 +28,7 @@ export const link: LinkParser = lazy(() => creator(10, bind(reverse(
28
28
  surround('[', shortmedia, ']'),
29
29
  surround(
30
30
  '[',
31
- startTight(
31
+ startLoose(
32
32
  context({ syntax: { inline: {
33
33
  annotation: false,
34
34
  reference: false,
@@ -39,7 +39,7 @@ export const link: LinkParser = lazy(() => creator(10, bind(reverse(
39
39
  media: false,
40
40
  autolink: false,
41
41
  }}},
42
- some(inline, ']', /^\\?\n/))),
42
+ some(inline, ']', /^\\?\n/)), ']'),
43
43
  ']',
44
44
  true),
45
45
  ]))),
@@ -54,9 +54,9 @@ export const link: LinkParser = lazy(() => creator(10, bind(reverse(
54
54
  assert(!INSECURE_URI.match(/\s/));
55
55
  const el = create(
56
56
  INSECURE_URI,
57
- trimEnd(defrag(content)),
57
+ trimNode(defrag(content)),
58
58
  new ReadonlyURL(
59
- resolve(INSECURE_URI, context.host || location, context.url || location),
59
+ resolve(INSECURE_URI, context.host ?? location, context.url ?? context.host ?? location),
60
60
  context.host?.href || location.href),
61
61
  context.host?.origin || location.origin);
62
62
  if (el.classList.contains('invalid')) return [[el], rest];
@@ -81,33 +81,25 @@ export function resolve(uri: string, host: URL | Location, source: URL | Locatio
81
81
  assert(uri === uri.trim());
82
82
  switch (true) {
83
83
  case uri.slice(0, 2) === '^/':
84
- const file = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
85
- return file.includes('.')
86
- ? `${host.pathname.slice(0, -file.length)}${uri.slice(2)}`
87
- : `${fillTrailingSlash(host.pathname)}${uri.slice(2)}`;
84
+ const last = host.pathname.slice(host.pathname.lastIndexOf('/') + 1);
85
+ return last.includes('.') // isFile
86
+ && /^[0-9]*[a-z][0-9a-z]*$/i.test(last.slice(last.lastIndexOf('.') + 1))
87
+ ? `${host.pathname.slice(0, -last.length)}${uri.slice(2)}`
88
+ : `${host.pathname.replace(/\/?$/, '/')}${uri.slice(2)}`;
88
89
  case host.origin === source.origin
89
90
  && host.pathname === source.pathname:
90
91
  case uri.slice(0, 2) === '//':
91
92
  return uri;
92
93
  default:
93
94
  const target = new ReadonlyURL(uri, source.href);
94
- return target.origin === uri.match(/^[A-Za-z][0-9A-Za-z.+-]*:\/\/[^/?#]*/)?.[0]
95
- ? uri
96
- : target.origin === host.origin
95
+ return target.origin === host.origin
97
96
  ? target.href.slice(target.origin.length)
98
97
  : target.href;
99
98
  }
100
99
  }
101
100
 
102
- function fillTrailingSlash(pathname: string): string {
103
- assert(pathname);
104
- return pathname[pathname.length - 1] === '/'
105
- ? pathname
106
- : pathname + '/';
107
- }
108
-
109
101
  function create(
110
- address: string,
102
+ INSECURE_URI: string,
111
103
  content: readonly (string | HTMLElement)[],
112
104
  uri: ReadonlyURL,
113
105
  origin: string,
@@ -118,6 +110,12 @@ function create(
118
110
  case 'http:':
119
111
  case 'https:':
120
112
  assert(uri.host);
113
+ if (INSECURE_URI.slice(0, 2) === '^/' &&
114
+ /\/\.\.?(?:\/|$)/.test(INSECURE_URI.slice(0, INSECURE_URI.search(/[?#]|$/)))) {
115
+ type = 'argument';
116
+ description = 'Dot-segments cannot be used in subresource paths.';
117
+ break;
118
+ }
121
119
  return html('a',
122
120
  {
123
121
  href: uri.source,
@@ -128,19 +126,19 @@ function create(
128
126
  : undefined,
129
127
  },
130
128
  content.length === 0
131
- ? decode(address)
129
+ ? decode(INSECURE_URI)
132
130
  : content);
133
131
  case 'tel:':
134
132
  if (content.length === 0) {
135
- content = [address];
133
+ content = [INSECURE_URI];
136
134
  }
137
135
  const pattern = /^(?:tel:)?(?:\+(?!0))?\d+(?:-\d+)*$/i;
138
136
  switch (true) {
139
137
  case content.length === 1
140
138
  && typeof content[0] === 'string'
141
- && pattern.test(address)
139
+ && pattern.test(INSECURE_URI)
142
140
  && pattern.test(content[0])
143
- && address.replace(/[^+\d]/g, '') === content[0].replace(/[^+\d]/g, ''):
141
+ && INSECURE_URI.replace(/[^+\d]/g, '') === content[0].replace(/[^+\d]/g, ''):
144
142
  return html('a', { href: uri.source }, content);
145
143
  }
146
144
  type = 'content';
@@ -155,7 +153,7 @@ function create(
155
153
  'data-invalid-description': description ??= 'Invalid protocol.',
156
154
  },
157
155
  content.length === 0
158
- ? address
156
+ ? INSECURE_URI
159
157
  : content);
160
158
  }
161
159
 
@@ -12,7 +12,10 @@ describe('Unit: parser/inline/mark', () => {
12
12
  assert.deepStrictEqual(inspect(parser('==')), undefined);
13
13
  assert.deepStrictEqual(inspect(parser('==a')), [['==', 'a'], '']);
14
14
  assert.deepStrictEqual(inspect(parser('==a=')), [['==', 'a', '='], '']);
15
- assert.deepStrictEqual(inspect(parser('==a ==')), [['==', 'a', ' '], '==']);
15
+ assert.deepStrictEqual(inspect(parser('==a ==')), [['==', 'a', ' '], '==']);
16
+ assert.deepStrictEqual(inspect(parser('==a\n==')), [['==', 'a', '<br>'], '==']);
17
+ assert.deepStrictEqual(inspect(parser('==a\\ ==')), [['==', 'a', ' '], '==']);
18
+ assert.deepStrictEqual(inspect(parser('==a\\\n==')), [['==', 'a', '<span class="linebreak"> </span>'], '==']);
16
19
  assert.deepStrictEqual(inspect(parser('== ==')), undefined);
17
20
  assert.deepStrictEqual(inspect(parser('== a==')), undefined);
18
21
  assert.deepStrictEqual(inspect(parser('== a ==')), undefined);
@@ -26,14 +29,10 @@ describe('Unit: parser/inline/mark', () => {
26
29
  it('basic', () => {
27
30
  assert.deepStrictEqual(inspect(parser('==a==')), [['<mark>a</mark>'], '']);
28
31
  assert.deepStrictEqual(inspect(parser('==ab==')), [['<mark>ab</mark>'], '']);
29
- assert.deepStrictEqual(inspect(parser('==a ==')), [['<mark>a </mark>'], '']);
30
- assert.deepStrictEqual(inspect(parser('==a\n==')), [['<mark>a</mark>'], '']);
31
32
  assert.deepStrictEqual(inspect(parser('==a\nb==')), [['<mark>a<br>b</mark>'], '']);
32
33
  assert.deepStrictEqual(inspect(parser('==a\\\nb==')), [['<mark>a<span class="linebreak"> </span>b</mark>'], '']);
33
34
  assert.deepStrictEqual(inspect(parser('==\\===')), [['<mark>=</mark>'], '']);
34
35
  assert.deepStrictEqual(inspect(parser('==a===')), [['<mark>a</mark>'], '=']);
35
- assert.deepStrictEqual(inspect(parser('===a==')), [['<mark>=a</mark>'], '']);
36
- assert.deepStrictEqual(inspect(parser('===a===')), [['<mark>=a</mark>'], '=']);
37
36
  });
38
37
 
39
38
  it('nest', () => {
@@ -2,7 +2,7 @@ import { MarkParser } from '../inline';
2
2
  import { union, some, creator, surround, lazy } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { str } from '../source';
5
- import { startTight, verifyEndTight, trimEndBR } from '../util';
5
+ import { startTight, isEndTightNodes } from '../util';
6
6
  import { html, defrag } from 'typed-dom';
7
7
  import { unshift } from 'spica/array';
8
8
 
@@ -11,7 +11,7 @@ export const mark: MarkParser = lazy(() => creator(surround(
11
11
  startTight(union([some(inline, '==')])),
12
12
  str('=='), false,
13
13
  ([as, bs, cs], rest) =>
14
- verifyEndTight(bs)
15
- ? [[html('mark', defrag(trimEndBR(bs)))], rest]
14
+ isEndTightNodes(bs)
15
+ ? [[html('mark', defrag(bs))], rest]
16
16
  : [unshift(as, bs), cs[0] + rest],
17
17
  ([as, bs], rest) => [unshift(as, bs), rest])));
@@ -23,6 +23,7 @@ describe('Unit: parser/inline/math', () => {
23
23
  assert.deepStrictEqual(inspect(parser('$-0$-1')), undefined);
24
24
  assert.deepStrictEqual(inspect(parser('$-a$')), undefined);
25
25
  assert.deepStrictEqual(inspect(parser('$-a$-b')), undefined);
26
+ assert.deepStrictEqual(inspect(parser('$a $')), undefined);
26
27
  assert.deepStrictEqual(inspect(parser('$a-b$')), undefined);
27
28
  assert.deepStrictEqual(inspect(parser('$a-b$c-d')), undefined);
28
29
  assert.deepStrictEqual(inspect(parser('$a+b$')), undefined);
@@ -33,6 +34,8 @@ describe('Unit: parser/inline/math', () => {
33
34
  assert.deepStrictEqual(inspect(parser('$a$b')), undefined);
34
35
  assert.deepStrictEqual(inspect(parser('$a$b$')), undefined);
35
36
  assert.deepStrictEqual(inspect(parser('$ $')), undefined);
37
+ assert.deepStrictEqual(inspect(parser('$ a$')), undefined);
38
+ assert.deepStrictEqual(inspect(parser('$ a $')), undefined);
36
39
  assert.deepStrictEqual(inspect(parser('$\n$')), undefined);
37
40
  assert.deepStrictEqual(inspect(parser('$a\nb$')), undefined);
38
41
  assert.deepStrictEqual(inspect(parser('$a\\\nb$')), undefined);
@@ -59,6 +62,7 @@ describe('Unit: parser/inline/math', () => {
59
62
  assert.deepStrictEqual(inspect(parser('${}$')), [['<span class="math" translate="no" data-src="${}$">${}$</span>'], '']);
60
63
  assert.deepStrictEqual(inspect(parser('${ }$')), [['<span class="math" translate="no" data-src="${ }$">${ }$</span>'], '']);
61
64
  assert.deepStrictEqual(inspect(parser('${a}$')), [['<span class="math" translate="no" data-src="${a}$">${a}$</span>'], '']);
65
+ assert.deepStrictEqual(inspect(parser('${a}$$')), [['<span class="math" translate="no" data-src="${a}$">${a}$</span>'], '$']);
62
66
  assert.deepStrictEqual(inspect(parser('${a}$0')), [['<span class="math" translate="no" data-src="${a}$">${a}$</span>'], '0']);
63
67
  assert.deepStrictEqual(inspect(parser('${a}$b')), [['<span class="math" translate="no" data-src="${a}$">${a}$</span>'], 'b']);
64
68
  assert.deepStrictEqual(inspect(parser('${ab}$')), [['<span class="math" translate="no" data-src="${ab}$">${ab}$</span>'], '']);
@@ -71,14 +75,15 @@ describe('Unit: parser/inline/math', () => {
71
75
  assert.deepStrictEqual(inspect(parser('${\\$}$')), [['<span class="math" translate="no" data-src="${\\$}$">${\\$}$</span>'], '']);
72
76
  assert.deepStrictEqual(inspect(parser('${\\\\}$')), [['<span class="math" translate="no" data-src="${\\\\}$">${\\\\}$</span>'], '']);
73
77
  assert.deepStrictEqual(inspect(parser('$a$')), [['<span class="math" translate="no" data-src="$a$">$a$</span>'], '']);
78
+ assert.deepStrictEqual(inspect(parser('$a$$')), [['<span class="math" translate="no" data-src="$a$">$a$</span>'], '$']);
74
79
  assert.deepStrictEqual(inspect(parser(`$a'$`)), [[`<span class="math" translate="no" data-src="$a'$">$a'$</span>`], '']);
75
80
  assert.deepStrictEqual(inspect(parser(`$a''$`)), [[`<span class="math" translate="no" data-src="$a''$">$a''$</span>`], '']);
76
81
  assert.deepStrictEqual(inspect(parser('$a$[A](a)')), [['<span class="math" translate="no" data-src="$a$">$a$</span>'], '[A](a)']);
77
- assert.deepStrictEqual(inspect(parser('$a$$')), [['<span class="math" translate="no" data-src="$a$">$a$</span>'], '$']);
78
82
  assert.deepStrictEqual(inspect(parser('$A$')), [['<span class="math" translate="no" data-src="$A$">$A$</span>'], '']);
79
83
  assert.deepStrictEqual(inspect(parser('$\\$$')), [['<span class="math" translate="no" data-src="$\\$$">$\\$$</span>'], '']);
80
84
  assert.deepStrictEqual(inspect(parser('$\\Pi$')), [['<span class="math" translate="no" data-src="$\\Pi$">$\\Pi$</span>'], '']);
81
- assert.deepStrictEqual(inspect(parser('$\\Pi $')), [['<span class="math" translate="no" data-src="$\\Pi $">$\\Pi $</span>'], '']);
85
+ assert.deepStrictEqual(inspect(parser('$\\ 0$')), [['<span class="math" translate="no" data-src="$\\ 0$">$\\ 0$</span>'], '']);
86
+ assert.deepStrictEqual(inspect(parser('$\\\\0$')), [['<span class="math" translate="no" data-src="$\\\\0$">$\\\\0$</span>'], '']);
82
87
  assert.deepStrictEqual(inspect(parser('$|1|$')), [['<span class="math" translate="no" data-src="$|1|$">$|1|$</span>'], '']);
83
88
  assert.deepStrictEqual(inspect(parser('$[0,1)$]')), [['<span class="math" translate="no" data-src="$[0,1)$">$[0,1)$</span>'], ']']);
84
89
  assert.deepStrictEqual(inspect(parser('$(0, 1]$)')), [['<span class="math" translate="no" data-src="$(0, 1]$">$(0, 1]$</span>'], ')']);
@@ -1,7 +1,7 @@
1
1
  import { MathParser } from '../inline';
2
2
  import { union, some, validate, verify, rewrite, creator, surround, lazy } from '../../combinator';
3
3
  import { escsource, str } from '../source';
4
- import { verifyEndTight } from '../util';
4
+ import { isEndTightNodes } from '../util';
5
5
  import { html } from 'typed-dom';
6
6
 
7
7
  const disallowedCommand = /\\(?:begin|tiny|huge|large)(?![0-9a-z])/i;
@@ -21,7 +21,7 @@ export const math: MathParser = lazy(() => creator(validate('$', '$', '\n', rewr
21
21
  // $[A-z]*[,.!?()] : Incomplete syntax before texts
22
22
  // $[A-z]*\s?[!@#&*+~=`$[]{<] : Incomplete syntax in or around another syntax
23
23
  str(/^(?=[\\^_[(|]|[A-Za-z][0-9A-Za-z]*'*[ ~]?(?:\$|([\\^_(|:=<>])(?!\1)))(?:\\\$|[\x20-\x23\x25-\x7E])*/),
24
- verifyEndTight),
24
+ isEndTightNodes),
25
25
  /^\$(?![0-9A-Za-z])/),
26
26
  ]),
27
27
  (source, { caches: { math: cache } = {} }) => [[
@@ -29,26 +29,34 @@ describe('Unit: parser/inline/media', () => {
29
29
  assert.deepStrictEqual(inspect(parser('![]{ }')), undefined);
30
30
  assert.deepStrictEqual(inspect(parser('![]]{/}')), undefined);
31
31
  assert.deepStrictEqual(inspect(parser('![]{{}')), undefined);
32
- assert.deepStrictEqual(inspect(parser('![]{{a}}')), undefined);
33
- assert.deepStrictEqual(inspect(parser('![]{a\nb}')), undefined);
34
- assert.deepStrictEqual(inspect(parser('![]{a\\\nb}')), undefined);
35
- assert.deepStrictEqual(inspect(parser('![]{ a}')), undefined);
36
- assert.deepStrictEqual(inspect(parser('![]{ a\n}')), undefined);
37
- assert.deepStrictEqual(inspect(parser('![ ]{#}')), undefined);
38
- assert.deepStrictEqual(inspect(parser('![ ]{#}')), undefined);
39
- assert.deepStrictEqual(inspect(parser('![\\ ]{#}')), undefined);
40
- assert.deepStrictEqual(inspect(parser('![&Tab;]{#}')), undefined);
32
+ assert.deepStrictEqual(inspect(parser('![]{{b}}')), undefined);
33
+ assert.deepStrictEqual(inspect(parser('![]{b\nc}')), undefined);
34
+ assert.deepStrictEqual(inspect(parser('![]{a\\\nc}')), undefined);
35
+ assert.deepStrictEqual(inspect(parser('![]{ b}')), undefined);
36
+ assert.deepStrictEqual(inspect(parser('![]{ b\n}')), undefined);
37
+ assert.deepStrictEqual(inspect(parser('![ ]{}')), undefined);
38
+ assert.deepStrictEqual(inspect(parser('![ ]{b}')), undefined);
39
+ assert.deepStrictEqual(inspect(parser('![ ]{b}')), undefined);
40
+ assert.deepStrictEqual(inspect(parser('![\n]{b}')), undefined);
41
+ assert.deepStrictEqual(inspect(parser('![\\ ]{b}')), undefined);
42
+ assert.deepStrictEqual(inspect(parser('![\\\n]{b}')), undefined);
43
+ assert.deepStrictEqual(inspect(parser('![&Tab;]{b}')), undefined);
44
+ assert.deepStrictEqual(inspect(parser('![[]{b}')), undefined);
45
+ assert.deepStrictEqual(inspect(parser('![]]{b}')), undefined);
41
46
  assert.deepStrictEqual(inspect(parser('![a]{}')), undefined);
42
- assert.deepStrictEqual(inspect(parser('![ a]{#}')), undefined);
43
- assert.deepStrictEqual(inspect(parser('![ a ]{#}')), undefined);
44
- assert.deepStrictEqual(inspect(parser('![\\ a ]{#}')), undefined);
45
- assert.deepStrictEqual(inspect(parser('![a\nb]{#}')), undefined);
46
- assert.deepStrictEqual(inspect(parser('![a\\\nb]{#}')), undefined);
47
+ assert.deepStrictEqual(inspect(parser('![\\ a ]{b}')), undefined);
48
+ assert.deepStrictEqual(inspect(parser('![ \\ a ]{b}')), undefined);
49
+ assert.deepStrictEqual(inspect(parser('![a\nb]{b}')), undefined);
50
+ assert.deepStrictEqual(inspect(parser('![a\\\nb]{b}')), undefined);
47
51
  assert.deepStrictEqual(inspect(parser('![]{ttp://host}')), [['<img class="media invalid" data-src="ttp://host" alt="">'], '']);
48
52
  assert.deepStrictEqual(inspect(parser('![]{tel:1234567890}')), [['<img class="media invalid" data-src="tel:1234567890" alt="">'], '']);
49
53
  //assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0%1]}')), [['<img class="media invalid" alt="">'], '']);
50
54
  //assert.deepStrictEqual(inspect(parser('![]{http://[::ffff:0:0/96]}')), [['<img class="media invalid" alt="">'], '']);
51
- assert.deepStrictEqual(inspect(parser(' ![]{a}')), undefined);
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="">'], '']);
59
+ assert.deepStrictEqual(inspect(parser(' ![]{b}')), undefined);
52
60
  assert.deepStrictEqual(inspect(parser('[]{/}')), undefined);
53
61
  });
54
62
 
@@ -61,8 +69,11 @@ describe('Unit: parser/inline/media', () => {
61
69
  assert.deepStrictEqual(inspect(parser('![]{\\}')), [['<a href="\\" target="_blank"><img class="media" data-src="\\" alt=""></a>'], '']);
62
70
  assert.deepStrictEqual(inspect(parser('![]{\\ }')), [['<a href="\\" target="_blank"><img class="media" data-src="\\" alt=""></a>'], '']);
63
71
  assert.deepStrictEqual(inspect(parser('![]{\\b}')), [['<a href="\\b" target="_blank"><img class="media" data-src="\\b" alt=""></a>'], '']);
64
- assert.deepStrictEqual(inspect(parser('![]{./b}')), [['<a href="./b" target="_blank"><img class="media" data-src="./b" alt=""></a>'], '']);
72
+ assert.deepStrictEqual(inspect(parser('![]{?/../}')), [[`<a href="?/../" target="_blank"><img class="media" data-src="?/../" alt=""></a>`], '']);
73
+ assert.deepStrictEqual(inspect(parser('![]{#/../}')), [[`<a href="#/../" target="_blank"><img class="media" data-src="#/../" alt=""></a>`], '']);
65
74
  assert.deepStrictEqual(inspect(parser('![]{^/b}')), [[`<a href="/b" target="_blank"><img class="media" data-src="/b" alt=""></a>`], '']);
75
+ assert.deepStrictEqual(inspect(parser('![ a]{b}')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
76
+ assert.deepStrictEqual(inspect(parser('![ a ]{b}')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
66
77
  assert.deepStrictEqual(inspect(parser('![a ]{b}')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
67
78
  assert.deepStrictEqual(inspect(parser('![a ]{b}')), [['<a href="b" target="_blank"><img class="media" data-src="b" alt="a"></a>'], '']);
68
79
  assert.deepStrictEqual(inspect(parser('![a b]{c}')), [['<a href="c" target="_blank"><img class="media" data-src="c" alt="a b"></a>'], '']);
@@ -6,7 +6,6 @@ import { link, uri, option as linkoption, resolve } from './link';
6
6
  import { attributes } from './html';
7
7
  import { htmlentity } from './htmlentity';
8
8
  import { txt, str } from '../source';
9
- import { verifyStartTight } from '../util';
10
9
  import { html, define } from 'typed-dom';
11
10
  import { ReadonlyURL } from 'spica/url';
12
11
  import { unshift, push, join } from 'spica/array';
@@ -24,28 +23,29 @@ export const media: MediaParser = lazy(() => creator(10, bind(verify(fmap(open(
24
23
  validate(['[', '{'], '}', '\n',
25
24
  guard(context => context.syntax?.inline?.media ?? true,
26
25
  tails([
27
- dup(surround(/^\[(?!\\?\s)/, some(union([htmlentity, bracket, txt]), ']', /^\\?\n/), ']', true)),
26
+ dup(surround(/^\[(?!\s*\\\s)/, some(union([htmlentity, bracket, txt]), ']', /^\\?\n/), ']', true)),
28
27
  dup(surround(/^{(?![{}])/, inits([uri, some(option)]), /^[^\S\n]?}/)),
29
28
  ])))),
30
- ([as, bs]) => bs ? [[join(as)], bs] : [[''], as]),
31
- ([[text]]) => verifyStartTight([text || '-'])),
29
+ ([as, bs]) => bs ? [[join(as).trim() || join(as)], bs] : [[''], as]),
30
+ ([[text]]) => text === '' || text.trim() !== ''),
32
31
  ([[text], params], rest, context) => {
33
32
  const INSECURE_URI = params.shift()!;
34
33
  assert(INSECURE_URI === INSECURE_URI.trim());
35
34
  assert(!INSECURE_URI.match(/\s/));
36
35
  const url = new ReadonlyURL(
37
- resolve(INSECURE_URI, context.host || location, context.url || location),
36
+ resolve(INSECURE_URI, context.host ?? location, context.url ?? context.host ?? location),
38
37
  context.host?.href || location.href);
39
- const cache = context.caches?.media;
40
- const cached = cache?.has(url.href);
41
- const el = cache && cached
42
- ? cache.get(url.href)!.cloneNode(true)
43
- : html('img', { class: 'media', 'data-src': url.source, alt: text.trimEnd() });
44
- if (!cached && !sanitize(url, el)) return [[el], rest];
45
- cached && el.hasAttribute('alt') && el.setAttribute('alt', text.trimEnd());
38
+ let cache: HTMLElement | undefined;
39
+ const el = undefined
40
+ || (cache = context.caches?.media?.get(url.href)?.cloneNode(true))
41
+ || html('img', { class: 'media', 'data-src': url.source, alt: text });
42
+ assert(!el.matches('.invalid'));
43
+ if (!cache && !sanitize(url, el)) return [[el], rest];
44
+ assert(!el.matches('.invalid'));
45
+ cache?.hasAttribute('alt') && cache?.setAttribute('alt', text);
46
46
  define(el, attributes('media', push([], el.classList), optspec, params));
47
47
  assert(el.matches('img') || !el.matches('.invalid'));
48
- if (context.syntax?.inline?.link === false || cached && el.tagName !== 'IMG') return [[el], rest];
48
+ if (context.syntax?.inline?.link === false || cache && cache.tagName !== 'IMG') return [[el], rest];
49
49
  return fmap(
50
50
  link as MediaParser,
51
51
  ([link]) => [define(link, { target: '_blank' }, [el])])
@@ -67,18 +67,29 @@ const option: MediaParser.ParameterParser.OptionParser = union([
67
67
 
68
68
  function sanitize(uri: ReadonlyURL, target: HTMLElement): boolean {
69
69
  assert(target.tagName === 'IMG');
70
+ assert(!target.matches('.invalid'));
71
+ if (/\/\.\.?(?:\/|$)/.test('/' + uri.source.slice(0, uri.source.search(/[?#]|$/)))) {
72
+ define(target, {
73
+ class: void target.classList.add('invalid'),
74
+ 'data-invalid-syntax': 'media',
75
+ 'data-invalid-type': 'argument',
76
+ 'data-invalid-description': 'Dot-segments cannot be used in media paths; use subresource paths instead.',
77
+ });
78
+ return false;
79
+ }
70
80
  switch (uri.protocol) {
71
81
  case 'http:':
72
82
  case 'https:':
73
83
  assert(uri.host);
74
- return true;
84
+ break;
85
+ default:
86
+ define(target, {
87
+ class: void target.classList.add('invalid'),
88
+ 'data-invalid-syntax': 'media',
89
+ 'data-invalid-type': 'argument',
90
+ 'data-invalid-description': 'Invalid protocol.',
91
+ });
92
+ return false;
75
93
  }
76
- assert(!target.classList.contains('invalid'));
77
- define(target, {
78
- class: void target.classList.add('invalid'),
79
- 'data-invalid-syntax': 'media',
80
- 'data-invalid-type': 'argument',
81
- 'data-invalid-description': 'Invalid protocol.',
82
- });
83
- return false;
94
+ return true;
84
95
  }
@@ -14,8 +14,6 @@ describe('Unit: parser/inline/reference', () => {
14
14
  assert.deepStrictEqual(inspect(parser('[[]]')), undefined);
15
15
  assert.deepStrictEqual(inspect(parser('[[]]]')), undefined);
16
16
  assert.deepStrictEqual(inspect(parser('[[ ]]')), undefined);
17
- assert.deepStrictEqual(inspect(parser('[[ a]]')), undefined);
18
- assert.deepStrictEqual(inspect(parser('[[ a ]]')), undefined);
19
17
  assert.deepStrictEqual(inspect(parser('[[\n]]')), undefined);
20
18
  assert.deepStrictEqual(inspect(parser('[[\na]]')), undefined);
21
19
  assert.deepStrictEqual(inspect(parser('[[\\ a]]')), undefined);
@@ -34,6 +32,8 @@ describe('Unit: parser/inline/reference', () => {
34
32
  });
35
33
 
36
34
  it('basic', () => {
35
+ assert.deepStrictEqual(inspect(parser('[[ a]]')), [['<sup class="reference">a</sup>'], '']);
36
+ assert.deepStrictEqual(inspect(parser('[[ a ]]')), [['<sup class="reference">a</sup>'], '']);
37
37
  assert.deepStrictEqual(inspect(parser('[[a]]')), [['<sup class="reference">a</sup>'], '']);
38
38
  assert.deepStrictEqual(inspect(parser('[[a ]]')), [['<sup class="reference">a</sup>'], '']);
39
39
  assert.deepStrictEqual(inspect(parser('[[a ]]')), [['<sup class="reference">a</sup>'], '']);
@@ -3,13 +3,13 @@ import { ReferenceParser } from '../inline';
3
3
  import { union, subsequence, some, validate, verify, focus, guard, context, creator, surround, lazy, fmap } from '../../combinator';
4
4
  import { inline } from '../inline';
5
5
  import { str } from '../source';
6
- import { startTight, isStartTight, trimEnd, stringify } from '../util';
6
+ import { startLoose, isStartLoose, trimNode, stringify } from '../util';
7
7
  import { html, defrag } from 'typed-dom';
8
8
 
9
9
  export const reference: ReferenceParser = lazy(() => creator(validate('[[', ']]', '\n', fmap(surround(
10
10
  '[[',
11
11
  guard(context => context.syntax?.inline?.reference ?? true,
12
- startTight(
12
+ startLoose(
13
13
  context({ syntax: { inline: {
14
14
  annotation: false,
15
15
  reference: false,
@@ -24,15 +24,15 @@ export const reference: ReferenceParser = lazy(() => creator(validate('[[', ']]'
24
24
  abbr,
25
25
  focus('^', c => [['', c], '']),
26
26
  some(inline, ']', /^\\?\n/),
27
- ])))),
27
+ ])), ']]')),
28
28
  ']]'),
29
- ns => [html('sup', attributes(ns), trimEnd(defrag(ns)))]))));
29
+ ns => [html('sup', attributes(ns), trimNode(defrag(ns)))]))));
30
30
 
31
31
  const abbr: ReferenceParser.AbbrParser = creator(fmap(verify(surround(
32
32
  '^',
33
33
  union([str(/^(?![0-9]+\s?[|\]])[0-9A-Za-z]+(?:(?:-|(?=\W)(?!'\d)'?(?!\.\d)\.?(?!,\S),? ?)[0-9A-Za-z]+)*(?:-|'?\.?,? ?)?/)]),
34
- /^\|?(?=]])|^\|[^\S\n]+/),
35
- (_, rest, context) => isStartTight(rest, context)),
34
+ /^\|?(?=]])|^\|[^\S\n]/),
35
+ (_, rest, context) => isStartLoose(rest, context)),
36
36
  ([source]) => [html('abbr', source)]));
37
37
 
38
38
  function attributes(ns: (string | HTMLElement)[]): Record<string, string | undefined> {
@@ -4,7 +4,7 @@ import { eval, exec } from '../../combinator/data/parser';
4
4
  import { sequence, validate, verify, focus, creator, surround, lazy, bind } from '../../combinator';
5
5
  import { htmlentity } from './htmlentity';
6
6
  import { text as txt } from '../source';
7
- import { verifyStartTight } from '../util';
7
+ import { isStartTightNodes } from '../util';
8
8
  import { html, defrag } from 'typed-dom';
9
9
  import { unshift, push, join } from 'spica/array';
10
10
 
@@ -14,7 +14,7 @@ export const ruby: RubyParser = lazy(() => creator(bind(verify(
14
14
  surround('[', focus(/^(?:\\[^\n]|[^\[\]\n])+(?=]\()/, text), ']'),
15
15
  surround('(', focus(/^(?:\\[^\n]|[^\(\)\n])+(?=\))/, text), ')'),
16
16
  ])),
17
- ([texts]) => verifyStartTight(texts)),
17
+ ([texts]) => isStartTightNodes(texts)),
18
18
  ([texts, rubies], rest) => {
19
19
  const tail = typeof texts[texts.length - 1] === 'object'
20
20
  ? [texts.pop()!]
@@ -10,7 +10,10 @@ describe('Unit: parser/inline/strong', () => {
10
10
  assert.deepStrictEqual(inspect(parser('**')), undefined);
11
11
  assert.deepStrictEqual(inspect(parser('**a')), [['**', 'a'], '']);
12
12
  assert.deepStrictEqual(inspect(parser('**a*')), [['**', 'a', '*'], '']);
13
- assert.deepStrictEqual(inspect(parser('**a **')), [['**', 'a', ' '], '**']);
13
+ assert.deepStrictEqual(inspect(parser('**a **')), [['**', 'a', ' '], '**']);
14
+ assert.deepStrictEqual(inspect(parser('**a\n**')), [['**', 'a', '<br>'], '**']);
15
+ assert.deepStrictEqual(inspect(parser('**a\\ **')), [['**', 'a', ' '], '**']);
16
+ assert.deepStrictEqual(inspect(parser('**a\\\n**')), [['**', 'a', '<span class="linebreak"> </span>'], '**']);
14
17
  assert.deepStrictEqual(inspect(parser('**a*b**')), [['**', 'a', '<em>b</em>', '*'], '']);
15
18
  assert.deepStrictEqual(inspect(parser('** **')), undefined);
16
19
  assert.deepStrictEqual(inspect(parser('** a**')), undefined);
@@ -27,15 +30,12 @@ describe('Unit: parser/inline/strong', () => {
27
30
 
28
31
  it('basic', () => {
29
32
  assert.deepStrictEqual(inspect(parser('**a**')), [['<strong>a</strong>'], '']);
30
- assert.deepStrictEqual(inspect(parser('**a **')), [['<strong>a </strong>'], '']);
31
- assert.deepStrictEqual(inspect(parser('**a\n**')), [['<strong>a</strong>'], '']);
32
- assert.deepStrictEqual(inspect(parser('**a\\\n**')), [['<strong>a<span class="linebreak"> </span></strong>'], '']);
33
33
  assert.deepStrictEqual(inspect(parser('**ab**')), [['<strong>ab</strong>'], '']);
34
34
  assert.deepStrictEqual(inspect(parser('**a\nb**')), [['<strong>a<br>b</strong>'], '']);
35
35
  assert.deepStrictEqual(inspect(parser('**a\\\nb**')), [['<strong>a<span class="linebreak"> </span>b</strong>'], '']);
36
36
  assert.deepStrictEqual(inspect(parser('**a*b*c**')), [['<strong>a<em>b</em>c</strong>'], '']);
37
37
  assert.deepStrictEqual(inspect(parser('**a*b*c**d')), [['<strong>a<em>b</em>c</strong>'], 'd']);
38
- assert.deepStrictEqual(inspect(parser('**a *b***')), [['<strong>a <em>b</em></strong>'], '']);
38
+ assert.deepStrictEqual(inspect(parser('**a *b***')), [['<strong>a <em>b</em></strong>'], '']);
39
39
  });
40
40
 
41
41
  it('nest', () => {