securemark 0.254.2 → 0.255.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.255.0
4
+
5
+ - Change html parser not to use backtracking.
6
+
3
7
  ## 0.254.2
4
8
 
5
9
  - Refactoring.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! securemark v0.254.2 https://github.com/falsandtru/securemark | (c) 2017, falsandtru | UNLICENSED License */
1
+ /*! securemark v0.255.0 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"));
@@ -6219,14 +6219,17 @@ const attrspecs = {
6219
6219
  global_1.Object.setPrototypeOf(attrspecs, null);
6220
6220
  global_1.Object.values(attrspecs).forEach(o => global_1.Object.setPrototypeOf(o, null));
6221
6221
  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)(new RegExp(String.raw`^<(?!${TAGS.join('|')}\b)([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)))])))));
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, 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
6223
  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
6224
  // [...document.querySelectorAll('tbody > tr > td:first-child')].map(el => el.textContent.slice(1, -1))
6225
6225
 
6226
6226
  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
6227
 
6228
6228
  function elem(tag, as, bs, cs) {
6229
- if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag <${tag}>`, as, bs, cs);
6229
+ if (!tags.includes(tag)) return invalid('tag', `Invalid HTML tag name "${tag}"`, as, bs, cs);
6230
+ if (cs.length === 0) return invalid('tag', `Missing the closing HTML tag "</${tag}>"`, as, bs, cs);
6231
+ if (bs.length === 0) return invalid('content', `Missing the content`, as, bs, cs);
6232
+ if (!(0, util_1.isStartLooseNodes)(bs)) return invalid('content', `Missing the visible content in the same line`, as, bs, cs);
6230
6233
  const attrs = attributes('html', [], attrspecs[tag], as.slice(1, -1));
6231
6234
  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
6235
  }
@@ -7773,7 +7776,7 @@ exports.unescsource = (0, combinator_1.creator)(source => {
7773
7776
  Object.defineProperty(exports, "__esModule", ({
7774
7777
  value: true
7775
7778
  }));
7776
- exports.stringify = exports.trimBlankEnd = exports.trimBlankStart = exports.trimBlank = exports.isStartTightNodes = exports.startTight = exports.startLoose = exports.visualize = exports.blankWith = exports.regBlankStart = void 0;
7779
+ exports.stringify = exports.trimBlankEnd = exports.trimBlankStart = exports.trimBlank = exports.isStartTightNodes = exports.isStartLooseNodes = exports.startTight = exports.startLoose = exports.visualize = exports.blankWith = exports.regBlankStart = void 0;
7777
7780
 
7778
7781
  const global_1 = __webpack_require__(4128);
7779
7782
 
@@ -7877,6 +7880,24 @@ const isStartTight = (0, memoize_1.reduce)((source, context, except) => {
7877
7880
  }
7878
7881
  }, (source, _, except = '') => `${source}\x1E${except}`);
7879
7882
 
7883
+ function isStartLooseNodes(nodes) {
7884
+ if (nodes.length === 0) return true;
7885
+
7886
+ for (let i = 0; i < nodes.length; ++i) {
7887
+ const node = nodes[i];
7888
+ if (isVisible(node)) return true;
7889
+
7890
+ if (typeof node === 'object') {
7891
+ if (node.tagName === 'BR') break;
7892
+ if (node.className === 'linebreak') break;
7893
+ }
7894
+ }
7895
+
7896
+ return false;
7897
+ }
7898
+
7899
+ exports.isStartLooseNodes = isStartLooseNodes;
7900
+
7880
7901
  function isStartTightNodes(nodes) {
7881
7902
  if (nodes.length === 0) return true;
7882
7903
  return isVisible(nodes[0], 0);
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.2",
3
+ "version": "0.255.0",
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",
@@ -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,25 +32,31 @@ 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
- new RegExp(String.raw`^<(?!${TAGS.join('|')}\b)([a-z]+)(?=[^\S\n]|>)`),
46
+ /^<([a-z]+)(?=[^\S\n]|>)/,
44
47
  memoize(
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)
@@ -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);