securemark 0.283.2 → 0.283.4

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 (44) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/index.js +144 -92
  3. package/markdown.d.ts +16 -8
  4. package/package.json +1 -1
  5. package/src/combinator/control/manipulation/indent.ts +1 -1
  6. package/src/combinator/control/manipulation/surround.ts +4 -4
  7. package/src/combinator/data/parser/context/delimiter.ts +46 -21
  8. package/src/combinator/data/parser/context.ts +6 -2
  9. package/src/parser/block/dlist.ts +1 -1
  10. package/src/parser/context.ts +10 -11
  11. package/src/parser/inline/annotation.ts +2 -2
  12. package/src/parser/inline/autolink/account.ts +14 -13
  13. package/src/parser/inline/autolink/anchor.ts +13 -12
  14. package/src/parser/inline/autolink/channel.ts +6 -3
  15. package/src/parser/inline/autolink/email.ts +4 -3
  16. package/src/parser/inline/autolink/hashnum.ts +10 -8
  17. package/src/parser/inline/autolink/hashtag.ts +10 -8
  18. package/src/parser/inline/autolink/url.ts +25 -19
  19. package/src/parser/inline/autolink.ts +3 -4
  20. package/src/parser/inline/bracket.ts +17 -14
  21. package/src/parser/inline/code.ts +1 -1
  22. package/src/parser/inline/deletion.ts +1 -1
  23. package/src/parser/inline/emphasis.test.ts +2 -2
  24. package/src/parser/inline/emphasis.ts +3 -3
  25. package/src/parser/inline/emstrong.ts +7 -7
  26. package/src/parser/inline/extension/index.ts +3 -3
  27. package/src/parser/inline/extension/indexee.ts +4 -2
  28. package/src/parser/inline/extension/placeholder.ts +2 -2
  29. package/src/parser/inline/html.ts +45 -43
  30. package/src/parser/inline/htmlentity.ts +5 -5
  31. package/src/parser/inline/insertion.ts +1 -1
  32. package/src/parser/inline/link.ts +11 -10
  33. package/src/parser/inline/mark.test.ts +2 -2
  34. package/src/parser/inline/mark.ts +3 -3
  35. package/src/parser/inline/math.ts +2 -2
  36. package/src/parser/inline/media.ts +2 -2
  37. package/src/parser/inline/reference.ts +2 -2
  38. package/src/parser/inline/remark.ts +3 -3
  39. package/src/parser/inline/ruby.ts +5 -4
  40. package/src/parser/inline/strong.test.ts +2 -2
  41. package/src/parser/inline/strong.ts +3 -3
  42. package/src/parser/inline/template.ts +2 -2
  43. package/src/parser/inline.test.ts +5 -2
  44. package/src/parser/visibility.ts +4 -4
package/markdown.d.ts CHANGED
@@ -603,7 +603,7 @@ export namespace MarkdownParser {
603
603
  Block<'reply/cite'>,
604
604
  Parser<HTMLSpanElement | HTMLBRElement, Context, [
605
605
  SourceParser.StrParser,
606
- Parser<HTMLAnchorElement, Context, [
606
+ Parser<string | HTMLAnchorElement, Context, [
607
607
  InlineParser.AutolinkParser.AnchorParser,
608
608
  Parser<HTMLAnchorElement, Context, []>,
609
609
  Parser<HTMLAnchorElement, Context, []>,
@@ -1147,8 +1147,9 @@ export namespace MarkdownParser {
1147
1147
  export interface UrlParser extends
1148
1148
  // https://host
1149
1149
  Inline<'url'>,
1150
- Parser<HTMLAnchorElement, Context, [
1150
+ Parser<string | HTMLAnchorElement, Context, [
1151
1151
  LinkParser.UnsafeLinkParser,
1152
+ Parser<string, Context, []>,
1152
1153
  ]> {
1153
1154
  }
1154
1155
  export namespace UrlParser {
@@ -1156,7 +1157,10 @@ export namespace MarkdownParser {
1156
1157
  Inline<'url/lineurl'>,
1157
1158
  Parser<string | HTMLElement, Context, [
1158
1159
  SourceParser.StrParser,
1159
- InlineParser.LinkParser.UnsafeLinkParser,
1160
+ Parser<string | HTMLElement, Context, [
1161
+ InlineParser.LinkParser.UnsafeLinkParser,
1162
+ Parser<string, Context, []>,
1163
+ ]>,
1160
1164
  ]> {
1161
1165
  }
1162
1166
  export interface BracketParser extends
@@ -1188,7 +1192,7 @@ export namespace MarkdownParser {
1188
1192
  export interface ChannelParser extends
1189
1193
  // @user#tag
1190
1194
  Inline<'channel'>,
1191
- Parser<HTMLAnchorElement, Context, [
1195
+ Parser<string | HTMLAnchorElement, Context, [
1192
1196
  InlineParser.AutolinkParser.AccountParser,
1193
1197
  InlineParser.AutolinkParser.HashtagParser,
1194
1198
  ]> {
@@ -1196,29 +1200,33 @@ export namespace MarkdownParser {
1196
1200
  export interface AccountParser extends
1197
1201
  // @user
1198
1202
  Inline<'account'>,
1199
- Parser<HTMLAnchorElement, Context, [
1203
+ Parser<string | HTMLAnchorElement, Context, [
1200
1204
  LinkParser.UnsafeLinkParser,
1205
+ Parser<string, Context, []>,
1201
1206
  ]> {
1202
1207
  }
1203
1208
  export interface HashtagParser extends
1204
1209
  // #tag
1205
1210
  Inline<'hashtag'>,
1206
- Parser<HTMLAnchorElement, Context, [
1211
+ Parser<string | HTMLAnchorElement, Context, [
1207
1212
  LinkParser.UnsafeLinkParser,
1213
+ Parser<string, Context, []>,
1208
1214
  ]> {
1209
1215
  }
1210
1216
  export interface HashnumParser extends
1211
1217
  // #1
1212
1218
  Inline<'hashnum'>,
1213
- Parser<HTMLAnchorElement, Context, [
1219
+ Parser<string | HTMLAnchorElement, Context, [
1214
1220
  LinkParser.UnsafeLinkParser,
1221
+ Parser<string, Context, []>,
1215
1222
  ]> {
1216
1223
  }
1217
1224
  export interface AnchorParser extends
1218
1225
  // >>1
1219
1226
  Inline<'anchor'>,
1220
- Parser<HTMLAnchorElement, Context, [
1227
+ Parser<string | HTMLAnchorElement, Context, [
1221
1228
  LinkParser.UnsafeLinkParser,
1229
+ Parser<string, Context, []>,
1222
1230
  ]> {
1223
1231
  }
1224
1232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securemark",
3
- "version": "0.283.2",
3
+ "version": "0.283.4",
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",
@@ -32,7 +32,7 @@ export function indent<T>(opener: RegExp | Parser<T>, parser?: Parser<T> | boole
32
32
 
33
33
  function trimBlockEnd(block: string): string {
34
34
  return block === ''
35
- || block[block.length - 1] !== '\n'
35
+ || block.at(-1) !== '\n'
36
36
  ? block
37
37
  : block.slice(0, -1);
38
38
  }
@@ -59,10 +59,10 @@ export function surround<T>(
59
59
  const { logger = {}, offset = 0 } = context;
60
60
  for (let i = 0; i < source.length - mr_.length; ++i) {
61
61
  if (source[i] !== source[0]) break;
62
- const j = source.length + offset - i;
63
- if (!(j in logger)) continue;
62
+ const pos = source.length + offset - i - 1;
63
+ if (!(pos in logger)) continue;
64
64
  assert(log >>> 2);
65
- if (logger[j] & 1 << (log >>> 2)) return;
65
+ if (logger[pos] & 1 << (log >>> 2)) return;
66
66
  }
67
67
  }
68
68
  const res2 = mr_ !== '' ? parser({ source: mr_, context }) : undefined;
@@ -77,7 +77,7 @@ export function surround<T>(
77
77
  if (rest.length === lmr_.length) return;
78
78
  if (log & 2 && rr === undefined) {
79
79
  const { logger = {}, offset = 0 } = context;
80
- logger[source.length + offset] |= 1 << (log >>> 2);
80
+ logger[source.length + offset - 1] |= 1 << (log >>> 2);
81
81
  }
82
82
  return rr
83
83
  ? f
@@ -5,6 +5,7 @@ interface Delimiter {
5
5
  readonly signature: string;
6
6
  readonly matcher: (source: string) => boolean | undefined;
7
7
  readonly precedence: number;
8
+ state: boolean;
8
9
  }
9
10
 
10
11
  export class Delimiters {
@@ -32,7 +33,8 @@ export class Delimiters {
32
33
  this.signature);
33
34
  private readonly registry = memoize<(signature: string) => Delimiter[]>(() => []);
34
35
  private readonly delimiters: Delimiter[] = [];
35
- private readonly order: number[] = [];
36
+ private readonly stack: number[] = [];
37
+ private readonly states: (readonly number[])[] = [];
36
38
  public push(
37
39
  delims: readonly {
38
40
  readonly signature: string;
@@ -40,53 +42,76 @@ export class Delimiters {
40
42
  readonly precedence: number;
41
43
  }[]
42
44
  ): void {
43
- const { registry, delimiters, order } = this;
45
+ const { registry, delimiters, stack } = this;
46
+ // シグネチャ数以下
47
+ assert(delimiters.length < 100);
44
48
  for (let i = 0; i < delims.length; ++i) {
45
49
  const { signature, matcher, precedence } = delims[i];
46
- const stack = registry(signature);
47
- const index = stack[0]?.index ?? delimiters.length;
48
- if (stack.length === 0 || precedence > delimiters[index].precedence) {
50
+ const memory = registry(signature);
51
+ const index = memory[0]?.index ?? delimiters.length;
52
+ if (memory.length === 0 || precedence > delimiters[index].precedence) {
49
53
  const delimiter: Delimiter = {
50
54
  index,
51
55
  signature,
52
56
  matcher,
53
57
  precedence,
58
+ state: true,
54
59
  };
55
60
  delimiters[index] = delimiter;
56
- stack.push(delimiter);
57
- order.push(index);
61
+ memory.push(delimiter);
62
+ stack.push(index);
58
63
  }
59
64
  else {
60
- order.push(-1);
65
+ stack.push(-1);
61
66
  }
67
+ // 現状各優先順位は固定
68
+ assert(memory.length === 1);
62
69
  }
63
70
  }
64
71
  public pop(count: number): void {
65
72
  assert(count > 0);
66
- const { registry, delimiters, order } = this;
73
+ const { registry, delimiters, stack } = this;
67
74
  for (let i = 0; i < count; ++i) {
68
- assert(this.order.length > 0);
69
- const index = order.pop()!;
75
+ assert(this.stack.length > 0);
76
+ const index = stack.pop()!;
70
77
  if (index === -1) continue;
71
- const stack = registry(delimiters[index].signature);
72
- assert(stack.length > 0);
73
- if (stack.length === 1) {
78
+ const memory = registry(delimiters[index].signature);
79
+ assert(memory.length > 0);
80
+ if (memory.length === 1) {
74
81
  assert(index === delimiters.length - 1);
75
- assert(stack[0] === delimiters.at(-1));
76
- stack.pop();
82
+ assert(memory[0] === delimiters.at(-1));
83
+ memory.pop();
77
84
  delimiters.pop();
78
85
  }
79
86
  else {
80
- stack.pop();
81
- delimiters[index] = stack.at(-1)!;
87
+ memory.pop();
88
+ delimiters[index] = memory.at(-1)!;
82
89
  }
83
90
  }
84
91
  }
85
- public match(source: string, precedence = 1): boolean {
92
+ public shift(precedence: number): void {
86
93
  const { delimiters } = this;
87
- for (let i = 0; i < delimiters.length; ++i) {
94
+ const indexes: number[] = [];
95
+ for (let i = delimiters.length; i--;) {
88
96
  const delimiter = delimiters[i];
89
- if (precedence >= delimiter.precedence) continue;
97
+ if (delimiter.precedence >= precedence || !delimiter.state) continue;
98
+ delimiter.state = false;
99
+ indexes.push(i)
100
+ }
101
+ this.states.push(indexes);
102
+ }
103
+ public unshift(): void {
104
+ const { delimiters } = this;
105
+ const indexes = this.states.pop()!;
106
+ for (let i = indexes.length; i--;) {
107
+ delimiters[indexes[i]].state = true;
108
+ }
109
+ }
110
+ public match(source: string, precedence = 0): boolean {
111
+ const { delimiters } = this;
112
+ for (let i = delimiters.length; i--;) {
113
+ const delimiter = delimiters[i];
114
+ if (delimiter.precedence <= precedence || !delimiter.state) continue;
90
115
  switch (delimiter.matcher(source)) {
91
116
  case true:
92
117
  return true;
@@ -93,11 +93,15 @@ export function creation(cost: number, recursion: number, parser: Parser<unknown
93
93
 
94
94
  export function precedence<P extends Parser<unknown>>(precedence: number, parser: P): P;
95
95
  export function precedence<T>(precedence: number, parser: Parser<T>): Parser<T> {
96
- assert(precedence > 0);
96
+ assert(precedence >= 0);
97
97
  return ({ source, context }) => {
98
- const p = context.precedence;
98
+ const { delimiters, precedence: p = 0 } = context;
99
+ const shift = delimiters && precedence > p;
99
100
  context.precedence = precedence;
101
+ // デリミタはシフト後に設定しなければならない
102
+ shift && delimiters.shift(precedence);
100
103
  const result = parser({ source, context });
104
+ shift && delimiters.unshift();
101
105
  context.precedence = p;
102
106
  return result;
103
107
  };
@@ -33,7 +33,7 @@ const desc: DListParser.DescriptionParser = block(fmap(open(
33
33
  false);
34
34
 
35
35
  function fillTrailingDescription(es: HTMLElement[]): HTMLElement[] {
36
- return es.length > 0 && es[es.length - 1].tagName === 'DT'
36
+ return es.length > 0 && es.at(-1)!.tagName === 'DT'
37
37
  ? push(es, [html('dd')])
38
38
  : es;
39
39
  }
@@ -1,13 +1,12 @@
1
1
  export const enum State {
2
- annotation = 1 << 8,
3
- reference = 1 << 7,
4
- index = 1 << 6,
5
- label = 1 << 5,
6
- link = 1 << 4,
7
- media = 1 << 3,
8
- mark = 1 << 2,
9
- autolink = 1 << 1,
10
- shortcut = 1 << 0,
2
+ annotation = 1 << 7,
3
+ reference = 1 << 6,
4
+ index = 1 << 5,
5
+ label = 1 << 4,
6
+ link = 1 << 3,
7
+ media = 1 << 2,
8
+ mark = 1 << 1,
9
+ autolink = 1 << 0,
11
10
  none = 0,
12
11
  all = ~0,
13
12
  linkers = 0
@@ -32,10 +31,10 @@ export const enum Recursion {
32
31
 
33
32
  export const enum Backtrack {
34
33
  template = 7 << 2,
35
- media = 6 << 2,
34
+ index = 6 << 2,
36
35
  ruby = 5 << 2,
37
36
  link = 4 << 2,
38
- index = 3 << 2,
37
+ media = 3 << 2,
39
38
  url = 2 << 2,
40
39
  bracket = 1 << 2,
41
40
  }
@@ -8,8 +8,8 @@ import { html, defrag } from 'typed-dom/dom';
8
8
  export const annotation: AnnotationParser = lazy(() => creation(1, Recursion.ignore, surround(
9
9
  '((',
10
10
  constraint(State.annotation, false,
11
- syntax(6, State.annotation | State.media,
12
- trimBlankStart(some(union([inline]), ')', [[/^\\?\n/, 9], [')', 2]])))),
11
+ syntax(1, State.annotation | State.media,
12
+ trimBlankStart(some(union([inline]), ')', [[/^\\?\n/, 9], [')', 1]])))),
13
13
  '))',
14
14
  false,
15
15
  ([, ns], rest) => [[html('sup', { class: 'annotation' }, [html('span', trimNodeEnd(defrag(ns)))])], rest],
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, tails, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, tails, syntax, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { State } from '../../context';
@@ -7,20 +7,21 @@ import { define } from 'typed-dom/dom';
7
7
 
8
8
  // https://example/@user must be a user page or a redirect page going there.
9
9
 
10
- export const account: AutolinkParser.AccountParser = lazy(() => fmap(rewrite(
11
- constraint(State.shortcut, false,
10
+ export const account: AutolinkParser.AccountParser = lazy(() => rewrite(
12
11
  open(
13
12
  '@',
14
13
  tails([
15
14
  str(/^[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*\//i),
16
15
  str(/^[a-z][0-9a-z]*(?:[-.][0-9a-z]+)*/i),
17
- ]))),
18
- convert(
19
- source =>
20
- `[${source}]{ ${
21
- source.includes('/')
22
- ? `https://${source.slice(1).replace('/', '/@')}`
23
- : `/${source}`
24
- } }`,
25
- union([unsafelink]))),
26
- ([el]) => [define(el, { class: 'account' })]));
16
+ ])),
17
+ union([
18
+ constraint(State.autolink, false, syntax(0, State.autolink, fmap(convert(
19
+ source =>
20
+ `[${source}]{ ${source.includes('/')
21
+ ? `https://${source.slice(1).replace('/', '/@')}`
22
+ : `/${source}`
23
+ } }`,
24
+ unsafelink),
25
+ ([el]) => [define(el, { class: 'account' })]))),
26
+ ({ source }) => [[source], ''],
27
+ ])));
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, constraint, validate, focus, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, syntax, constraint, validate, focus, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { State } from '../../context';
5
5
  import { define } from 'typed-dom/dom';
@@ -14,16 +14,17 @@ import { define } from 'typed-dom/dom';
14
14
  // 内部表現はUnixTimeに統一する(時系列順)
15
15
  // 外部表現は投稿ごとに投稿者の投稿時のタイムゾーンに統一する(非時系列順)
16
16
 
17
- export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>', fmap(
18
- constraint(State.shortcut, false,
17
+ export const anchor: AutolinkParser.AnchorParser = lazy(() => validate('>>',
19
18
  focus(
20
19
  /^>>(?:[a-z][0-9a-z]*(?:-[0-9a-z]+)*\/)?[0-9a-z]+(?:-[0-9a-z]+)*(?![0-9a-z@#:])/i,
21
- convert(
22
- source =>
23
- `[${source}]{ ${
24
- source.includes('/')
25
- ? `/@${source.slice(2).replace('/', '/timeline?at=')}`
26
- : `?at=${source.slice(2)}`
27
- } }`,
28
- union([unsafelink])))),
29
- ([el]) => [define(el, { class: 'anchor' })])));
20
+ union([
21
+ constraint(State.autolink, false, syntax(0, State.autolink, fmap(convert(
22
+ source =>
23
+ `[${source}]{ ${source.includes('/')
24
+ ? `/@${source.slice(2).replace('/', '/timeline?at=')}`
25
+ : `?at=${source.slice(2)}`
26
+ } }`,
27
+ unsafelink),
28
+ ([el]) => [define(el, { class: 'anchor' })]))),
29
+ ({ source }) => [[source], ''],
30
+ ]))));
@@ -1,13 +1,15 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { sequence, some, validate, bind } from '../../../combinator';
2
+ import { sequence, some, constraint, validate, bind } from '../../../combinator';
3
3
  import { account } from './account';
4
4
  import { hashtag } from './hashtag';
5
+ import { State } from '../../context';
5
6
  import { stringify } from '../../util';
6
7
  import { define } from 'typed-dom/dom';
7
8
 
8
9
  // https://example/@user?ch=a+b must be a user channel page or a redirect page going there.
9
10
 
10
- export const channel: AutolinkParser.ChannelParser = validate('@', bind(
11
+ export const channel: AutolinkParser.ChannelParser = validate('@',
12
+ constraint(State.autolink, false, bind(
11
13
  sequence([
12
14
  account,
13
15
  some(hashtag),
@@ -15,6 +17,7 @@ export const channel: AutolinkParser.ChannelParser = validate('@', bind(
15
17
  (es, rest) => {
16
18
  const source = stringify(es);
17
19
  const el = es[0];
20
+ if (typeof el === 'string') return [es, rest];
18
21
  const url = `${el.getAttribute('href')}?ch=${source.slice(source.indexOf('#') + 1).replace(/#/g, '+')}`;
19
22
  return [[define(el, { class: 'channel', href: url }, source)], rest];
20
- }));
23
+ })));
@@ -1,7 +1,7 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { creation, verify, rewrite } from '../../../combinator';
2
+ import { syntax, creation, constraint, verify, rewrite } from '../../../combinator';
3
3
  import { str } from '../../source';
4
- import { Recursion } from '../../context';
4
+ import { State, Recursion } from '../../context';
5
5
  import { html } from 'typed-dom/dom';
6
6
 
7
7
  // https://html.spec.whatwg.org/multipage/input.html
@@ -9,4 +9,5 @@ import { html } from 'typed-dom/dom';
9
9
  export const email: AutolinkParser.EmailParser = creation(1, Recursion.ignore, rewrite(verify(
10
10
  str(/^[0-9a-z](?:[_.+-](?=[0-9a-z])|[0-9a-z]){0,255}@[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?(?:\.[0-9a-z](?:(?:[0-9a-z]|-(?=[0-9a-z])){0,61}[0-9a-z])?)*(?![0-9a-z])/i),
11
11
  ([source]) => source.length <= 255),
12
- ({ source }) => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], '']));
12
+ constraint(State.autolink, false, syntax(0, State.autolink,
13
+ ({ source }) => [[html('a', { class: 'email', href: `mailto:${source}` }, source)], '']))));
@@ -1,15 +1,17 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, syntax, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { emoji } from './hashtag';
5
5
  import { str } from '../../source';
6
6
  import { State } from '../../context';
7
7
  import { define } from 'typed-dom/dom';
8
8
 
9
- export const hashnum: AutolinkParser.HashnumParser = lazy(() => fmap(rewrite(
10
- constraint(State.shortcut, false,
11
- open('#', str(new RegExp(/^[0-9]{1,9}(?![^\p{C}\p{S}\p{P}\s]|emoji|['_])/u.source.replace(/emoji/, emoji), 'u')))),
12
- convert(
13
- source => `[${source}]{ ${source.slice(1)} }`,
14
- union([unsafelink]))),
15
- ([el]) => [define(el, { class: 'hashnum', href: null })]));
9
+ export const hashnum: AutolinkParser.HashnumParser = lazy(() => rewrite(
10
+ open('#', str(new RegExp(/^[0-9]{1,9}(?![^\p{C}\p{S}\p{P}\s]|emoji|['_])/u.source.replace(/emoji/, emoji), 'u'))),
11
+ union([
12
+ constraint(State.autolink, false, syntax(0, State.autolink, fmap(convert(
13
+ source => `[${source}]{ ${source.slice(1)} }`,
14
+ unsafelink),
15
+ ([el]) => [define(el, { class: 'hashnum', href: null })]))),
16
+ ({ source }) => [[source], ''],
17
+ ])));
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
2
+ import { union, syntax, constraint, rewrite, open, convert, fmap, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { str } from '../../source';
5
5
  import { State } from '../../context';
@@ -10,15 +10,17 @@ import { define } from 'typed-dom/dom';
10
10
  // https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
11
11
  export const emoji = String.raw`\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F`;
12
12
 
13
- export const hashtag: AutolinkParser.HashtagParser = lazy(() => fmap(rewrite(
14
- constraint(State.shortcut, false,
13
+ export const hashtag: AutolinkParser.HashtagParser = lazy(() => rewrite(
15
14
  open(
16
15
  '#',
17
16
  str(new RegExp([
18
17
  /^(?!['_])(?=(?:[0-9]{1,9})?(?:[^\d\p{C}\p{S}\p{P}\s]|emoji|'|_(?=[^\p{C}\p{S}\p{P}\s]|emoji|')))/u.source,
19
18
  /(?:[^\p{C}\p{S}\p{P}\s]|emoji|'|_(?=[^\p{C}\p{S}\p{P}\s]|emoji|'))+/u.source,
20
- ].join('').replace(/emoji/g, emoji), 'u')))),
21
- convert(
22
- source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`,
23
- union([unsafelink]))),
24
- ([el]) => [define(el, { class: 'hashtag' })]));
19
+ ].join('').replace(/emoji/g, emoji), 'u'))),
20
+ union([
21
+ constraint(State.autolink, false, syntax(0, State.autolink, fmap(convert(
22
+ source => `[${source}]{ ${`/hashtags/${source.slice(1)}`} }`,
23
+ unsafelink),
24
+ ([el]) => [define(el, { class: 'hashtag' })]))),
25
+ ({ source }) => [[source], ''],
26
+ ])));
@@ -1,8 +1,8 @@
1
1
  import { AutolinkParser } from '../../inline';
2
- import { union, tails, some, creation, precedence, validate, focus, rewrite, convert, surround, open, lazy } from '../../../combinator';
2
+ import { union, tails, some, syntax, creation, precedence, constraint, validate, focus, rewrite, convert, surround, open, lazy } from '../../../combinator';
3
3
  import { unsafelink } from '../link';
4
4
  import { linebreak, unescsource, str } from '../../source';
5
- import { Backtrack, Recursion } from '../../context';
5
+ import { State, Recursion, Backtrack } from '../../context';
6
6
 
7
7
  const closer = /^[-+*=~^_,.;:!?]*(?=[\\"`|\[\](){}<>]|$)/;
8
8
 
@@ -10,24 +10,30 @@ export const url: AutolinkParser.UrlParser = lazy(() => validate(['http://', 'ht
10
10
  open(
11
11
  /^https?:\/\/(?=[\x21-\x7E])/,
12
12
  focus(/^[\x21-\x7E]+/, some(union([bracket, some(unescsource, closer)])))),
13
- convert(
14
- url => `{ ${url} }`,
15
- union([unsafelink])))));
13
+ union([
14
+ constraint(State.autolink, false, syntax(0, State.autolink, convert(
15
+ url => `{ ${url} }`,
16
+ unsafelink))),
17
+ ({ source }) => [[source], ''],
18
+ ]))));
16
19
 
17
20
  export const lineurl: AutolinkParser.UrlParser.LineUrlParser = lazy(() => open(
18
21
  linebreak,
19
- tails([
20
- str('!'),
21
- focus(
22
- /^https?:\/\/\S+(?=[^\S\n]*(?:$|\n))/,
23
- convert(
24
- url => `{ ${url} }`,
25
- unsafelink)),
26
- ])));
22
+ focus(
23
+ /^!?https?:\/\/\S+(?=[^\S\n]*(?:$|\n))/,
24
+ tails([
25
+ str('!'),
26
+ union([
27
+ constraint(State.autolink, false, syntax(0, State.autolink, convert(
28
+ url => `{ ${url} }`,
29
+ unsafelink))),
30
+ ({ source }) => [[source], ''],
31
+ ]),
32
+ ]))));
27
33
 
28
- const bracket: AutolinkParser.UrlParser.BracketParser = lazy(() => creation(0, Recursion.terminal, precedence(2, union([
29
- surround(str('('), some(union([bracket, unescsource]), ')'), str(')'), true, undefined, undefined, 3 | Backtrack.url),
30
- surround(str('['), some(union([bracket, unescsource]), ']'), str(']'), true, undefined, undefined, 3 | Backtrack.url),
31
- surround(str('{'), some(union([bracket, unescsource]), '}'), str('}'), true, undefined, undefined, 3 | Backtrack.url),
32
- surround(str('"'), precedence(3, some(unescsource, '"')), str('"'), true, undefined, undefined, 3 | Backtrack.url),
33
- ]))));
34
+ const bracket: AutolinkParser.UrlParser.BracketParser = lazy(() => creation(0, Recursion.terminal, union([
35
+ surround(str('('), precedence(1, some(union([bracket, unescsource]), ')')), str(')'), true, undefined, undefined, 3 | Backtrack.url),
36
+ surround(str('['), precedence(1, some(union([bracket, unescsource]), ']')), str(']'), true, undefined, undefined, 3 | Backtrack.url),
37
+ surround(str('{'), precedence(1, some(union([bracket, unescsource]), '}')), str('}'), true, undefined, undefined, 3 | Backtrack.url),
38
+ surround(str('"'), precedence(2, some(unescsource, '"')), str('"'), true, undefined, undefined, 3 | Backtrack.url),
39
+ ])));
@@ -1,5 +1,5 @@
1
1
  import { AutolinkParser } from '../inline';
2
- import { union, some, syntax, constraint, validate, lazy, fmap } from '../../combinator';
2
+ import { union, some, syntax, validate, lazy, fmap } from '../../combinator';
3
3
  import { url, lineurl } from './autolink/url';
4
4
  import { email } from './autolink/email';
5
5
  import { channel } from './autolink/channel';
@@ -13,8 +13,7 @@ import { stringify } from '../util';
13
13
 
14
14
  export const autolink: AutolinkParser = lazy(() =>
15
15
  validate(/^(?:[@#>0-9a-z]|\S[#>]|[\r\n]!?https?:\/\/)/iu,
16
- constraint(State.autolink, false,
17
- syntax(1, ~State.shortcut,
16
+ syntax(0, ~State.autolink,
18
17
  union([
19
18
  some(union([lineurl])),
20
19
  fmap(some(union([
@@ -37,4 +36,4 @@ export const autolink: AutolinkParser = lazy(() =>
37
36
  anchor,
38
37
  ])),
39
38
  ns => ns.length === 1 ? ns : [stringify(ns)]),
40
- ])))));
39
+ ]))));
@@ -6,42 +6,45 @@ import { State, Recursion, Backtrack } from '../context';
6
6
  import { unshift, push } from 'spica/array';
7
7
  import { html, defrag } from 'typed-dom/dom';
8
8
 
9
- const index = /^[0-9A-Za-z]+(?:(?:[.-]|, )[0-9A-Za-z]+)*/;
9
+ const indexA = /^[0-9A-Za-z]+(?:(?:[.-]|, )[0-9A-Za-z]+)*/;
10
+ const indexF = new RegExp(indexA.source.replace(', ', '[,、]')
11
+ .replace(/[09AZaz.]|\-(?!\w)/g, c => String.fromCodePoint(c.codePointAt(0)! + 0xFEE0)));
10
12
 
11
13
  export const bracket: BracketParser = lazy(() => union([
12
- surround(str('('), creation(0, Recursion.bracket, syntax(2, State.none, str(index))), str(')')),
13
- surround(str('('), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, ')', [[')', 2]]))), str(')'), true,
14
+ surround(str('('), creation(0, Recursion.bracket, syntax(1, State.none, str(indexA))), str(')')),
15
+ surround(str('('), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, ')', [[')', 1]]))), str(')'), true,
14
16
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
15
17
  ([as, bs = []], rest) => [unshift(as, bs), rest], 3 | Backtrack.bracket),
16
- surround(str('('), creation(0, Recursion.bracket, syntax(2, State.none, str(new RegExp(index.source.replace(', ', '[,、]').replace(/[09AZaz.]|\-(?!\w)/g, c => c.trimStart() && String.fromCharCode(c.charCodeAt(0) + 0xFEE0)))))), str(')')),
17
- surround(str('('), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, ')', [[')', 2]]))), str(')'), true,
18
+ surround(str('('), creation(0, Recursion.bracket, syntax(1, State.none, str(indexF))), str(')')),
19
+ surround(str('('), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, ')', [[')', 1]]))), str(')'), true,
18
20
  ([as, bs = [], cs], rest) => [[html('span', { class: 'paren' }, defrag(push(unshift(as, bs), cs)))], rest],
19
21
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
20
- surround(str('['), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, ']', [[']', 2]]))), str(']'), true,
22
+ surround(str('['), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, ']', [[']', 1]]))), str(']'), true,
21
23
  undefined,
22
24
  ([as, bs = []], rest) => [unshift(as, bs), rest], 3 | Backtrack.bracket),
23
- surround(str('['), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, ']', [[']', 2]]))), str(']'), true,
25
+ surround(str('['), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, ']', [[']', 1]]))), str(']'), true,
24
26
  undefined,
25
27
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
26
- surround(str('{'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '}', [['}', 2]]))), str('}'), true,
28
+ surround(str('{'), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, '}', [['}', 1]]))), str('}'), true,
27
29
  undefined,
28
30
  ([as, bs = []], rest) => [unshift(as, bs), rest], 3 | Backtrack.bracket),
29
- surround(str('{'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '}', [['}', 2]]))), str('}'), true,
31
+ surround(str('{'), creation(0, Recursion.bracket, syntax(1, State.none, some(inline, '}', [['}', 1]]))), str('}'), true,
30
32
  undefined,
31
33
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
32
- surround(str('"'), creation(0, Recursion.bracket, syntax(3, State.none, some(inline, '"', [[/^\\?\n/, 9], ['"', 3]]))), str('"'), true,
34
+ // 改行禁止はバックトラックなしでは内側の構文を破壊するため安易に行えない。
35
+ surround(str('"'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '"', [[/^\\?\n/, 9], ['"', 2]]))), str('"'), true,
33
36
  undefined,
34
37
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
35
- surround(str('“'), creation(0, Recursion.bracket, syntax(3, State.none, some(inline, '”', [[/^\\?\n/, 9], ['”', 3]]))), str('”'), true,
38
+ surround(str('“'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '”', [[/^\\?\n/, 9], ['”', 2]]))), str('”'), true,
36
39
  undefined,
37
40
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
38
- surround(str('‘'), creation(0, Recursion.bracket, syntax(3, State.none, some(inline, '’', [[/^\\?\n/, 9], ['’', 3]]))), str('’'), true,
41
+ surround(str('‘'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '’', [[/^\\?\n/, 9], ['’', 2]]))), str('’'), true,
39
42
  undefined,
40
43
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
41
- surround(str('「'), creation(0, Recursion.bracket, syntax(3, State.none, some(inline, '」', [[/^\\?\n/, 9], ['」', 3]]))), str('」'), true,
44
+ surround(str('「'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '」', [[/^\\?\n/, 9], ['」', 2]]))), str('」'), true,
42
45
  undefined,
43
46
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
44
- surround(str('『'), creation(0, Recursion.bracket, syntax(3, State.none, some(inline, '』', [[/^\\?\n/, 9], ['』', 3]]))), str('』'), true,
47
+ surround(str('『'), creation(0, Recursion.bracket, syntax(2, State.none, some(inline, '』', [[/^\\?\n/, 9], ['』', 2]]))), str('』'), true,
45
48
  undefined,
46
49
  ([as, bs = []], rest) => [unshift(as, bs), rest]),
47
50
  ]));