securemark 0.257.0 → 0.257.3

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +21 -8
  3. package/dist/index.js +94 -102
  4. package/markdown.d.ts +21 -22
  5. package/package.json +1 -1
  6. package/src/combinator/data/parser/inits.ts +1 -1
  7. package/src/combinator/data/parser/sequence.ts +1 -1
  8. package/src/debug.test.ts +1 -1
  9. package/src/parser/block/table.test.ts +5 -0
  10. package/src/parser/block/table.ts +6 -5
  11. package/src/parser/inline/annotation.test.ts +6 -5
  12. package/src/parser/inline/annotation.ts +5 -4
  13. package/src/parser/inline/autolink/account.ts +3 -7
  14. package/src/parser/inline/autolink/anchor.ts +3 -7
  15. package/src/parser/inline/autolink/hashnum.ts +3 -7
  16. package/src/parser/inline/autolink/hashtag.ts +3 -7
  17. package/src/parser/inline/autolink/url.test.ts +1 -0
  18. package/src/parser/inline/autolink/url.ts +4 -5
  19. package/src/parser/inline/bracket.test.ts +3 -1
  20. package/src/parser/inline/bracket.ts +6 -6
  21. package/src/parser/inline/comment.test.ts +1 -0
  22. package/src/parser/inline/deletion.ts +1 -1
  23. package/src/parser/inline/extension/index.ts +2 -2
  24. package/src/parser/inline/extension/placeholder.ts +2 -2
  25. package/src/parser/inline/insertion.ts +1 -1
  26. package/src/parser/inline/link.ts +55 -14
  27. package/src/parser/inline/mark.ts +1 -1
  28. package/src/parser/inline/math.ts +8 -9
  29. package/src/parser/inline/media.ts +5 -5
  30. package/src/parser/inline/reference.test.ts +6 -5
  31. package/src/parser/inline/reference.ts +7 -15
  32. package/src/parser/inline/template.ts +1 -1
  33. package/src/parser/inline.test.ts +5 -3
  34. package/src/parser/inline.ts +1 -0
  35. package/src/parser/util.ts +18 -18
package/markdown.d.ts CHANGED
@@ -1,22 +1,6 @@
1
1
  import { Parser, Ctx } from './src/combinator/data/parser';
2
2
  import { Dict } from 'spica/dict';
3
3
 
4
- /*
5
-
6
- Operator precedence
7
-
8
- 9: \n, \\\n
9
- 8: `, "
10
- 7: $
11
- 6: (()), [[]]
12
- 5: <tag></tag>
13
- 4: [% %]
14
- 3: (), [], {}
15
- 2: ==, ++, ~~
16
- 1: *, **
17
-
18
- */
19
-
20
4
  declare abstract class Markdown<T> {
21
5
  private parser?: T;
22
6
  }
@@ -869,11 +853,20 @@ export namespace MarkdownParser {
869
853
  // { uri }
870
854
  // [abc]{uri nofollow}
871
855
  Inline<'link'>,
872
- Parser<HTMLAnchorElement, Context, [
856
+ Parser<HTMLElement | string, Context, [
873
857
  LinkParser.ContentParser,
874
858
  LinkParser.ParameterParser,
875
859
  ]> {
876
860
  }
861
+ export interface TextLinkParser extends
862
+ // { uri }
863
+ // [abc]{uri nofollow}
864
+ Inline<'textlink'>,
865
+ Parser<HTMLAnchorElement, Context, [
866
+ LinkParser.TextParser,
867
+ LinkParser.ParameterParser,
868
+ ]> {
869
+ }
877
870
  export namespace LinkParser {
878
871
  export interface ContentParser extends
879
872
  Inline<'link/content'>,
@@ -883,6 +876,12 @@ export namespace MarkdownParser {
883
876
  InlineParser,
884
877
  ]> {
885
878
  }
879
+ export interface TextParser extends
880
+ Inline<'link/text'>,
881
+ Parser<string[], Context, [
882
+ SourceParser.UnescapableSourceParser,
883
+ ]> {
884
+ }
886
885
  export interface ParameterParser extends
887
886
  Inline<'link/parameter'>,
888
887
  Parser<string[], Context, [
@@ -1107,7 +1106,7 @@ export namespace MarkdownParser {
1107
1106
  // https://host
1108
1107
  Inline<'url'>,
1109
1108
  Parser<HTMLAnchorElement, Context, [
1110
- LinkParser,
1109
+ TextLinkParser,
1111
1110
  ]> {
1112
1111
  }
1113
1112
  export namespace UrlParser {
@@ -1149,28 +1148,28 @@ export namespace MarkdownParser {
1149
1148
  // @user
1150
1149
  Inline<'account'>,
1151
1150
  Parser<HTMLAnchorElement, Context, [
1152
- LinkParser,
1151
+ TextLinkParser,
1153
1152
  ]> {
1154
1153
  }
1155
1154
  export interface HashtagParser extends
1156
1155
  // #tag
1157
1156
  Inline<'hashtag'>,
1158
1157
  Parser<HTMLAnchorElement, Context, [
1159
- LinkParser,
1158
+ TextLinkParser,
1160
1159
  ]> {
1161
1160
  }
1162
1161
  export interface HashnumParser extends
1163
1162
  // #1
1164
1163
  Inline<'hashnum'>,
1165
1164
  Parser<HTMLAnchorElement, Context, [
1166
- LinkParser,
1165
+ TextLinkParser,
1167
1166
  ]> {
1168
1167
  }
1169
1168
  export interface AnchorParser extends
1170
1169
  // >>1
1171
1170
  Inline<'anchor'>,
1172
1171
  Parser<HTMLAnchorElement, Context, [
1173
- LinkParser,
1172
+ TextLinkParser,
1174
1173
  ]> {
1175
1174
  }
1176
1175
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.257.0",
3
+ "version": "0.257.3",
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",
@@ -11,10 +11,10 @@ export function inits<T, D extends Parser<T>[]>(parsers: D): Parser<T, Ctx, D> {
11
11
  let nodes: T[] | undefined;
12
12
  for (let i = 0, len = parsers.length; i < len; ++i) {
13
13
  if (rest === '') break;
14
+ if (context.delimiters?.match(rest, context.precedence)) break;
14
15
  const result = parsers[i](rest, context);
15
16
  assert(check(rest, result));
16
17
  if (!result) break;
17
- assert(!context?.delimiters?.match(rest, context.precedence));
18
18
  nodes = nodes
19
19
  ? push(nodes, eval(result))
20
20
  : eval(result);
@@ -11,10 +11,10 @@ export function sequence<T, D extends Parser<T>[]>(parsers: D): Parser<T, Ctx, D
11
11
  let nodes: T[] | undefined;
12
12
  for (let i = 0, len = parsers.length; i < len; ++i) {
13
13
  if (rest === '') return;
14
+ if (context.delimiters?.match(rest, context.precedence)) break;
14
15
  const result = parsers[i](rest, context);
15
16
  assert(check(rest, result));
16
17
  if (!result) return;
17
- assert(!context?.delimiters?.match(rest, context.precedence));
18
18
  nodes = nodes
19
19
  ? push(nodes, eval(result))
20
20
  : eval(result);
package/src/debug.test.ts CHANGED
@@ -5,7 +5,7 @@ import { querySelector, querySelectorAll } from 'typed-dom/query';
5
5
  export function inspect(result: Result<HTMLElement | string>, until: number | string = Infinity): Result<string> {
6
6
  return result && [
7
7
  eval(result).map((node, i, nodes) => {
8
- assert(node || node === '' && '([{'.includes(nodes[i + 1] as string));
8
+ assert(node || node === '' && '([{'.includes(nodes[i + 1][0]));
9
9
  if (typeof node === 'string') return node;
10
10
  node = node.cloneNode(true);
11
11
  assert(!querySelector(node, '.invalid[data-invalid-message$="."]'));
@@ -29,6 +29,11 @@ describe('Unit: parser/block/table', () => {
29
29
  assert.deepStrictEqual(inspect(parser('|\n|-\n|')), [['<table><thead><tr></tr></thead><tbody><tr></tr></tbody></table>'], '']);
30
30
  assert.deepStrictEqual(inspect(parser('||\n|-|\n||')), [['<table><thead><tr><th></th></tr></thead><tbody><tr><td></td></tr></tbody></table>'], '']);
31
31
  assert.deepStrictEqual(inspect(parser('|||\n|-|-|\n|||')), [['<table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td></td><td></td></tr></tbody></table>'], '']);
32
+ assert.deepStrictEqual(inspect(parser('|"|\n|-\n|')), [['<table><thead><tr><th>"</th></tr></thead><tbody><tr></tr></tbody></table>'], '']);
33
+ assert.deepStrictEqual(inspect(parser('|`|\n|-\n|')), [['<table><thead><tr><th>`</th></tr></thead><tbody><tr></tr></tbody></table>'], '']);
34
+ assert.deepStrictEqual(inspect(parser('|`|`|\n|-\n|')), [['<table><thead><tr><th><code data-src="`|`">|</code></th></tr></thead><tbody><tr></tr></tbody></table>'], '']);
35
+ assert.deepStrictEqual(inspect(parser('|((|\n|-\n|')), [['<table><thead><tr><th>((</th></tr></thead><tbody><tr></tr></tbody></table>'], '']);
36
+ assert.deepStrictEqual(inspect(parser('|${|\n|-\n|')), [['<table><thead><tr><th>${</th></tr></thead><tbody><tr></tr></tbody></table>'], '']);
32
37
  assert.deepStrictEqual(inspect(parser('|a|b|\n|-|-|\n|1|2|')), [['<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>'], '']);
33
38
  assert.deepStrictEqual(inspect(parser('|a|b\n|-|-\n|1|2')), [['<table><thead><tr><th>a</th><th>b</th></tr></thead><tbody><tr><td>1</td><td>2</td></tr></tbody></table>'], '']);
34
39
  assert.deepStrictEqual(inspect(parser('|a|\n|-|\n|1|')), [['<table><thead><tr><th>a</th></tr></thead><tbody><tr><td>1</td></tr></tbody></table>'], '']);
@@ -2,6 +2,7 @@ import { TableParser } from '../block';
2
2
  import { union, sequence, some, block, line, validate, focus, rewrite, creator, surround, open, fallback, lazy, fmap } from '../../combinator';
3
3
  import { inline } from '../inline';
4
4
  import { contentline } from '../source';
5
+ import { trimNode } from '../util';
5
6
  import { html, defrag } from 'typed-dom/dom';
6
7
  import { push } from 'spica/array';
7
8
 
@@ -24,7 +25,7 @@ export const table: TableParser = lazy(() => block(fmap(validate(
24
25
  ])));
25
26
 
26
27
  const row = <P extends CellParser | AlignParser>(parser: P, optional: boolean): RowParser<P> => creator(fallback(fmap(
27
- line(surround(/^(?=\|)/, some(union([parser])), /^\|?\s*$/, optional)),
28
+ line(surround(/^(?=\|)/, some(union([parser])), /^[|\\]?\s*$/, optional)),
28
29
  es => [html('tr', es)]),
29
30
  rewrite(contentline, source => [[
30
31
  html('tr', {
@@ -46,17 +47,17 @@ const align: AlignParser = creator(fmap(open(
46
47
  ns => [html('td', defrag(ns))]));
47
48
 
48
49
  const cell: CellParser = surround(
49
- /^\|(?:\\?\s)*(?=\S)/,
50
- some(union([inline]), /^(?:\\?\s)*(?=\||\\?$)/),
50
+ /^\|\s*(?=\S)/,
51
+ some(union([inline]), /^\|/, [[/^[|\\]?\s*$/, 9]]),
51
52
  /^[^|]*/, true);
52
53
 
53
54
  const head: CellParser.HeadParser = creator(fmap(
54
55
  cell,
55
- ns => [html('th', defrag(ns))]));
56
+ ns => [html('th', trimNode(defrag(ns)))]));
56
57
 
57
58
  const data: CellParser.DataParser = creator(fmap(
58
59
  cell,
59
- ns => [html('td', defrag(ns))]));
60
+ ns => [html('td', trimNode(defrag(ns)))]));
60
61
 
61
62
  function format(rows: HTMLTableRowElement[]): HTMLTableRowElement[] {
62
63
  const aligns = rows[0].classList.contains('invalid')
@@ -14,14 +14,15 @@ describe('Unit: parser/inline/annotation', () => {
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')), [['', '(('], ' (a']);
17
18
  assert.deepStrictEqual(inspect(parser('((\n))')), undefined);
18
19
  assert.deepStrictEqual(inspect(parser('((\na))')), undefined);
19
20
  assert.deepStrictEqual(inspect(parser('((\\\na))')), undefined);
20
- assert.deepStrictEqual(inspect(parser('((a\n))')), [['(('], 'a\n))']);
21
- assert.deepStrictEqual(inspect(parser('((a\\\n))')), [['(('], 'a\\\n))']);
22
- assert.deepStrictEqual(inspect(parser('((a\nb))')), [['(('], 'a\nb))']);
23
- assert.deepStrictEqual(inspect(parser('((a\\\nb))')), [['(('], 'a\\\nb))']);
24
- assert.deepStrictEqual(inspect(parser('((*a\nb*))')), [['(('], '*a\nb*))']);
21
+ assert.deepStrictEqual(inspect(parser('((a\n))')), [['', '(('], 'a\n))']);
22
+ assert.deepStrictEqual(inspect(parser('((a\\\n))')), [['', '(('], 'a\\\n))']);
23
+ assert.deepStrictEqual(inspect(parser('((a\nb))')), [['', '(('], 'a\nb))']);
24
+ assert.deepStrictEqual(inspect(parser('((a\\\nb))')), [['', '(('], 'a\\\nb))']);
25
+ assert.deepStrictEqual(inspect(parser('((*a\nb*))')), [['', '(('], '*a\nb*))']);
25
26
  assert.deepStrictEqual(inspect(parser('((\\))')), undefined);
26
27
  assert.deepStrictEqual(inspect(parser('((a)b))')), undefined);
27
28
  assert.deepStrictEqual(inspect(parser('(((a))')), undefined);
@@ -2,13 +2,14 @@ import { undefined } from 'spica/global';
2
2
  import { AnnotationParser } from '../inline';
3
3
  import { union, some, validate, guard, context, precedence, creator, recursion, surround, lazy } from '../../combinator';
4
4
  import { inline } from '../inline';
5
- import { optimize } from './reference';
6
- import { trimBlankStart, trimNodeEnd } from '../util';
5
+ import { optimize } from './link';
6
+ import { startLoose, trimNode } from '../util';
7
7
  import { html, defrag } from 'typed-dom/dom';
8
8
 
9
9
  export const annotation: AnnotationParser = lazy(() => creator(recursion(precedence(6, validate('((', surround(
10
10
  '((',
11
11
  guard(context => context.syntax?.inline?.annotation ?? true,
12
+ startLoose(
12
13
  context({ syntax: { inline: {
13
14
  annotation: false,
14
15
  // Redundant
@@ -20,8 +21,8 @@ export const annotation: AnnotationParser = lazy(() => creator(recursion(precede
20
21
  //link: true,
21
22
  //autolink: true,
22
23
  }}, delimiters: undefined },
23
- trimBlankStart(some(union([inline]), ')', [[/^\\?\n/, 9], [')', 3], ['))', 6]])))),
24
+ some(union([inline]), ')', [[/^\\?\n/, 9], [')', 2], ['))', 6]])), ')')),
24
25
  '))',
25
26
  false,
26
- ([, ns], rest) => [[html('sup', { class: 'annotation' }, [html('span', trimNodeEnd(defrag(ns)))])], rest],
27
+ ([, ns], rest) => [[html('sup', { class: 'annotation' }, [html('span', trimNode(defrag(ns)))])], rest],
27
28
  ([, ns, rest], next) => next[0] === ')' ? undefined : optimize('((', ns, rest)))))));
@@ -1,6 +1,6 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, tails, verify, rewrite, context, open, convert, fmap, lazy } from '../../../combinator';
3
- import { link } from '../link';
2
+ import { union, tails, verify, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
+ import { textlink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { define } from 'typed-dom/dom';
6
6
 
@@ -17,10 +17,6 @@ export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
17
17
  str(/^[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*/),
18
18
  ([source]) => source.length <= 64),
19
19
  ])),
20
- context({ syntax: { inline: {
21
- link: true,
22
- autolink: false,
23
- }}},
24
20
  convert(
25
21
  source =>
26
22
  `[${source}]{ ${
@@ -28,5 +24,5 @@ export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
28
24
  ? `https://${source.slice(1).replace('/', '/@')}`
29
25
  : `/${source}`
30
26
  } }`,
31
- union([link])))),
27
+ union([textlink]))),
32
28
  ([el]) => [define(el, { class: 'account' })]));
@@ -1,6 +1,6 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, validate, focus, context, convert, fmap, lazy } from '../../../combinator';
3
- import { link } from '../link';
2
+ import { union, validate, focus, convert, fmap, lazy } from '../../../combinator';
3
+ import { textlink } from '../link';
4
4
  import { define } from 'typed-dom/dom';
5
5
 
6
6
  // Timeline(pseudonym): user/tid
@@ -14,10 +14,6 @@ import { define } from 'typed-dom/dom';
14
14
 
15
15
  export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>', fmap(focus(
16
16
  /^>>(?:[A-Za-z][0-9A-Za-z]*(?:-[0-9A-Za-z]+)*\/)?[0-9A-Za-z]+(?:-[0-9A-Za-z]+)*(?![0-9A-Za-z@#:])/,
17
- context({ syntax: { inline: {
18
- link: true,
19
- autolink: false,
20
- }}},
21
17
  convert(
22
18
  source =>
23
19
  `[${source}]{ ${
@@ -25,5 +21,5 @@ export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>', fma
25
21
  ? `/@${source.slice(2).replace('/', '/timeline/')}`
26
22
  : `?at=${source.slice(2)}`
27
23
  } }`,
28
- union([link])))),
24
+ union([textlink]))),
29
25
  ([el]) => [define(el, { class: 'anchor' })])));
@@ -1,17 +1,13 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, rewrite, context, open, convert, fmap, lazy } from '../../../combinator';
3
- import { link } from '../link';
2
+ import { union, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
+ import { textlink } from '../link';
4
4
  import { emoji } from './hashtag';
5
5
  import { str } from '../../source';
6
6
  import { define } from 'typed-dom/dom';
7
7
 
8
8
  export const hashnum: AutolinkParser.HashnumParser = lazy(() => fmap(rewrite(
9
9
  open('#', str(new RegExp(/^[0-9]{1,16}(?![^\p{C}\p{S}\p{P}\s]|emoji|['_])/u.source.replace(/emoji/, emoji), 'u'))),
10
- context({ syntax: { inline: {
11
- link: true,
12
- autolink: false,
13
- }}},
14
10
  convert(
15
11
  source => `[${source}]{ ${source.slice(1)} }`,
16
- union([link])))),
12
+ union([textlink]))),
17
13
  ([el]) => [define(el, { class: 'hashnum', href: null })]));
@@ -1,6 +1,6 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, tails, verify, rewrite, context, open, convert, fmap, lazy } from '../../../combinator';
3
- import { link } from '../link';
2
+ import { union, tails, verify, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
+ import { textlink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { define } from 'typed-dom/dom';
6
6
 
@@ -24,10 +24,6 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
24
24
  ].join('').replace(/emoji/g, emoji), 'u')),
25
25
  ([source]) => source.length <= 128),
26
26
  ])),
27
- context({ syntax: { inline: {
28
- link: true,
29
- autolink: false,
30
- }}},
31
27
  convert(
32
28
  source =>
33
29
  `[${source}]{ ${
@@ -35,5 +31,5 @@ export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
35
31
  ? `https://${source.slice(1).replace('/', '/hashtags/')}`
36
32
  : `/hashtags/${source.slice(1)}`
37
33
  } }`,
38
- union([link])))),
34
+ union([textlink]))),
39
35
  ([el]) => [define(el, { class: 'hashtag' }, el.innerText)]));
@@ -72,6 +72,7 @@ describe('Unit: parser/inline/autolink/url', () => {
72
72
  assert.deepStrictEqual(inspect(parser('http://host>')), [['<a href="http://host" target="_blank">http://host</a>'], '>']);
73
73
  assert.deepStrictEqual(inspect(parser('http://host(')), [['<a href="http://host" target="_blank">http://host</a>'], '(']);
74
74
  assert.deepStrictEqual(inspect(parser('http://host)')), [['<a href="http://host" target="_blank">http://host</a>'], ')']);
75
+ assert.deepStrictEqual(inspect(parser('http://host\\"')), [['<a href="http://host" target="_blank">http://host</a>'], '\\"']);
75
76
  assert.deepStrictEqual(inspect(parser('http://host!?**.*--++==~~^^')), [['<a href="http://host" target="_blank">http://host</a>'], '!?**.*--++==~~^^']);
76
77
  });
77
78
 
@@ -1,10 +1,9 @@
1
1
  import { AutolinkParser } from '../../inline';
2
2
  import { union, some, validate, focus, rewrite, precedence, creator, convert, surround, open, lazy } from '../../../combinator';
3
- import { link } from '../link';
3
+ import { textlink } from '../link';
4
4
  import { unescsource } from '../../source';
5
- import { clean } from '../../util';
6
5
 
7
- const closer = /^[-+*=~^,.;:!?]*(?=["`|\[\](){}<>]|\\?$)/;
6
+ const closer = /^[-+*=~^,.;:!?]*(?=[\\"`|\[\](){}<>]|$)/;
8
7
 
9
8
  export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'https://'], rewrite(
10
9
  open(
@@ -12,9 +11,9 @@ export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'ht
12
11
  focus(/^[\x21-\x7E]+/, some(union([bracket, some(unescsource, closer)])))),
13
12
  convert(
14
13
  url => `{ ${url} }`,
15
- clean(union([link]))))));
14
+ union([textlink])))));
16
15
 
17
- const bracket: AutolinkParser.UrlParser.BracketParser = lazy(() => creator(precedence(3, union([
16
+ const bracket: AutolinkParser.UrlParser.BracketParser = lazy(() => creator(precedence(2, union([
18
17
  surround('(', some(union([bracket, unescsource]), ')'), ')', true),
19
18
  surround('[', some(union([bracket, unescsource]), ']'), ']', true),
20
19
  surround('{', some(union([bracket, unescsource]), '}'), '}', true),
@@ -39,6 +39,7 @@ describe('Unit: parser/inline/bracket', () => {
39
39
  assert.deepStrictEqual(inspect(parser('(ABBR, ABBR)')), [['(', 'ABBR, ABBR', ')'], '']);
40
40
  assert.deepStrictEqual(inspect(parser('(\\a)')), [['<span class="paren">(a)</span>'], '']);
41
41
  assert.deepStrictEqual(inspect(parser('(==)')), [['<span class="paren">(==)</span>'], '']);
42
+ assert.deepStrictEqual(inspect(parser('($)$')), [['', '(', '<span class="math" translate="no" data-src="$)$">$)$</span>'], '']);
42
43
  assert.deepStrictEqual(inspect(parser(')')), undefined);
43
44
  assert.deepStrictEqual(inspect(parser('(1,2)')), [['(', '1,2', ')'], '']);
44
45
  assert.deepStrictEqual(inspect(parser('(0-1)')), [['(', '0-1', ')'], '']);
@@ -55,6 +56,7 @@ describe('Unit: parser/inline/bracket', () => {
55
56
  assert.deepStrictEqual(inspect(parser('[a')), [['', '[', 'a'], '']);
56
57
  assert.deepStrictEqual(inspect(parser('[a]')), [['[', 'a', ']'], '']);
57
58
  assert.deepStrictEqual(inspect(parser('[==]')), [['[', '==', ']'], '']);
59
+ assert.deepStrictEqual(inspect(parser('[$]$')), [['', '[', '<span class="math" translate="no" data-src="$]$">$]$</span>'], '']);
58
60
  assert.deepStrictEqual(inspect(parser(']')), undefined);
59
61
  });
60
62
 
@@ -73,7 +75,7 @@ describe('Unit: parser/inline/bracket', () => {
73
75
  assert.deepStrictEqual(inspect(parser('"a')), [['"', 'a'], '']);
74
76
  assert.deepStrictEqual(inspect(parser('"a"')), [['"', 'a', '"'], '']);
75
77
  assert.deepStrictEqual(inspect(parser('"(")"')), [['"', '', '(', '"'], ')"']);
76
- assert.deepStrictEqual(inspect(parser('"(("')), [['"', '((', '"'], '']);
78
+ assert.deepStrictEqual(inspect(parser('"(("')), [['"', '', '((', '"'], '']);
77
79
  assert.deepStrictEqual(inspect(parser('"(\\")"')), [['"', '<span class="paren">(")</span>', '"'], '']);
78
80
  assert.deepStrictEqual(inspect(parser('"(\n)"')), [['"', '<span class="paren">(<br>)</span>', '"'], '']);
79
81
  assert.deepStrictEqual(inspect(parser('"(\\\n)"')), [['"', '<span class="paren">(<span class="linebreak"> </span>)</span>', '"'], '']);
@@ -9,18 +9,18 @@ import { unshift, push } from 'spica/array';
9
9
  const index = /^[0-9A-Za-z]+(?:(?:[.-]|, )[0-9A-Za-z]+)*/;
10
10
 
11
11
  export const bracket: BracketParser = lazy(() => creator(0, union([
12
- surround(str('('), precedence(3, str(index)), str(')')),
13
- surround(str('('), precedence(3, some(inline, ')', [[')', 3]])), str(')'), true,
12
+ surround(str('('), precedence(2, str(index)), str(')')),
13
+ surround(str('('), precedence(2, some(inline, ')', [[')', 2]])), str(')'), true,
14
14
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
15
15
  ([as, bs = []], rest) => [unshift([''], unshift(as, bs)), rest]),
16
- surround(str('('), precedence(3, str(new RegExp(index.source.replace(', ', '[,、]').replace(/[09AZaz.]|\-(?!\w)/g, c => c.trimStart() && String.fromCharCode(c.charCodeAt(0) + 0xFEE0))))), str(')')),
17
- surround(str('('), precedence(3, some(inline, ')', [[')', 3]])), str(')'), true,
16
+ surround(str('('), precedence(2, str(new RegExp(index.source.replace(', ', '[,、]').replace(/[09AZaz.]|\-(?!\w)/g, c => c.trimStart() && String.fromCharCode(c.charCodeAt(0) + 0xFEE0))))), str(')')),
17
+ surround(str('('), precedence(2, some(inline, ')', [[')', 2]])), str(')'), true,
18
18
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
19
19
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
20
- surround(str('['), precedence(3, some(inline, ']', [[']', 3]])), str(']'), true,
20
+ surround(str('['), precedence(2, some(inline, ']', [[']', 2]])), str(']'), true,
21
21
  undefined,
22
22
  ([as, bs = []], rest) => [unshift([''], unshift(as, bs)), rest]),
23
- surround(str('{'), precedence(3, some(inline, '}', [['}', 3]])), str('}'), true,
23
+ surround(str('{'), precedence(2, some(inline, '}', [['}', 2]])), str('}'), true,
24
24
  undefined,
25
25
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
26
26
  // Control media blinking in editing rather than control confusion of pairs of quote marks.
@@ -57,6 +57,7 @@ describe('Unit: parser/inline/comment', () => {
57
57
  assert.deepStrictEqual(inspect(parser('[% &amp;copy; %]')), [['<span class="comment"><input type="checkbox"><span>[% &amp;copy; %]</span></span>'], '']);
58
58
  assert.deepStrictEqual(inspect(parser('[% [ %]')), [['<span class="comment"><input type="checkbox"><span>[% [ %]</span></span>'], '']);
59
59
  assert.deepStrictEqual(inspect(parser('[% \\ a %]')), [['<span class="comment"><input type="checkbox"><span>[% a %]</span></span>'], '']);
60
+ assert.deepStrictEqual(inspect(parser('[% $-a %]$')), [['<span class="comment"><input type="checkbox"><span>[% <a class="label" data-label="$-a">$-a</a> %]</span></span>'], '$']);
60
61
  });
61
62
 
62
63
  });
@@ -6,7 +6,7 @@ import { blankWith } from '../util';
6
6
  import { html, defrag } from 'typed-dom/dom';
7
7
  import { unshift } from 'spica/array';
8
8
 
9
- export const deletion: DeletionParser = lazy(() => creator(precedence(2, surround(
9
+ export const deletion: DeletionParser = lazy(() => creator(precedence(1, surround(
10
10
  str('~~'),
11
11
  some(union([
12
12
  some(inline, blankWith('\n', '~~')),
@@ -9,7 +9,7 @@ import { html, define, defrag } from 'typed-dom/dom';
9
9
 
10
10
  import IndexParser = ExtensionParser.IndexParser;
11
11
 
12
- export const index: IndexParser = lazy(() => validate('[#', creator(precedence(3, fmap(indexee(surround(
12
+ export const index: IndexParser = lazy(() => validate('[#', creator(precedence(2, fmap(indexee(surround(
13
13
  '[#',
14
14
  guard(context => context.syntax?.inline?.index ?? true,
15
15
  startTight(
@@ -25,7 +25,7 @@ export const index: IndexParser = lazy(() => validate('[#', creator(precedence(3
25
25
  open(stropt(/^\|?/), trimBlankEnd(some(union([
26
26
  signature,
27
27
  inline,
28
- ]), ']', [[/^\\?\n/, 9], [']', 3]])), true)))),
28
+ ]), ']', [[/^\\?\n/, 9], [']', 2]])), true)))),
29
29
  ']',
30
30
  false,
31
31
  ([, ns], rest) => [[html('a', defrag(ns))], rest])),
@@ -10,9 +10,9 @@ import { unshift } from 'spica/array';
10
10
 
11
11
  // All syntax surrounded by square brackets shouldn't contain line breaks.
12
12
 
13
- export const placeholder: ExtensionParser.PlaceholderParser = lazy(() => validate(['[:', '[^'], creator(precedence(3, surround(
13
+ export const placeholder: ExtensionParser.PlaceholderParser = lazy(() => validate(['[:', '[^'], creator(precedence(2, surround(
14
14
  str(/^\[[:^]/),
15
- startTight(some(union([inline]), ']', [[/^\\?\n/, 9], [']', 3]])),
15
+ startTight(some(union([inline]), ']', [[/^\\?\n/, 9], [']', 2]])),
16
16
  str(']'), false,
17
17
  ([as, bs], rest) => [[
18
18
  html('span', {
@@ -6,7 +6,7 @@ import { blankWith } from '../util';
6
6
  import { html, defrag } from 'typed-dom/dom';
7
7
  import { unshift } from 'spica/array';
8
8
 
9
- export const insertion: InsertionParser = lazy(() => creator(precedence(2, surround(
9
+ export const insertion: InsertionParser = lazy(() => creator(precedence(1, surround(
10
10
  str('++'),
11
11
  some(union([
12
12
  some(inline, blankWith('\n', '++')),
@@ -1,12 +1,12 @@
1
1
  import { undefined, location, encodeURI, decodeURI, Location } from 'spica/global';
2
- import { LinkParser } from '../inline';
3
- import { eval } from '../../combinator/data/parser';
4
- import { union, inits, tails, some, validate, guard, context, precedence, creator, surround, open, dup, reverse, lazy, fmap, bind } from '../../combinator';
2
+ import { LinkParser, TextLinkParser } from '../inline';
3
+ import { Result, eval } from '../../combinator/data/parser';
4
+ import { union, inits, tails, subsequence, some, validate, guard, context, precedence, creator, surround, open, dup, reverse, lazy, fmap, bind } from '../../combinator';
5
5
  import { inline, media, shortmedia } from '../inline';
6
6
  import { attributes } from './html';
7
7
  import { autolink } from '../autolink';
8
- import { str } from '../source';
9
- import { trimBlankStart, trimNodeEnd, stringify } from '../util';
8
+ import { unescsource, str } from '../source';
9
+ import { trimNode, stringify } from '../util';
10
10
  import { html, define, defrag } from 'typed-dom/dom';
11
11
  import { ReadonlyURL } from 'spica/url';
12
12
 
@@ -15,9 +15,9 @@ const optspec = {
15
15
  } as const;
16
16
  Object.setPrototypeOf(optspec, null);
17
17
 
18
- export const link: LinkParser = lazy(() => validate(['[', '{'], creator(10, precedence(3, bind(
18
+ export const link: LinkParser = lazy(() => validate(['[', '{'], creator(10, precedence(2, bind(
19
19
  guard(context => context.syntax?.inline?.link ?? true,
20
- reverse(tails([
20
+ fmap(subsequence([
21
21
  context({ syntax: { inline: {
22
22
  link: false,
23
23
  }}},
@@ -36,15 +36,22 @@ export const link: LinkParser = lazy(() => validate(['[', '{'], creator(10, prec
36
36
  media: false,
37
37
  autolink: false,
38
38
  }}},
39
- trimBlankStart(some(inline, ']', [[/^\\?\n/, 9], [']', 3]]))),
39
+ some(inline, ']', [[/^\\?\n/, 9], [']', 2]])),
40
40
  ']',
41
- true),
41
+ true,
42
+ undefined,
43
+ ([, ns = [], rest], next) => next[0] === ']' ? undefined : optimize('[', ns, rest)),
42
44
  ]))),
45
+ // 全体の失敗が確定した時も解析し予算を浪費している
43
46
  dup(surround(/^{(?![{}])/, inits([uri, some(option)]), /^[^\S\n]*}/)),
44
- ]))),
45
- ([params, content = []]: [string[], (HTMLElement | string)[]], rest, context) => {
47
+ ]),
48
+ ([as, bs = []]) => bs[0] === '\r' && bs.shift() ? [as, bs] : as[0] === '\r' && as.shift() ? [[], as] : [as, []])),
49
+ ([content, params]: [(HTMLElement | string)[], string[]], rest, context) => {
50
+ if (params.length === 0) return;
51
+ assert(content[0] !== '' || params.length === 0);
52
+ if (content[0] === '') return [content, rest];
46
53
  assert(params.every(p => typeof p === 'string'));
47
- content = trimNodeEnd(content);
54
+ if (content.length !== 0 && trimNode(content).length === 0) return;
48
55
  if (eval(some(autolink)(stringify(content), context))?.some(node => typeof node === 'object')) return;
49
56
  assert(!html('div', content).querySelector('a, .media, .annotation, .reference') || (content[0] as HTMLElement).matches('.media'));
50
57
  const INSECURE_URI = params.shift()!;
@@ -62,10 +69,35 @@ export const link: LinkParser = lazy(() => validate(['[', '{'], creator(10, prec
62
69
  return [[define(el, attributes('link', [], optspec, params))], rest];
63
70
  })))));
64
71
 
65
- export const uri: LinkParser.ParameterParser.UriParser = union([
72
+ export const textlink: TextLinkParser = lazy(() => validate(['[', '{'], creator(10, precedence(2, bind(
73
+ reverse(tails([
74
+ dup(surround('[', some(union([unescsource]), ']'), ']')),
75
+ dup(surround(/^{(?![{}])/, inits([uri, some(option)]), /^[^\S\n]*}/)),
76
+ ])),
77
+ ([params, content = []], rest, context) => {
78
+ assert(params[0] === '\r');
79
+ params.shift();
80
+ assert(params.every(p => typeof p === 'string'));
81
+ trimNode(content);
82
+ const INSECURE_URI = params.shift()!;
83
+ assert(INSECURE_URI === INSECURE_URI.trim());
84
+ assert(!INSECURE_URI.match(/\s/));
85
+ const el = elem(
86
+ INSECURE_URI,
87
+ defrag(content),
88
+ new ReadonlyURL(
89
+ resolve(INSECURE_URI, context.host ?? location, context.url ?? context.host ?? location),
90
+ context.host?.href || location.href),
91
+ context.host?.origin || location.origin);
92
+ assert(!el.classList.contains('invalid'));
93
+ assert(el.classList.length === 0);
94
+ return [[define(el, attributes('link', [], optspec, params))], rest];
95
+ })))));
96
+
97
+ export const uri: LinkParser.ParameterParser.UriParser = fmap(union([
66
98
  open(/^[^\S\n]+/, str(/^\S+/)),
67
99
  str(/^[^\s{}]+/),
68
- ]);
100
+ ]), ([uri]) => ['\r', uri]);
69
101
 
70
102
  export const option: LinkParser.ParameterParser.OptionParser = union([
71
103
  fmap(str(/^[^\S\n]+nofollow(?=[^\S\n]|})/), () => [` rel="nofollow"`]),
@@ -163,3 +195,12 @@ function decode(uri: string): string {
163
195
  return uri.replace(/\s+/g, encodeURI);
164
196
  }
165
197
  }
198
+
199
+ export function optimize(opener: string, ns: readonly (string | HTMLElement)[], rest: string): Result<string> {
200
+ let count = 0;
201
+ for (let i = 0; i < ns.length - 1; i += 2) {
202
+ if (ns[i] !== '' || ns[i + 1] !== opener[0]) break;
203
+ ++count;
204
+ }
205
+ return [['', opener[0].repeat(opener.length + count)], rest.slice(count)];
206
+ }
@@ -6,7 +6,7 @@ import { startTight, blankWith } from '../util';
6
6
  import { html, defrag } from 'typed-dom/dom';
7
7
  import { unshift } from 'spica/array';
8
8
 
9
- export const mark: MarkParser = lazy(() => creator(precedence(2, surround(
9
+ export const mark: MarkParser = lazy(() => creator(precedence(1, surround(
10
10
  str('=='),
11
11
  startTight(some(union([
12
12
  some(inline, blankWith('==')),