securemark 0.254.1 → 0.255.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.255.1
4
+
5
+ - Refactoring.
6
+
7
+ ## 0.255.0
8
+
9
+ - Change html parser not to use backtracking.
10
+
11
+ ## 0.254.2
12
+
13
+ - Refactoring.
14
+
3
15
  ## 0.254.1
4
16
 
5
17
  - Fix footnote processing.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! securemark v0.254.1 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
1
+ /*! securemark v0.255.1 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
2
2
  (function webpackUniversalModuleDefinition(root, factory) {
3
3
  if(typeof exports === 'object' && typeof module === 'object')
4
4
  module.exports = factory(require("DOMPurify"), require("Prism"));
@@ -2127,14 +2127,12 @@ function guard(f, parser) {
2127
2127
  exports.guard = guard;
2128
2128
 
2129
2129
  function reset(base, parser) {
2130
- if (isEmpty(base)) return parser;
2131
2130
  return (source, context) => parser(source, inherit((0, alias_1.ObjectCreate)(context), base));
2132
2131
  }
2133
2132
 
2134
2133
  exports.reset = reset;
2135
2134
 
2136
2135
  function context(base, parser) {
2137
- if (isEmpty(base)) return parser;
2138
2136
  const override = (0, memoize_1.memoize)(context => inherit((0, alias_1.ObjectCreate)(context), base), new global_1.WeakMap());
2139
2137
  return (source, context) => parser(source, override(context));
2140
2138
  }
@@ -2166,12 +2164,6 @@ const inherit = (0, assign_1.template)((prop, target, source) => {
2166
2164
  }
2167
2165
  });
2168
2166
 
2169
- function isEmpty(context) {
2170
- for (const _ in context) return false;
2171
-
2172
- return true;
2173
- }
2174
-
2175
2167
  /***/ }),
2176
2168
 
2177
2169
  /***/ 7957:
@@ -3550,12 +3542,13 @@ function parse(source, opts = {}, context) {
3550
3542
  const url = (0, header_2.headers)(source).find(field => field.toLowerCase().startsWith('url:'))?.slice(4).trim() ?? '';
3551
3543
  source = !context ? (0, normalize_1.normalize)(source) : source;
3552
3544
  context = {
3553
- url: url ? new url_1.ReadonlyURL(url) : context?.url,
3554
3545
  host: opts.host ?? context?.host ?? new url_1.ReadonlyURL(global_1.location.pathname, global_1.location.origin),
3546
+ url: url ? new url_1.ReadonlyURL(url) : context?.url,
3547
+ id: opts.id ?? context?.id,
3555
3548
  caches: context?.caches,
3556
- footnotes: global_1.undefined,
3557
- test: global_1.undefined,
3558
- ...opts
3549
+ ...(context?.resources && {
3550
+ resources: context.resources
3551
+ })
3559
3552
  };
3560
3553
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
3561
3554
  const node = (0, dom_1.frag)();
@@ -3888,7 +3881,7 @@ const parse_1 = __webpack_require__(5013);
3888
3881
 
3889
3882
  const dom_1 = __webpack_require__(3252);
3890
3883
 
3891
- exports.aside = (0, combinator_1.creator)(100, (0, combinator_1.block)((0, combinator_1.validate)('~~~', (0, combinator_1.fmap)((0, combinator_1.fence)(/^(~{3,})aside(?!\S)([^\n]*)(?:$|\n)/, 300), // Bug: Type mismatch between outer and inner.
3884
+ exports.aside = (0, combinator_1.block)((0, combinator_1.validate)('~~~', (0, combinator_1.fmap)((0, combinator_1.fence)(/^(~{3,})aside(?!\S)([^\n]*)(?:$|\n)/, 300), // Bug: Type mismatch between outer and inner.
3892
3885
  ([body, overflow, closer, opener, delim, param], _, context) => {
3893
3886
  if (!closer || overflow || param.trimStart()) return [(0, dom_1.html)('pre', {
3894
3887
  class: 'invalid',
@@ -3920,7 +3913,7 @@ exports.aside = (0, combinator_1.creator)(100, (0, combinator_1.block)((0, combi
3920
3913
  id: (0, indexee_1.identity)((0, indexee_1.text)(heading)),
3921
3914
  class: 'aside'
3922
3915
  }, [document, references])];
3923
- }))));
3916
+ })));
3924
3917
 
3925
3918
  /***/ }),
3926
3919
 
@@ -3946,7 +3939,7 @@ const mathblock_1 = __webpack_require__(3754);
3946
3939
  const dom_1 = __webpack_require__(3252);
3947
3940
 
3948
3941
  const opener = /^(~{3,})(?:example\/(\S+))?(?!\S)([^\n]*)(?:$|\n)/;
3949
- exports.example = (0, combinator_1.creator)(100, (0, combinator_1.block)((0, combinator_1.validate)('~~~', (0, combinator_1.fmap)((0, combinator_1.fence)(opener, 300), // Bug: Type mismatch between outer and inner.
3942
+ exports.example = (0, combinator_1.block)((0, combinator_1.validate)('~~~', (0, combinator_1.fmap)((0, combinator_1.fence)(opener, 300), // Bug: Type mismatch between outer and inner.
3950
3943
  ([body, overflow, closer, opener, delim, type = 'markdown', param], _, context) => {
3951
3944
  if (!closer || overflow || param.trimStart()) return [(0, dom_1.html)('pre', {
3952
3945
  class: 'invalid',
@@ -3993,7 +3986,7 @@ exports.example = (0, combinator_1.creator)(100, (0, combinator_1.block)((0, com
3993
3986
  'data-invalid-message': 'Invalid example type'
3994
3987
  }, `${opener}${body}${closer}`)];
3995
3988
  }
3996
- }))));
3989
+ })));
3997
3990
 
3998
3991
  /***/ }),
3999
3992
 
@@ -6219,14 +6212,17 @@ const attrspecs = {
6219
6212
  global_1.Object.setPrototypeOf(attrspecs, null);
6220
6213
  global_1.Object.values(attrspecs).forEach(o => global_1.Object.setPrototypeOf(o, null));
6221
6214
  exports.html = (0, combinator_1.lazy)(() => (0, combinator_1.creator)((0, combinator_1.validate)('<', (0, combinator_1.validate)(/^<[a-z]+(?=[^\S\n]|>)/, (0, combinator_1.union)([(0, combinator_1.focus)('<wbr>', () => [[(0, dom_1.html)('wbr')], '']), (0, combinator_1.focus)( // https://html.spec.whatwg.org/multipage/syntax.html#void-elements
6222
- /^<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[^\S\n]|>)/, source => [[source], '']), (0, combinator_1.match)(new RegExp(String.raw`^<(${TAGS.join('|')})(?=[^\S\n]|>)`), (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, source_1.str)(/^[^\S\n]*>/), true), (0, util_1.startLoose)((0, combinator_1.some)((0, combinator_1.union)([(0, combinator_1.open)(/^\n?/, (0, combinator_1.some)(inline_1.inline, (0, util_1.blankWith)('\n', `</${tag}>`)), true)])), `</${tag}>`), (0, source_1.str)(`</${tag}>`), false, ([as, bs, cs], rest) => [[elem(tag, as, bs, cs)], rest]), ([, tag]) => TAGS.indexOf(tag), [])), (0, combinator_1.match)(/^<([a-z]+)(?=[^\S\n]|>)/, (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, source_1.str)(/^[^\S\n]*>/), true), (0, util_1.startLoose)((0, combinator_1.some)((0, combinator_1.union)([(0, combinator_1.open)(/^\n?/, (0, combinator_1.some)(inline_1.inline, (0, util_1.blankWith)('\n', `</${tag}>`)), true)])), `</${tag}>`), (0, source_1.str)(`</${tag}>`), false, ([as, bs, cs], rest) => [[elem(tag, as, bs, cs)], rest]), ([, tag]) => tag, new cache_1.Cache(10000)))])))));
6215
+ /^<(?:area|base|br|col|embed|hr|img|input|link|meta|source|track|wbr)(?=[^\S\n]|>)/, source => [[source], '']), (0, combinator_1.match)(new RegExp(String.raw`^<(${TAGS.join('|')})(?=[^\S\n]|>)`), (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, source_1.str)(/^[^\S\n]*>/), true), (0, combinator_1.subsequence)([(0, combinator_1.focus)(/^[^\S\n]*\n/, (0, combinator_1.some)(inline_1.inline)), (0, combinator_1.some)((0, combinator_1.open)(/^\n?/, (0, combinator_1.some)(inline_1.inline, (0, util_1.blankWith)('\n', `</${tag}>`)), true))]), (0, source_1.str)(`</${tag}>`), true, ([as, bs = [], cs], rest) => [[elem(tag, as, bs, cs)], rest], ([as, bs = []], rest) => [[elem(tag, as, bs, [])], rest]), ([, tag]) => TAGS.indexOf(tag), [])), (0, combinator_1.match)(/^<([a-z]+)(?=[^\S\n]|>)/, (0, memoize_1.memoize)(([, tag]) => (0, combinator_1.surround)((0, combinator_1.surround)((0, source_1.str)(`<${tag}`), (0, combinator_1.some)(exports.attribute), (0, source_1.str)(/^[^\S\n]*>/), true), (0, combinator_1.subsequence)([(0, combinator_1.focus)(/^[^\S\n]*\n/, (0, combinator_1.some)(inline_1.inline)), (0, combinator_1.some)(inline_1.inline, `</${tag}>`)]), (0, source_1.str)(`</${tag}>`), true, ([as, bs = [], cs], rest) => [[elem(tag, as, bs, cs)], rest], ([as, bs = []], rest) => [[elem(tag, as, bs, [])], rest]), ([, tag]) => tag, new cache_1.Cache(10000)))])))));
6223
6216
  exports.attribute = (0, combinator_1.union)([(0, source_1.str)(/^[^\S\n]+[a-z]+(?:-[a-z]+)*(?:="(?:\\[^\n]|[^\\\n"])*")?(?=[^\S\n]|>)/)]); // https://developer.mozilla.org/en-US/docs/Web/HTML/Element
6224
6217
  // [...document.querySelectorAll('tbody > tr > td:first-child')].map(el => el.textContent.slice(1, -1))
6225
6218
 
6226
6219
  const TAGS = global_1.Object.freeze(["html", "base", "head", "link", "meta", "style", "title", "body", "address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4", "h5", "h6", "main", "nav", "section", "blockquote", "dd", "div", "dl", "dt", "figcaption", "figure", "hr", "li", "menu", "ol", "p", "pre", "ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn", "em", "i", "kbd", "mark", "q", "rp", "rt", "ruby", "s", "samp", "small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "area", "audio", "img", "map", "track", "video", "embed", "iframe", "object", "picture", "portal", "source", "svg", "math", "canvas", "noscript", "script", "del", "ins", "caption", "col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "button", "datalist", "fieldset", "form", "input", "label", "legend", "meter", "optgroup", "option", "output", "progress", "select", "textarea", "details", "dialog", "summary", "slot", "template", "acronym", "applet", "basefont", "bgsound", "big", "blink", "center", "content", "dir", "font", "frame", "frameset", "hgroup", "image", "keygen", "marquee", "menuitem", "nobr", "noembed", "noframes", "param", "plaintext", "rb", "rtc", "shadow", "spacer", "strike", "tt", "xmp"]);
6227
6220
 
6228
6221
  function elem(tag, as, bs, cs) {
6229
- if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag <${tag}>`, as, bs, cs);
6222
+ if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
6223
+ if (cs.length === 0) return invalid('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
6224
+ if (bs.length === 0) return invalid('content', `Missing the content`, as, bs, cs);
6225
+ if (!(0, util_1.isStartLooseNodes)(bs)) return invalid('content', `Missing the visible content in the same line`, as, bs, cs);
6230
6226
  const attrs = attributes('html', [], attrspecs[tag], as.slice(1, -1));
6231
6227
  return 'data-invalid-syntax' in attrs ? invalid('attribute', 'Invalid HTML attribute', as, bs, cs) : (0, dom_1.html)(tag, attrs, (0, dom_1.defrag)(bs));
6232
6228
  }
@@ -6588,6 +6584,12 @@ exports.media = (0, combinator_1.lazy)(() => (0, combinator_1.creator)(10, (0, c
6588
6584
  cache?.hasAttribute('alt') && cache?.setAttribute('alt', text);
6589
6585
  if (!sanitize(el, url, text)) return [[el], rest];
6590
6586
  (0, dom_1.define)(el, (0, html_1.attributes)('media', (0, array_1.push)([], el.classList), optspec, params));
6587
+
6588
+ // Awaiting the generic support for attr().
6589
+ if (el.hasAttribute('aspect-ratio')) {
6590
+ el.style.aspectRatio = el.getAttribute('aspect-ratio');
6591
+ }
6592
+
6591
6593
  if (context.syntax?.inline?.link === false || cache && cache.tagName !== 'IMG') return [[el], rest];
6592
6594
  return (0, combinator_1.fmap)(link_1.link, ([link]) => [(0, dom_1.define)(link, {
6593
6595
  target: '_blank'
@@ -7637,7 +7639,7 @@ const str_1 = __webpack_require__(2790);
7637
7639
 
7638
7640
  const dom_1 = __webpack_require__(3252);
7639
7641
 
7640
- exports.delimiter = /[\s\x00-\x7F]|\S#|[、。!?][^\S\n]*(?=\\\n)/;
7642
+ exports.delimiter = /[\s\x00-\x7F]|\S#|[()、。!?][^\S\n]*(?=\\\n)/;
7641
7643
  exports.nonWhitespace = /[\S\n]|$/;
7642
7644
  exports.nonAlphanumeric = /[^0-9A-Za-z]|\S#|$/;
7643
7645
  const repeat = (0, str_1.str)(/^(.)\1*/);
@@ -7767,7 +7769,7 @@ exports.unescsource = (0, combinator_1.creator)(source => {
7767
7769
  Object.defineProperty(exports, "__esModule", ({
7768
7770
  value: true
7769
7771
  }));
7770
- exports.stringify = exports.trimBlankEnd = exports.trimBlankStart = exports.trimBlank = exports.isStartTightNodes = exports.startTight = exports.startLoose = exports.visualize = exports.blankWith = exports.regBlankStart = void 0;
7772
+ exports.stringify = exports.trimBlankEnd = exports.trimBlankStart = exports.trimBlank = exports.isStartTightNodes = exports.isStartLooseNodes = exports.startTight = exports.startLoose = exports.visualize = exports.blankWith = exports.regBlankStart = void 0;
7771
7773
 
7772
7774
  const global_1 = __webpack_require__(4128);
7773
7775
 
@@ -7871,6 +7873,24 @@ const isStartTight = (0, memoize_1.reduce)((source, context, except) => {
7871
7873
  }
7872
7874
  }, (source, _, except = '') => `${source}\x1E${except}`);
7873
7875
 
7876
+ function isStartLooseNodes(nodes) {
7877
+ if (nodes.length === 0) return true;
7878
+
7879
+ for (let i = 0; i < nodes.length; ++i) {
7880
+ const node = nodes[i];
7881
+ if (isVisible(node)) return true;
7882
+
7883
+ if (typeof node === 'object') {
7884
+ if (node.tagName === 'BR') break;
7885
+ if (node.className === 'linebreak') break;
7886
+ }
7887
+ }
7888
+
7889
+ return false;
7890
+ }
7891
+
7892
+ exports.isStartLooseNodes = isStartLooseNodes;
7893
+
7874
7894
  function isStartTightNodes(nodes) {
7875
7895
  if (nodes.length === 0) return true;
7876
7896
  return isVisible(nodes[0], 0);
@@ -8378,6 +8398,7 @@ function video(source, url) {
8378
8398
  src: source.getAttribute('data-src'),
8379
8399
  'data-type': 'video',
8380
8400
  ...global_1.Object.fromEntries([...source.attributes].map(attr => [attr.name, attr.value])),
8401
+ style: source.hasAttribute('aspect-ratio') ? `aspect-ratio: ${source.getAttribute('aspect-ratio')};` : global_1.undefined,
8381
8402
  muted: '',
8382
8403
  controls: ''
8383
8404
  });
package/markdown.d.ts CHANGED
@@ -985,6 +985,7 @@ export namespace MarkdownParser {
985
985
  Inline<'html/tag'>,
986
986
  Parser<HTMLElement | string, Context, [
987
987
  InlineParser,
988
+ InlineParser,
988
989
  ]> {
989
990
  }
990
991
  export namespace TagParser {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.254.1",
3
+ "version": "0.255.1",
4
4
  "description": "Secure markdown renderer working on browsers for user input data.",
5
5
  "private": false,
6
6
  "homepage": "https://github.com/falsandtru/securemark",
@@ -17,7 +17,6 @@ export function reset<P extends Parser<unknown>>(context: Context<P>, parser: P)
17
17
  export function reset<T>(base: Ctx, parser: Parser<T>): Parser<T> {
18
18
  assert(Object.getPrototypeOf(base) === Object.prototype);
19
19
  assert(Object.freeze(base));
20
- if (isEmpty(base)) return parser;
21
20
  return (source, context) =>
22
21
  parser(source, inherit(ObjectCreate(context), base));
23
22
  }
@@ -26,7 +25,6 @@ export function context<P extends Parser<unknown>>(context: Context<P>, parser:
26
25
  export function context<T>(base: Ctx, parser: Parser<T>): Parser<T> {
27
26
  assert(Object.getPrototypeOf(base) === Object.prototype);
28
27
  assert(Object.freeze(base));
29
- if (isEmpty(base)) return parser;
30
28
  const override = memoize<Ctx, Ctx>(context => inherit(ObjectCreate(context), base), new WeakMap());
31
29
  return (source, context) =>
32
30
  parser(source, override(context));
@@ -40,6 +38,7 @@ const inherit = template((prop, target, source) => {
40
38
  switch (prop) {
41
39
  case 'resources':
42
40
  assert(typeof source[prop] === 'object');
41
+ assert(target[prop] || !(prop in target));
43
42
  if (prop in target && !hasOwnProperty(target, prop)) return;
44
43
  return target[prop] = ObjectCreate(source[prop]);
45
44
  }
@@ -58,8 +57,3 @@ const inherit = template((prop, target, source) => {
58
57
  return target[prop] = source[prop];
59
58
  }
60
59
  });
61
-
62
- function isEmpty(context: Ctx): boolean {
63
- for (const _ in context) return false;
64
- return true;
65
- }
@@ -27,7 +27,7 @@ describe('Unit: parser/api/bind', () => {
27
27
  return acc;
28
28
  }
29
29
 
30
- const cfgs = { footnotes: { annotations: html('ol'), references: html('ol') } };
30
+ const cfgs = { footnotes: { references: html('ol') } };
31
31
 
32
32
  it('huge input', () => {
33
33
  const iter = bind(html('div'), { ...cfgs, id: '' }).parse(`${'\n'.repeat(10 * 1000 ** 2)}`);
@@ -168,62 +168,62 @@ describe('Unit: parser/api/bind', () => {
168
168
  const el = html('div');
169
169
  const chunk = frag();
170
170
  const update = bind(chunk, { ...cfgs, chunk: true }).parse;
171
- const iter = update([...Array(3)].map((_, i) => `((${i + 1}))`).join('\n\n'));
171
+ const iter = update([...Array(3)].map((_, i) => `[[${i + 1}]]`).join('\n\n'));
172
172
 
173
173
  inspect(iter, 2);
174
174
  el.appendChild(chunk);
175
175
  assert.deepStrictEqual(
176
176
  [...el.children].map(el => el.outerHTML),
177
177
  [
178
- html('p', [html('sup', { class: "annotation" }, [html('span', '1')]),]).outerHTML,
179
- html('p', [html('sup', { class: "annotation" }, [html('span', '2')]),]).outerHTML,
178
+ html('p', [html('sup', { class: "reference" }, [html('span', '1')]),]).outerHTML,
179
+ html('p', [html('sup', { class: "reference" }, [html('span', '2')]),]).outerHTML,
180
180
  ]);
181
181
  inspect(iter, 1);
182
182
  el.appendChild(chunk);
183
183
  assert.deepStrictEqual(
184
184
  [...el.children].map(el => el.outerHTML),
185
185
  [
186
- html('p', [html('sup', { class: "annotation" }, [html('span', '1')]),]).outerHTML,
187
- html('p', [html('sup', { class: "annotation" }, [html('span', '2')]),]).outerHTML,
188
- html('p', [html('sup', { class: "annotation" }, [html('span', '3')]),]).outerHTML,
186
+ html('p', [html('sup', { class: "reference" }, [html('span', '1')]),]).outerHTML,
187
+ html('p', [html('sup', { class: "reference" }, [html('span', '2')]),]).outerHTML,
188
+ html('p', [html('sup', { class: "reference" }, [html('span', '3')]),]).outerHTML,
189
189
  ]);
190
190
  inspect(iter);
191
191
  assert.deepStrictEqual(
192
192
  [...el.children].map(el => el.outerHTML),
193
193
  [
194
194
  html('p', [
195
- html('sup', { class: "annotation", id: "annotation:ref:1", title: "1" }, [
195
+ html('sup', { class: "reference", id: "reference:ref:1", title: "1" }, [
196
196
  html('span', { hidden: '' }, '1'),
197
- html('a', { href: "#annotation:def:1" }, '*1')
197
+ html('a', { href: "#reference:def:1" }, '[1]'),
198
198
  ]),
199
199
  ]).outerHTML,
200
200
  html('p', [
201
- html('sup', { class: "annotation", id: "annotation:ref:2", title: "2" }, [
201
+ html('sup', { class: "reference", id: "reference:ref:2", title: "2" }, [
202
202
  html('span', { hidden: '' }, '2'),
203
- html('a', { href: "#annotation:def:2" }, '*2')
203
+ html('a', { href: "#reference:def:2" }, '[2]'),
204
204
  ]),
205
205
  ]).outerHTML,
206
206
  html('p', [
207
- html('sup', { class: "annotation", id: "annotation:ref:3", title: "3" }, [
207
+ html('sup', { class: "reference", id: "reference:ref:3", title: "3" }, [
208
208
  html('span', { hidden: '' }, '3'),
209
- html('a', { href: "#annotation:def:3" }, '*3')
209
+ html('a', { href: "#reference:def:3" }, '[3]'),
210
210
  ]),
211
211
  ]).outerHTML,
212
212
  ]);
213
213
  assert.deepStrictEqual(
214
- cfgs.footnotes.annotations?.outerHTML,
214
+ cfgs.footnotes.references?.outerHTML,
215
215
  html('ol', [
216
- html('li', { id: 'annotation:def:1' }, [
216
+ html('li', { id: 'reference:def:1' }, [
217
217
  '1',
218
- html('sup', [html('a', { href: '#annotation:ref:1' }, '^1')])
218
+ html('sup', [html('a', { href: '#reference:ref:1' }, '^1')]),
219
219
  ]),
220
- html('li', { id: 'annotation:def:2' }, [
220
+ html('li', { id: 'reference:def:2' }, [
221
221
  '2',
222
- html('sup', [html('a', { href: '#annotation:ref:2' }, '^2')])
222
+ html('sup', [html('a', { href: '#reference:ref:2' }, '^2')]),
223
223
  ]),
224
- html('li', { id: 'annotation:def:3' }, [
224
+ html('li', { id: 'reference:def:3' }, [
225
225
  '3',
226
- html('sup', [html('a', { href: '#annotation:ref:3' }, '^3')])
226
+ html('sup', [html('a', { href: '#reference:ref:3' }, '^3')]),
227
227
  ]),
228
228
  ]).outerHTML);
229
229
  assert.throws(() => update('').next());
@@ -204,16 +204,14 @@ describe('Unit: parser/api/parse', () => {
204
204
  });
205
205
 
206
206
  it('footnote', () => {
207
- const footnotes = { annotations: html('ol'), references: html('ol') };
207
+ const footnotes = { references: html('ol') };
208
208
  assert.deepStrictEqual(
209
209
  [...parse('$-a\n$$\n$$\n\n(($-a[[b]][[c*d*]]))', { footnotes }).children].map(el => el.outerHTML),
210
210
  [
211
211
  '<figure data-type="math" data-label="$-a" data-group="$" data-number="1" id="label:$-a"><figcaption><span class="figindex">(1)</span><span class="figtext"></span></figcaption><div><div class="math" translate="no">$$\n$$</div></div></figure>',
212
212
  '<p><sup class="annotation" id="annotation:ref:1" title="(1)[1][2]"><span hidden=""><a class="label" data-label="$-a" href="#label:$-a">(1)</a><sup class="reference" id="reference:ref:1" title="b"><span hidden="">b</span><a href="#reference:def:1">[1]</a></sup><sup class="reference" id="reference:ref:2" title="cd"><span hidden="">c<em>d</em></span><a href="#reference:def:2">[2]</a></sup></span><a href="#annotation:def:1">*1</a></sup></p>',
213
+ '<ol class="annotations"><li id="annotation:def:1" data-marker="*1"><a class="label" data-label="$-a" href="#label:$-a">(1)</a><sup class="reference" id="reference:ref:1" title="b"><span hidden="">b</span><a href="#reference:def:1">[1]</a></sup><sup class="reference" id="reference:ref:2" title="cd"><span hidden="">c<em>d</em></span><a href="#reference:def:2">[2]</a></sup><sup><a href="#annotation:ref:1">^1</a></sup></li></ol>',
213
214
  ]);
214
- assert.deepStrictEqual(
215
- footnotes.annotations.outerHTML,
216
- '<ol><li id="annotation:def:1"><a class="label" data-label="$-a" href="#label:$-a">(1)</a><sup class="reference" id="reference:ref:1" title="b"><span hidden="">b</span><a href="#reference:def:1">[1]</a></sup><sup class="reference" id="reference:ref:2" title="cd"><span hidden="">c<em>d</em></span><a href="#reference:def:2">[2]</a></sup><sup><a href="#annotation:ref:1">^1</a></sup></li></ol>');
217
215
  assert.deepStrictEqual(
218
216
  footnotes.references.outerHTML,
219
217
  '<ol><li id="reference:def:1">b<sup><a href="#reference:ref:1">^1</a></sup></li><li id="reference:def:2">c<em>d</em><sup><a href="#reference:ref:2">^2</a></sup></li></ol>');
@@ -1,4 +1,4 @@
1
- import { undefined, location } from 'spica/global';
1
+ import { location } from 'spica/global';
2
2
  import { ParserOptions } from '../../..';
3
3
  import { MarkdownParser } from '../../../markdown';
4
4
  import { eval } from '../../combinator/data/parser';
@@ -22,12 +22,13 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
22
22
  source = !context ? normalize(source) : source;
23
23
  assert(!context?.delimiters);
24
24
  context = {
25
- url: url ? new ReadonlyURL(url as ':') : context?.url,
26
25
  host: opts.host ?? context?.host ?? new ReadonlyURL(location.pathname, location.origin),
26
+ url: url ? new ReadonlyURL(url as ':') : context?.url,
27
+ id: opts.id ?? context?.id,
27
28
  caches: context?.caches,
28
- footnotes: undefined,
29
- test: undefined,
30
- ...opts,
29
+ ...context?.resources && {
30
+ resources: context.resources,
31
+ },
31
32
  };
32
33
  if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
33
34
  const node = frag();
@@ -1,10 +1,10 @@
1
1
  import { ExtensionParser } from '../../block';
2
- import { block, validate, fence, creator, fmap } from '../../../combinator';
2
+ import { block, validate, fence, fmap } from '../../../combinator';
3
3
  import { identity, text } from '../../inline/extension/indexee';
4
4
  import { parse } from '../../api/parse';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
- export const aside: ExtensionParser.AsideParser = creator(100, block(validate('~~~', fmap(
7
+ export const aside: ExtensionParser.AsideParser = block(validate('~~~', fmap(
8
8
  fence(/^(~{3,})aside(?!\S)([^\n]*)(?:$|\n)/, 300),
9
9
  // Bug: Type mismatch between outer and inner.
10
10
  ([body, overflow, closer, opener, delim, param]: string[], _, context) => {
@@ -43,4 +43,4 @@ export const aside: ExtensionParser.AsideParser = creator(100, block(validate('~
43
43
  references,
44
44
  ]),
45
45
  ];
46
- }))));
46
+ })));
@@ -1,13 +1,13 @@
1
1
  import { ExtensionParser } from '../../block';
2
2
  import { eval } from '../../../combinator/data/parser';
3
- import { block, validate, fence, creator, fmap } from '../../../combinator';
3
+ import { block, validate, fence, fmap } from '../../../combinator';
4
4
  import { parse } from '../../api/parse';
5
5
  import { mathblock } from '../mathblock';
6
6
  import { html } from 'typed-dom/dom';
7
7
 
8
8
  const opener = /^(~{3,})(?:example\/(\S+))?(?!\S)([^\n]*)(?:$|\n)/;
9
9
 
10
- export const example: ExtensionParser.ExampleParser = creator(100, block(validate('~~~', fmap(
10
+ export const example: ExtensionParser.ExampleParser = block(validate('~~~', fmap(
11
11
  fence(opener, 300),
12
12
  // Bug: Type mismatch between outer and inner.
13
13
  ([body, overflow, closer, opener, delim, type = 'markdown', param]: string[], _, context) => {
@@ -58,4 +58,4 @@ export const example: ExtensionParser.ExampleParser = creator(100, block(validat
58
58
  }, `${opener}${body}${closer}`),
59
59
  ];
60
60
  }
61
- }))));
61
+ })));
@@ -46,7 +46,6 @@ describe('Unit: parser/inline/annotation', () => {
46
46
  assert.deepStrictEqual(inspect(parser('((@a))')), [['<sup class="annotation"><span><a href="/@a" class="account">@a</a></span></sup>'], '']);
47
47
  assert.deepStrictEqual(inspect(parser('((http://host))')), [['<sup class="annotation"><span><a href="http://host" target="_blank">http://host</a></span></sup>'], '']);
48
48
  assert.deepStrictEqual(inspect(parser('((![]{a}))')), [['<sup class="annotation"><span>!<a href="a">a</a></span></sup>'], '']);
49
- assert.deepStrictEqual(inspect(parser('((<a>))')), [['<sup class="annotation"><span>&lt;a&gt;</span></sup>'], '']);
50
49
  assert.deepStrictEqual(inspect(parser('(((a)))')), [['<sup class="annotation"><span>(a)</span></sup>'], '']);
51
50
  assert.deepStrictEqual(inspect(parser('((((a))))')), [['<sup class="annotation"><span><span class="paren">((a))</span></span></sup>'], '']);
52
51
  assert.deepStrictEqual(inspect(parser('(([[a]]))')), [['<sup class="annotation"><span><sup class="reference"><span>a</span></sup></span></sup>'], '']);
@@ -46,7 +46,6 @@ describe('Unit: parser/inline/emphasis', () => {
46
46
  assert.deepStrictEqual(inspect(parser('*a**b**c*')), [['<em>a<strong>b</strong>c</em>'], '']);
47
47
  assert.deepStrictEqual(inspect(parser('*a**b**c*d')), [['<em>a<strong>b</strong>c</em>'], 'd']);
48
48
  assert.deepStrictEqual(inspect(parser('*`a`*')), [['<em><code data-src="`a`">a</code></em>'], '']);
49
- assert.deepStrictEqual(inspect(parser('*<bdi>*')), [['<em>&lt;bdi&gt;</em>'], '']);
50
49
  assert.deepStrictEqual(inspect(parser('*(*a*)*')), [['<em><span class="paren">(<em>a</em>)</span></em>'], '']);
51
50
  assert.deepStrictEqual(inspect(parser('*(**a**)*')), [['<em><span class="paren">(<strong>a</strong>)</span></em>'], '']);
52
51
  });
@@ -7,12 +7,12 @@ describe('Unit: parser/inline/html', () => {
7
7
  const parser = (source: string) => some(html)(source, {});
8
8
 
9
9
  it('xss', () => {
10
- assert.deepStrictEqual(inspect(parser('<script>')), undefined);
11
- assert.deepStrictEqual(inspect(parser('<script>alert()<script>')), undefined);
10
+ assert.deepStrictEqual(inspect(parser('<script>')), [['<span class="invalid">&lt;script&gt;</span>'], '']);
11
+ assert.deepStrictEqual(inspect(parser('<script>alert()<script>')), [['<span class="invalid">&lt;script&gt;alert<span class="paren">()</span><span class="invalid">&lt;script&gt;</span></span>'], '']);
12
12
  assert.deepStrictEqual(inspect(parser('<script>alert()</script>')), [['<span class="invalid">&lt;script&gt;alert<span class="paren">()</span>&lt;/script&gt;</span>'], '']);
13
- assert.deepStrictEqual(inspect(parser('<script src="\\""></script>')), undefined);
14
- assert.deepStrictEqual(inspect(parser('<bdi onclick="alert()">')), undefined);
15
- assert.deepStrictEqual(inspect(parser('<bdi onclick="alert()"></bdi>')), undefined);
13
+ assert.deepStrictEqual(inspect(parser('<script src="\\""></script>')), [['<span class="invalid">&lt;script src="\\""&gt;&lt;/script&gt;</span>'], '']);
14
+ assert.deepStrictEqual(inspect(parser('<bdi onclick="alert()">')), [['<span class="invalid">&lt;bdi onclick="alert()"&gt;</span>'], '']);
15
+ assert.deepStrictEqual(inspect(parser('<bdi onclick="alert()"></bdi>')), [['<span class="invalid">&lt;bdi onclick="alert()"&gt;&lt;/bdi&gt;</span>'], '']);
16
16
  assert.deepStrictEqual(inspect(parser('<bdi onclick="alert()">a</bdi>')), [['<span class="invalid">&lt;bdi onclick="alert()"&gt;a&lt;/bdi&gt;</span>'], '']);
17
17
  assert.deepStrictEqual(inspect(parser('<bdi><bdi onclick="alert()">a</bdi></bdi>')), [['<bdi><span class="invalid">&lt;bdi onclick="alert()"&gt;a&lt;/bdi&gt;</span></bdi>'], '']);
18
18
  assert.deepStrictEqual(inspect(parser('<bdo dir="rtl\\"><">a</bdo>')), [['<span class="invalid">&lt;bdo dir="rtl\\"&gt;&lt;"&gt;a&lt;/bdo&gt;</span>'], '']);
@@ -25,17 +25,19 @@ describe('Unit: parser/inline/html', () => {
25
25
  assert.deepStrictEqual(inspect(parser('<a,b>')), undefined);
26
26
  assert.deepStrictEqual(inspect(parser('<a, b>')), undefined);
27
27
  assert.deepStrictEqual(inspect(parser('<T>')), undefined);
28
- assert.deepStrictEqual(inspect(parser('<bdi>z')), undefined);
29
- assert.deepStrictEqual(inspect(parser('<bdi></bdi>')), undefined);
30
- assert.deepStrictEqual(inspect(parser('<bdi> </bdi>')), undefined);
31
- assert.deepStrictEqual(inspect(parser('<bdi>\\ </bdi>')), undefined);
32
- assert.deepStrictEqual(inspect(parser('<bdi>&Tab;</bdi>')), undefined);
33
- assert.deepStrictEqual(inspect(parser('<bdi><wbr></bdi>')), undefined);
34
- assert.deepStrictEqual(inspect(parser('<bdi>\n</bdi>')), undefined);
35
- assert.deepStrictEqual(inspect(parser('<bdi>\na</bdi>')), undefined);
36
- assert.deepStrictEqual(inspect(parser('<bdi>\\\na</bdi>')), undefined);
37
- assert.deepStrictEqual(inspect(parser('<bdi>a')), undefined);
38
- assert.deepStrictEqual(inspect(parser('<bdi>a</BDO>')), undefined);
28
+ assert.deepStrictEqual(inspect(parser('<bdi>z')), [['<span class="invalid">&lt;bdi&gt;z</span>'], '']);
29
+ assert.deepStrictEqual(inspect(parser('<bdi></bdi>')), [['<span class="invalid">&lt;bdi&gt;&lt;/bdi&gt;</span>'], '']);
30
+ assert.deepStrictEqual(inspect(parser('<bdi> </bdi>')), [['<span class="invalid">&lt;bdi&gt; &lt;/bdi&gt;</span>'], '']);
31
+ assert.deepStrictEqual(inspect(parser('<bdi> \n</bdi>')), [['<span class="invalid">&lt;bdi&gt;<br>&lt;/bdi&gt;</span>'], '']);
32
+ assert.deepStrictEqual(inspect(parser('<bdi> \na</bdi>')), [['<span class="invalid">&lt;bdi&gt;<br>a&lt;/bdi&gt;</span>'], '']);
33
+ assert.deepStrictEqual(inspect(parser('<bdi>\\ </bdi>')), [['<span class="invalid">&lt;bdi&gt; &lt;/bdi&gt;</span>'], '']);
34
+ assert.deepStrictEqual(inspect(parser('<bdi>&Tab;</bdi>')), [['<span class="invalid">&lt;bdi&gt;\t&lt;/bdi&gt;</span>'], '']);
35
+ assert.deepStrictEqual(inspect(parser('<bdi><wbr></bdi>')), [['<span class="invalid">&lt;bdi&gt;<wbr>&lt;/bdi&gt;</span>'], '']);
36
+ assert.deepStrictEqual(inspect(parser('<bdi>\n</bdi>')), [['<span class="invalid">&lt;bdi&gt;<br>&lt;/bdi&gt;</span>'], '']);
37
+ assert.deepStrictEqual(inspect(parser('<bdi>\na</bdi>')), [['<span class="invalid">&lt;bdi&gt;<br>a&lt;/bdi&gt;</span>'], '']);
38
+ assert.deepStrictEqual(inspect(parser('<bdi>\\\na</bdi>')), [['<span class="invalid">&lt;bdi&gt;<span class="linebreak"> </span>a&lt;/bdi&gt;</span>'], '']);
39
+ assert.deepStrictEqual(inspect(parser('<bdi>a')), [['<span class="invalid">&lt;bdi&gt;a</span>'], '']);
40
+ assert.deepStrictEqual(inspect(parser('<bdi>a</BDO>')), [['<span class="invalid">&lt;bdi&gt;a&lt;/BDO&gt;</span>'], '']);
39
41
  assert.deepStrictEqual(inspect(parser('<BDI>a</BDI>')), undefined);
40
42
  assert.deepStrictEqual(inspect(parser('<BDI>a</bdo>')), undefined);
41
43
  assert.deepStrictEqual(inspect(parser('</bdi>')), undefined);
@@ -43,11 +45,11 @@ describe('Unit: parser/inline/html', () => {
43
45
  assert.deepStrictEqual(inspect(parser('<b><b><b>a</b></b></b>')), [['<span class="invalid">&lt;b&gt;<span class="invalid">&lt;b&gt;<span class="invalid">&lt;b&gt;a&lt;/b&gt;</span>&lt;/b&gt;</span>&lt;/b&gt;</span>'], '']);
44
46
  assert.deepStrictEqual(inspect(parser('<bdi><bdi><bdi>a</bdi></bdi></bdi>')), [['<bdi><bdi><bdi>a</bdi></bdi></bdi>'], '']);
45
47
  assert.deepStrictEqual(inspect(parser('<x a="*b*"')), undefined);
46
- assert.deepStrictEqual(inspect(parser('<x a="*b*">')), undefined);
47
- assert.deepStrictEqual(inspect(parser('<x a="*b*">c')), undefined);
48
+ assert.deepStrictEqual(inspect(parser('<x a="*b*">')), [['<span class="invalid">&lt;x a="*b*"&gt;</span>'], '']);
49
+ assert.deepStrictEqual(inspect(parser('<x a="*b*">c')), [['<span class="invalid">&lt;x a="*b*"&gt;c</span>'], '']);
48
50
  assert.deepStrictEqual(inspect(parser('<bdi a="*b*"')), undefined);
49
- assert.deepStrictEqual(inspect(parser('<bdi a="*b*">')), undefined);
50
- assert.deepStrictEqual(inspect(parser('<bdi a="*b*">c')), undefined);
51
+ assert.deepStrictEqual(inspect(parser('<bdi a="*b*">')), [['<span class="invalid">&lt;bdi a="*b*"&gt;</span>'], '']);
52
+ assert.deepStrictEqual(inspect(parser('<bdi a="*b*">c')), [['<span class="invalid">&lt;bdi a="*b*"&gt;c</span>'], '']);
51
53
  assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*')), undefined);
52
54
  assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>')), undefined);
53
55
  assert.deepStrictEqual(inspect(parser('<bdi a b="*" *c*>d</bdi>')), undefined);
@@ -80,7 +82,7 @@ describe('Unit: parser/inline/html', () => {
80
82
  });
81
83
 
82
84
  it('escape', () => {
83
- assert.deepStrictEqual(inspect(parser('<a>')), undefined);
85
+ assert.deepStrictEqual(inspect(parser('<a>')), [['<span class="invalid">&lt;a&gt;</span>'], '']);
84
86
  assert.deepStrictEqual(inspect(parser('<bdi><a>a</a></bdi>')), [['<bdi><span class="invalid">&lt;a&gt;a&lt;/a&gt;</span></bdi>'], '']);
85
87
  assert.deepStrictEqual(inspect(parser('<bdi>a<a>b</a>c</bdi>')), [['<bdi>a<span class="invalid">&lt;a&gt;b&lt;/a&gt;</span>c</bdi>'], '']);
86
88
  assert.deepStrictEqual(inspect(parser('<img>')), [['<img'], '>']);
@@ -1,9 +1,9 @@
1
1
  import { undefined, Object } from 'spica/global';
2
2
  import { HTMLParser } from '../inline';
3
- import { union, some, validate, focus, creator, surround, open, match, lazy } from '../../combinator';
3
+ import { union, subsequence, some, validate, focus, creator, surround, open, match, lazy } from '../../combinator';
4
4
  import { inline } from '../inline';
5
5
  import { str } from '../source';
6
- import { startLoose, blankWith } from '../util';
6
+ import { isStartLooseNodes, blankWith } from '../util';
7
7
  import { html as h, defrag } from 'typed-dom/dom';
8
8
  import { memoize } from 'spica/memoize';
9
9
  import { Cache } from 'spica/cache';
@@ -32,12 +32,15 @@ export const html: HTMLParser = lazy(() => creator(validate('<', validate(/^<[a-
32
32
  ([, tag]) =>
33
33
  surround<HTMLParser.TagParser, string>(surround(
34
34
  str(`<${tag}`), some(attribute), str(/^[^\S\n]*>/), true),
35
- startLoose(some(union([
36
- open(/^\n?/, some(inline, blankWith('\n', `</${tag}>`)), true),
37
- ])), `</${tag}>`),
38
- str(`</${tag}>`), false,
39
- ([as, bs, cs], rest) =>
40
- [[elem(tag, as, bs, cs)], rest]),
35
+ subsequence([
36
+ focus(/^[^\S\n]*\n/, some(inline)),
37
+ some(open(/^\n?/, some(inline, blankWith('\n', `</${tag}>`)), true)),
38
+ ]),
39
+ str(`</${tag}>`), true,
40
+ ([as, bs = [], cs], rest) =>
41
+ [[elem(tag, as, bs, cs)], rest],
42
+ ([as, bs = []], rest) =>
43
+ [[elem(tag, as, bs, [])], rest]),
41
44
  ([, tag]) => TAGS.indexOf(tag), [])),
42
45
  match(
43
46
  /^<([a-z]+)(?=[^\S\n]|>)/,
@@ -45,12 +48,15 @@ export const html: HTMLParser = lazy(() => creator(validate('<', validate(/^<[a-
45
48
  ([, tag]) =>
46
49
  surround<HTMLParser.TagParser, string>(surround(
47
50
  str(`<${tag}`), some(attribute), str(/^[^\S\n]*>/), true),
48
- startLoose(some(union([
49
- open(/^\n?/, some(inline, blankWith('\n', `</${tag}>`)), true),
50
- ])), `</${tag}>`),
51
- str(`</${tag}>`), false,
52
- ([as, bs, cs], rest) =>
53
- [[elem(tag, as, bs, cs)], rest]),
51
+ subsequence([
52
+ focus(/^[^\S\n]*\n/, some(inline)),
53
+ some(inline, `</${tag}>`),
54
+ ]),
55
+ str(`</${tag}>`), true,
56
+ ([as, bs = [], cs], rest) =>
57
+ [[elem(tag, as, bs, cs)], rest],
58
+ ([as, bs = []], rest) =>
59
+ [[elem(tag, as, bs, [])], rest]),
54
60
  ([, tag]) => tag,
55
61
  new Cache(10000))),
56
62
  ])))));
@@ -204,8 +210,10 @@ const TAGS = Object.freeze([
204
210
  function elem(tag: string, as: string[], bs: (HTMLElement | string)[], cs: string[]): HTMLElement {
205
211
  assert(as.length > 0);
206
212
  assert(as[0][0] === '<' && as[as.length - 1].slice(-1) === '>');
207
- assert(cs.length === 1);
208
- if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag <${tag}>`, as, bs, cs);
213
+ if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
214
+ if (cs.length === 0) return invalid('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
215
+ if (bs.length === 0) return invalid('content', `Missing the content`, as, bs, cs);
216
+ if (!isStartLooseNodes(bs)) return invalid('content', `Missing the visible content in the same line`, as, bs, cs);
209
217
  const attrs = attributes('html', [], attrspecs[tag], as.slice(1, -1));
210
218
  return 'data-invalid-syntax' in attrs
211
219
  ? invalid('attribute', 'Invalid HTML attribute', as, bs, cs)
@@ -106,9 +106,9 @@ describe('Unit: parser/inline/media', () => {
106
106
  assert.deepStrictEqual(inspect(parser('![]{/ nofollow}')), [['<a href="/" rel="nofollow" target="_blank"><img class="media" data-src="/" alt=""></a>'], '']);
107
107
  assert.deepStrictEqual(inspect(parser('![]{/ width="4" height="3"}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" width="4" height="3"></a>'], '']);
108
108
  assert.deepStrictEqual(inspect(parser('![]{/ 4x3}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" width="4" height="3"></a>'], '']);
109
- assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio="4/3"}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3"></a>'], '']);
110
- assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio="4/3" nofollow}')), [['<a href="/" rel="nofollow" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3"></a>'], '']);
111
- assert.deepStrictEqual(inspect(parser('![]{/ 4:3}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3"></a>'], '']);
109
+ assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio="4/3"}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3" style="aspect-ratio: 4 / 3;"></a>'], '']);
110
+ assert.deepStrictEqual(inspect(parser('![]{/ aspect-ratio="4/3" nofollow}')), [['<a href="/" rel="nofollow" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3" style="aspect-ratio: 4 / 3;"></a>'], '']);
111
+ assert.deepStrictEqual(inspect(parser('![]{/ 4:3}')), [['<a href="/" target="_blank"><img class="media" data-src="/" alt="" aspect-ratio="4/3" style="aspect-ratio: 4 / 3;"></a>'], '']);
112
112
  });
113
113
 
114
114
  });
@@ -48,6 +48,10 @@ export const media: MediaParser = lazy(() => creator(10, validate(['![', '!{'],
48
48
  assert(!el.matches('.invalid'));
49
49
  define(el, attributes('media', push([], el.classList), optspec, params));
50
50
  assert(el.matches('img') || !el.matches('.invalid'));
51
+ // Awaiting the generic support for attr().
52
+ if (el.hasAttribute('aspect-ratio')) {
53
+ el.style.aspectRatio = el.getAttribute('aspect-ratio')!;
54
+ }
51
55
  if (context.syntax?.inline?.link === false || cache && cache.tagName !== 'IMG') return [[el], rest];
52
56
  return fmap(
53
57
  link as MediaParser,
@@ -46,7 +46,6 @@ describe('Unit: parser/inline/reference', () => {
46
46
  assert.deepStrictEqual(inspect(parser('[[@a]]')), [['<sup class="reference"><span><a href="/@a" class="account">@a</a></span></sup>'], '']);
47
47
  assert.deepStrictEqual(inspect(parser('[[http://host]]')), [['<sup class="reference"><span><a href="http://host" target="_blank">http://host</a></span></sup>'], '']);
48
48
  assert.deepStrictEqual(inspect(parser('[[![]{a}]]')), [['<sup class="reference"><span>!<a href="a">a</a></span></sup>'], '']);
49
- assert.deepStrictEqual(inspect(parser('[[<a>]]')), [['<sup class="reference"><span>&lt;a&gt;</span></sup>'], '']);
50
49
  assert.deepStrictEqual(inspect(parser('[[[a]]]')), [['<sup class="reference"><span>[a]</span></sup>'], '']);
51
50
  assert.deepStrictEqual(inspect(parser('[[[[a]]]]')), [['<sup class="reference"><span>[[a]]</span></sup>'], '']);
52
51
  assert.deepStrictEqual(inspect(parser('[[((a))]]')), [['<sup class="reference"><span><span class="paren">((a))</span></span></sup>'], '']);
@@ -43,7 +43,6 @@ describe('Unit: parser/inline/strong', () => {
43
43
  assert.deepStrictEqual(inspect(parser('**a*b*c**')), [['<strong>a<em>b</em>c</strong>'], '']);
44
44
  assert.deepStrictEqual(inspect(parser('**a*b*c**d')), [['<strong>a<em>b</em>c</strong>'], 'd']);
45
45
  assert.deepStrictEqual(inspect(parser('**`a`**')), [['<strong><code data-src="`a`">a</code></strong>'], '']);
46
- assert.deepStrictEqual(inspect(parser('**<bdi>**')), [['<strong>&lt;bdi&gt;</strong>'], '']);
47
46
  assert.deepStrictEqual(inspect(parser('**(*a*)**')), [['<strong><span class="paren">(<em>a</em>)</span></strong>'], '']);
48
47
  assert.deepStrictEqual(inspect(parser('**(**a**)**')), [['<strong><span class="paren">(<strong>a</strong>)</span></strong>'], '']);
49
48
  });
@@ -155,8 +155,6 @@ describe('Unit: parser/inline', () => {
155
155
  assert.deepStrictEqual(inspect(parser('[(([a]{#}))]{#}')), [['<a href="#"><span class="paren">(<span class="paren">([a]{#})</span>)</span></a>'], '']);
156
156
  assert.deepStrictEqual(inspect(parser('"[[""]]')), [['"', '<sup class="reference"><span>""</span></sup>'], '']);
157
157
  assert.deepStrictEqual(inspect(parser('<http://host>')), [['<', '<a href="http://host" target="_blank">http://host</a>', '>'], '']);
158
- assert.deepStrictEqual(inspect(parser('<<bdi>a<</bdi>')), [['<', '<bdi>a&lt;</bdi>'], '']);
159
- assert.deepStrictEqual(inspect(parser('*<bdi>*`</bdi>`')), [['<em>&lt;bdi&gt;</em>', '<code data-src="`</bdi>`">&lt;/bdi&gt;</code>'], '']);
160
158
  assert.deepStrictEqual(inspect(parser('[~http://host')), [['[', '~', '<a href="http://host" target="_blank">http://host</a>'], '']);
161
159
  assert.deepStrictEqual(inspect(parser('[~a@b')), [['[', '~', '<a class="email" href="mailto:a@b">a@b</a>'], '']);
162
160
  assert.deepStrictEqual(inspect(parser('[~~a~~]')), [['[', '<del>a</del>', ']'], '']);
@@ -4,7 +4,7 @@ import { union, focus, creator } from '../../combinator';
4
4
  import { str } from './str';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
- export const delimiter = /[\s\x00-\x7F]|\S#|[、。!?][^\S\n]*(?=\\\n)/;
7
+ export const delimiter = /[\s\x00-\x7F]|\S#|[()、。!?][^\S\n]*(?=\\\n)/;
8
8
  export const nonWhitespace = /[\S\n]|$/;
9
9
  export const nonAlphanumeric = /[^0-9A-Za-z]|\S#|$/;
10
10
  const repeat = str(/^(.)\1*/);
@@ -101,6 +101,19 @@ const isStartTight = reduce((source: string, context: MarkdownParser.Context, ex
101
101
  return source[0].trimStart() !== '';
102
102
  }
103
103
  }, (source, _, except = '') => `${source}\x1E${except}`);
104
+
105
+ export function isStartLooseNodes(nodes: readonly (HTMLElement | string)[]): boolean {
106
+ if (nodes.length === 0) return true;
107
+ for (let i = 0; i < nodes.length; ++i) {
108
+ const node = nodes[i];
109
+ if (isVisible(node)) return true;
110
+ if (typeof node === 'object') {
111
+ if (node.tagName === 'BR') break;
112
+ if (node.className === 'linebreak') break;
113
+ }
114
+ }
115
+ return false;
116
+ }
104
117
  export function isStartTightNodes(nodes: readonly (HTMLElement | string)[]): boolean {
105
118
  if (nodes.length === 0) return true;
106
119
  return isVisible(nodes[0], 0);
@@ -1,4 +1,4 @@
1
- import { Object } from 'spica/global';
1
+ import { undefined, Object } from 'spica/global';
2
2
  import { html } from 'typed-dom/dom';
3
3
 
4
4
  const extensions = [
@@ -13,6 +13,9 @@ export function video(source: HTMLImageElement, url: URL): HTMLVideoElement | un
13
13
  'data-type': 'video',
14
14
  ...Object.fromEntries([...source.attributes]
15
15
  .map(attr => [attr.name, attr.value])),
16
+ style: source.hasAttribute('aspect-ratio')
17
+ ? `aspect-ratio: ${source.getAttribute('aspect-ratio')};`
18
+ : undefined,
16
19
  muted: '',
17
20
  controls: '',
18
21
  });