securemark 0.267.0 → 0.268.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 +8 -0
- package/README.md +0 -4
- package/dist/index.js +121 -106
- package/markdown.d.ts +22 -5
- package/package.json +8 -8
- package/src/combinator/control/constraint/block.ts +2 -2
- package/src/combinator/control/constraint/line.ts +2 -2
- package/src/combinator/control/manipulation/fence.ts +4 -4
- package/src/parser/api/bind.ts +1 -1
- package/src/parser/api/parse.ts +1 -1
- package/src/parser/autolink.test.ts +1 -2
- package/src/parser/autolink.ts +15 -22
- package/src/parser/block/blockquote.ts +1 -1
- package/src/parser/block/codeblock.ts +2 -2
- package/src/parser/block/olist.test.ts +4 -4
- package/src/parser/block/olist.ts +47 -20
- package/src/parser/block/paragraph.test.ts +3 -0
- package/src/parser/block/reply/cite.ts +1 -2
- package/src/parser/block/reply/quote.test.ts +3 -0
- package/src/parser/block/reply/quote.ts +15 -9
- package/src/parser/block/sidefence.ts +1 -1
- package/src/parser/block/ulist.test.ts +3 -3
- package/src/parser/block/ulist.ts +6 -6
- package/src/parser/inline/extension/index.ts +1 -1
- package/src/parser/inline/extension/indexee.ts +2 -1
- package/src/parser/processor/figure.ts +1 -2
- package/src/parser/processor/footnote.test.ts +3 -3
- package/src/parser/processor/footnote.ts +22 -35
- package/src/parser/source/line.ts +3 -3
- package/src/parser/source/str.ts +1 -1
- package/src/parser/util.ts +10 -0
- package/src/parser/visibility.ts +2 -1
- package/src/util/quote.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "securemark",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.268.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",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"@types/mocha": "10.0.1",
|
|
35
35
|
"@types/power-assert": "1.5.8",
|
|
36
36
|
"@types/prismjs": "1.26.0",
|
|
37
|
-
"@typescript-eslint/parser": "^5.
|
|
37
|
+
"@typescript-eslint/parser": "^5.53.0",
|
|
38
38
|
"babel-loader": "^9.1.2",
|
|
39
39
|
"babel-plugin-unassert": "^3.2.0",
|
|
40
40
|
"concurrently": "^7.6.0",
|
|
41
|
-
"eslint": "^8.
|
|
42
|
-
"eslint-plugin-redos": "^4.4.
|
|
43
|
-
"eslint-webpack-plugin": "^
|
|
41
|
+
"eslint": "^8.35.0",
|
|
42
|
+
"eslint-plugin-redos": "^4.4.5",
|
|
43
|
+
"eslint-webpack-plugin": "^4.0.0",
|
|
44
44
|
"glob": "^8.1.0",
|
|
45
45
|
"karma": "^6.4.1",
|
|
46
46
|
"karma-chrome-launcher": "^3.1.1",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"karma-mocha": "^2.0.1",
|
|
50
50
|
"karma-power-assert": "^1.0.0",
|
|
51
51
|
"mocha": "^10.2.0",
|
|
52
|
-
"npm-check-updates": "^16.
|
|
52
|
+
"npm-check-updates": "^16.7.9",
|
|
53
53
|
"semver": "^7.3.8",
|
|
54
54
|
"spica": "0.0.719",
|
|
55
55
|
"ts-loader": "^9.4.2",
|
|
56
|
-
"typed-dom": "^0.0.
|
|
57
|
-
"typescript": "4.9.
|
|
56
|
+
"typed-dom": "^0.0.316",
|
|
57
|
+
"typescript": "4.9.5",
|
|
58
58
|
"webpack": "^5.75.0",
|
|
59
59
|
"webpack-cli": "^5.0.1",
|
|
60
60
|
"webpack-merge": "^5.8.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Parser, exec } from '../../data/parser';
|
|
2
2
|
import { Memo } from '../../data/parser/context/memo';
|
|
3
|
-
import { firstline,
|
|
3
|
+
import { firstline, isBlank } from './line';
|
|
4
4
|
|
|
5
5
|
export function block<P extends Parser<unknown>>(parser: P, separation?: boolean): P;
|
|
6
6
|
export function block<T>(parser: Parser<T>, separation = true): Parser<T> {
|
|
@@ -11,7 +11,7 @@ export function block<T>(parser: Parser<T>, separation = true): Parser<T> {
|
|
|
11
11
|
const result = parser({ source, context });
|
|
12
12
|
if (result === undefined) return;
|
|
13
13
|
const rest = exec(result);
|
|
14
|
-
if (separation && !
|
|
14
|
+
if (separation && !isBlank(firstline(rest))) return;
|
|
15
15
|
assert(rest === '' || source[source.length - rest.length - 1] === '\n');
|
|
16
16
|
return rest === '' || source[source.length - rest.length - 1] === '\n'
|
|
17
17
|
? result
|
|
@@ -14,7 +14,7 @@ export function line<T>(parser: Parser<T>): Parser<T> {
|
|
|
14
14
|
assert(check(line, result));
|
|
15
15
|
context.offset -= source.length - line.length;
|
|
16
16
|
if (result === undefined) return;
|
|
17
|
-
return
|
|
17
|
+
return isBlank(exec(result))
|
|
18
18
|
? [eval(result), source.slice(line.length)]
|
|
19
19
|
: undefined;
|
|
20
20
|
};
|
|
@@ -32,7 +32,7 @@ export function firstline(source: string): string {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function
|
|
35
|
+
export function isBlank(line: string): boolean {
|
|
36
36
|
return line === ''
|
|
37
37
|
|| line === '\n'
|
|
38
38
|
|| line.trimStart() === '';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Parser, Ctx } from '../../data/parser';
|
|
2
|
-
import { firstline,
|
|
2
|
+
import { firstline, isBlank } from '../constraint/line';
|
|
3
3
|
import { unshift } from 'spica/array';
|
|
4
4
|
|
|
5
5
|
export function fence<C extends Ctx, D extends Parser<unknown, C>[]>(opener: RegExp, limit: number, separation = true): Parser<string, C, D> {
|
|
@@ -13,20 +13,20 @@ export function fence<C extends Ctx, D extends Parser<unknown, C>[]>(opener: Reg
|
|
|
13
13
|
if (matches[0].indexOf(delim, delim.length) !== -1) return;
|
|
14
14
|
let rest = source.slice(matches[0].length);
|
|
15
15
|
// Prevent annoying parsing in editing.
|
|
16
|
-
if (
|
|
16
|
+
if (isBlank(firstline(rest)) && firstline(rest.slice(firstline(rest).length)).trimEnd() !== delim) return;
|
|
17
17
|
let block = '';
|
|
18
18
|
let closer = '';
|
|
19
19
|
let overflow = '';
|
|
20
20
|
for (let count = 1; ; ++count) {
|
|
21
21
|
if (rest === '') break;
|
|
22
22
|
const line = firstline(rest);
|
|
23
|
-
if ((closer || count > limit + 1) &&
|
|
23
|
+
if ((closer || count > limit + 1) && isBlank(line)) break;
|
|
24
24
|
if(closer) {
|
|
25
25
|
overflow += line;
|
|
26
26
|
}
|
|
27
27
|
if (!closer && count <= limit + 1 && line.slice(0, delim.length) === delim && line.trimEnd() === delim) {
|
|
28
28
|
closer = line;
|
|
29
|
-
if (
|
|
29
|
+
if (isBlank(firstline(rest.slice(line.length)))) {
|
|
30
30
|
rest = rest.slice(line.length);
|
|
31
31
|
break;
|
|
32
32
|
}
|
package/src/parser/api/bind.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function bind(target: DocumentFragment | HTMLElement | ShadowRoot, settin
|
|
|
26
26
|
host: settings.host ?? new ReadonlyURL(location.pathname, location.origin),
|
|
27
27
|
memo: new Memo({ targets: State.backtrackers }),
|
|
28
28
|
};
|
|
29
|
-
if (context.id?.match(/[^0-9a-z
|
|
29
|
+
if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
|
|
30
30
|
if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
|
|
31
31
|
assert(!settings.id);
|
|
32
32
|
type Block = readonly [segment: string, blocks: readonly HTMLElement[], url: string];
|
package/src/parser/api/parse.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function parse(source: string, opts: Options = {}, context?: MarkdownPars
|
|
|
32
32
|
},
|
|
33
33
|
memo: new Memo({ targets: State.backtrackers }),
|
|
34
34
|
};
|
|
35
|
-
if (context.id?.match(/[^0-9a-z
|
|
35
|
+
if (context.id?.match(/[^0-9a-z/-]/i)) throw new Error('Invalid ID: ID must be alphanumeric');
|
|
36
36
|
if (context.host?.origin === 'null') throw new Error(`Invalid host: ${context.host.href}`);
|
|
37
37
|
const node = frag();
|
|
38
38
|
let index = 0;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { autolink } from './autolink';
|
|
2
|
-
import { some } from '../combinator';
|
|
3
2
|
import { inspect } from '../debug.test';
|
|
4
3
|
|
|
5
4
|
describe('Unit: parser/autolink', () => {
|
|
6
5
|
describe('autolink', () => {
|
|
7
|
-
const parser = (source: string) =>
|
|
6
|
+
const parser = (source: string) => autolink({ source, context: {} });
|
|
8
7
|
|
|
9
8
|
it('basic', () => {
|
|
10
9
|
assert.deepStrictEqual(inspect(parser(' http://host')), [[' ', '<a class="url" href="http://host" target="_blank">http://host</a>'], '']);
|
package/src/parser/autolink.ts
CHANGED
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
import { MarkdownParser } from '../../markdown';
|
|
2
|
-
import { union, lazy } from '../combinator';
|
|
2
|
+
import { union, tails, subsequence, some, line, focus, lazy } from '../combinator';
|
|
3
|
+
import { link } from './inline/link';
|
|
3
4
|
import { autolink as autolink_ } from './inline/autolink';
|
|
4
|
-
import { linebreak, unescsource } from './source';
|
|
5
|
+
import { linebreak, unescsource, str } from './source';
|
|
6
|
+
import { format } from './util';
|
|
5
7
|
|
|
6
8
|
export import AutolinkParser = MarkdownParser.AutolinkParser;
|
|
7
9
|
|
|
8
|
-
const
|
|
10
|
+
export const autolink: AutolinkParser = lazy(() => some(line(subsequence([
|
|
11
|
+
lineurl,
|
|
12
|
+
some(union([
|
|
13
|
+
autolink_,
|
|
14
|
+
linebreak,
|
|
15
|
+
unescsource,
|
|
16
|
+
])),
|
|
17
|
+
]))));
|
|
9
18
|
|
|
10
|
-
export const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const i = source.search(delimiter);
|
|
14
|
-
switch (i) {
|
|
15
|
-
case -1:
|
|
16
|
-
return [[source], ''];
|
|
17
|
-
case 0:
|
|
18
|
-
return parser({ source, context });
|
|
19
|
-
default:
|
|
20
|
-
return [[source.slice(0, i)], source.slice(i)];
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const parser: AutolinkParser = lazy(() => union([
|
|
25
|
-
autolink_,
|
|
26
|
-
linebreak,
|
|
27
|
-
unescsource
|
|
28
|
-
]));
|
|
19
|
+
export const lineurl: AutolinkParser.LineUrlParser = lazy(() => focus(
|
|
20
|
+
/^!?https?:\/\/\S+(?=[^\S\n]*(?:$|\n))/,
|
|
21
|
+
format(tails([str('!'), link]))));
|
|
@@ -25,7 +25,7 @@ const source: BlockquoteParser.SourceParser = lazy(() => fmap(
|
|
|
25
25
|
convert(unindent, source)),
|
|
26
26
|
rewrite(
|
|
27
27
|
some(contentline, opener),
|
|
28
|
-
convert(unindent, fmap(
|
|
28
|
+
convert(unindent, fmap(autolink, ns => [html('pre', defrag(ns))]))),
|
|
29
29
|
]))),
|
|
30
30
|
ns => [html('blockquote', ns)]));
|
|
31
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CodeBlockParser } from '../block';
|
|
2
2
|
import { eval } from '../../combinator/data/parser';
|
|
3
|
-
import {
|
|
3
|
+
import { block, validate, fence, clear, fmap } from '../../combinator';
|
|
4
4
|
import { autolink } from '../autolink';
|
|
5
5
|
import { html, defrag } from 'typed-dom/dom';
|
|
6
6
|
|
|
@@ -69,6 +69,6 @@ export const codeblock: CodeBlockParser = block(validate('```', fmap(
|
|
|
69
69
|
params.lang
|
|
70
70
|
? context.caches?.code?.get(`${params.lang ?? ''}\n${body.slice(0, -1)}`)?.cloneNode(true).childNodes ||
|
|
71
71
|
body.slice(0, -1) || undefined
|
|
72
|
-
: defrag(eval(
|
|
72
|
+
: defrag(eval(autolink({ source: body.slice(0, -1), context }), [])));
|
|
73
73
|
return [el];
|
|
74
74
|
})));
|
|
@@ -30,15 +30,15 @@ describe('Unit: parser/block/olist', () => {
|
|
|
30
30
|
assert.deepStrictEqual(inspect(parser('(1)\n')), undefined);
|
|
31
31
|
assert.deepStrictEqual(inspect(parser('(I) ')), undefined);
|
|
32
32
|
assert.deepStrictEqual(inspect(parser('(A) ')), undefined);
|
|
33
|
-
assert.deepStrictEqual(inspect(parser(' 1.')), undefined);
|
|
33
|
+
assert.deepStrictEqual(inspect(parser(' 1. ')), undefined);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it('single', () => {
|
|
37
37
|
// pending
|
|
38
38
|
assert.deepStrictEqual(inspect(parser('1. ')), [['<ol><li></li></ol>'], '']);
|
|
39
39
|
// filled
|
|
40
|
-
assert.deepStrictEqual(inspect(parser('1. \\')), [['<ol><li
|
|
41
|
-
assert.deepStrictEqual(inspect(parser('1. \\\n')), [['<ol><li
|
|
40
|
+
assert.deepStrictEqual(inspect(parser('1. \\')), [['<ol><li id="index::\\">\\</li></ol>'], '']);
|
|
41
|
+
assert.deepStrictEqual(inspect(parser('1. \\\n')), [['<ol><li id="index::\\">\\</li></ol>'], '']);
|
|
42
42
|
assert.deepStrictEqual(inspect(parser('1. -')), [['<ol><li id="index::-">-</li></ol>'], '']);
|
|
43
43
|
assert.deepStrictEqual(inspect(parser('1. -\n')), [['<ol><li id="index::-">-</li></ol>'], '']);
|
|
44
44
|
// pending
|
|
@@ -91,7 +91,7 @@ describe('Unit: parser/block/olist', () => {
|
|
|
91
91
|
assert.deepStrictEqual(inspect(parser('01. ')), [['<ol><li data-marker="01."></li></ol>'], '']);
|
|
92
92
|
assert.deepStrictEqual(inspect(parser('0.\n1')), [['<ol><li></li><li data-marker="1."></li></ol>'], '']);
|
|
93
93
|
assert.deepStrictEqual(inspect(parser('8.\n9')), [['<ol><li data-marker="8."></li><li data-marker="9."></li></ol>'], '']);
|
|
94
|
-
assert.deepStrictEqual(inspect(parser('9.\n9')), [['<ol><li data-marker="9."></li><li data-marker="9."></li></ol>'], '']);
|
|
94
|
+
assert.deepStrictEqual(inspect(parser('9.\n9')), [['<ol><li data-marker="9."></li><li data-marker="9." class="invalid"></li></ol>'], '']);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('branch', () => {
|
|
@@ -6,7 +6,7 @@ import { ilist_ } from './ilist';
|
|
|
6
6
|
import { inline, indexee, indexer } from '../inline';
|
|
7
7
|
import { contentline } from '../source';
|
|
8
8
|
import { State } from '../context';
|
|
9
|
-
import { trimBlank } from '../visibility';
|
|
9
|
+
import { visualize, trimBlank } from '../visibility';
|
|
10
10
|
import { memoize } from 'spica/memoize';
|
|
11
11
|
import { shift } from 'spica/array';
|
|
12
12
|
import { html, define, defrag } from 'typed-dom/dom';
|
|
@@ -27,17 +27,17 @@ export const olist: OListParser = lazy(() => block(validate(
|
|
|
27
27
|
export const olist_: OListParser = lazy(() => block(union([
|
|
28
28
|
match(
|
|
29
29
|
openers['.'],
|
|
30
|
-
memoize(ms => list(type(ms[1]), '.'), ms =>
|
|
30
|
+
memoize(ms => list(type(ms[1]), '.'), ms => index(ms[1]), [])),
|
|
31
31
|
match(
|
|
32
32
|
openers['('],
|
|
33
|
-
memoize(ms => list(type(ms[1]), '('), ms =>
|
|
33
|
+
memoize(ms => list(type(ms[1]), '('), ms => index(ms[1]), [])),
|
|
34
34
|
])));
|
|
35
35
|
|
|
36
36
|
const list = (type: string, form: string): OListParser.ListParser => fmap(
|
|
37
37
|
some(creation(1, false, union([
|
|
38
38
|
indexee(fmap(fallback(
|
|
39
39
|
inits([
|
|
40
|
-
line(open(heads[form], subsequence([checkbox, trimBlank(some(union([indexer, inline])))]), true)),
|
|
40
|
+
line(open(heads[form], subsequence([checkbox, trimBlank(visualize(some(union([indexer, inline]))))]), true)),
|
|
41
41
|
indent(union([ulist_, olist_, ilist_])),
|
|
42
42
|
]),
|
|
43
43
|
invalid),
|
|
@@ -61,14 +61,29 @@ export const invalid = rewrite(
|
|
|
61
61
|
'',
|
|
62
62
|
html('span', {
|
|
63
63
|
class: 'invalid',
|
|
64
|
-
'data-invalid-syntax': '
|
|
64
|
+
'data-invalid-syntax': 'list',
|
|
65
65
|
'data-invalid-type': 'syntax',
|
|
66
66
|
'data-invalid-message': 'Fix the indent or the head of the list item',
|
|
67
67
|
}, source.replace('\n', ''))
|
|
68
68
|
], '']);
|
|
69
69
|
|
|
70
|
-
function
|
|
71
|
-
switch (
|
|
70
|
+
function index(value: string): number {
|
|
71
|
+
switch (value) {
|
|
72
|
+
case 'i':
|
|
73
|
+
return 1;
|
|
74
|
+
case 'a':
|
|
75
|
+
return 2;
|
|
76
|
+
case 'I':
|
|
77
|
+
return 3;
|
|
78
|
+
case 'A':
|
|
79
|
+
return 4;
|
|
80
|
+
default:
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function type(value: string): string {
|
|
86
|
+
switch (value) {
|
|
72
87
|
case 'i':
|
|
73
88
|
return 'i';
|
|
74
89
|
case 'a':
|
|
@@ -97,7 +112,7 @@ function style(type: string): string {
|
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
function
|
|
115
|
+
function pattern(type: string): RegExp {
|
|
101
116
|
switch (type) {
|
|
102
117
|
case 'i':
|
|
103
118
|
return /^\(?i[).]?$/;
|
|
@@ -112,25 +127,37 @@ function initial(type: string): RegExp {
|
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
function format(
|
|
116
|
-
if (
|
|
117
|
-
|
|
130
|
+
function format(list: HTMLOListElement, type: string, form: string): HTMLOListElement {
|
|
131
|
+
if (list.firstElementChild?.firstElementChild?.classList.contains('checkbox')) {
|
|
132
|
+
list.classList.add('checklist');
|
|
118
133
|
}
|
|
119
|
-
define(
|
|
134
|
+
define(list, {
|
|
120
135
|
type: type || undefined,
|
|
121
136
|
'data-format': form === '.' ? undefined : 'paren',
|
|
122
137
|
'data-type': style(type) || undefined,
|
|
123
138
|
});
|
|
124
|
-
const marker =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
const marker = list.firstElementChild?.getAttribute('data-marker') ?? '';
|
|
140
|
+
// TODO: CSSカウンターをattr(start)でリセットできるようになればstart値からのオートインクリメントに対応させる。
|
|
141
|
+
const start = marker.match(pattern(type))?.[0] ?? '';
|
|
142
|
+
for (let es = list.children, len = es.length, i = 0; i < len; ++i) {
|
|
143
|
+
const item = es[i];
|
|
144
|
+
assert(item.getAttribute('data-marker') !== '');
|
|
145
|
+
switch (item.getAttribute('data-marker')) {
|
|
146
|
+
case null:
|
|
131
147
|
continue;
|
|
148
|
+
case start:
|
|
149
|
+
item.removeAttribute('data-marker');
|
|
150
|
+
continue;
|
|
151
|
+
case marker:
|
|
152
|
+
if (i === 0 || item.classList.contains('invalid')) continue;
|
|
153
|
+
define(item, {
|
|
154
|
+
class: 'invalid',
|
|
155
|
+
'data-invalid-syntax': 'list',
|
|
156
|
+
'data-invalid-type': 'index',
|
|
157
|
+
'data-invalid-message': 'Fix the duplicate index',
|
|
158
|
+
});
|
|
132
159
|
}
|
|
133
160
|
break;
|
|
134
161
|
}
|
|
135
|
-
return
|
|
162
|
+
return list;
|
|
136
163
|
}
|
|
@@ -30,6 +30,9 @@ describe('Unit: parser/block/paragraph', () => {
|
|
|
30
30
|
assert.deepStrictEqual(inspect(parser('_a\n<wbr>_\nb')), [['<p>_a<br><wbr>_<br>b</p>'], '']);
|
|
31
31
|
assert.deepStrictEqual(inspect(parser('*a\n<wbr>*\nb')), [['<p>*a<br><wbr>*<br>b</p>'], '']);
|
|
32
32
|
assert.deepStrictEqual(inspect(parser('==a\n<wbr>==\nb')), [['<p>==a<br><wbr>==<br>b</p>'], '']);
|
|
33
|
+
assert.deepStrictEqual(inspect(parser('http://host#!')), [['<p><a class="url" href="http://host#!" target="_blank">http://host#!</a></p>'], '']);
|
|
34
|
+
assert.deepStrictEqual(inspect(parser('a\nhttp://host#\\ \nb')), [['<p>a<br><a class="url" href="http://host#\\" target="_blank">http://host#\\</a><br>b</p>'], '']);
|
|
35
|
+
assert.deepStrictEqual(inspect(parser('!http://host#!')), [['<p><a href="http://host#!" target="_blank"><img class="media" data-src="http://host#!" alt=""></a></p>'], '']);
|
|
33
36
|
assert.deepStrictEqual(inspect(parser('\ta')), [['<p>\ta</p>'], '']);
|
|
34
37
|
});
|
|
35
38
|
|
|
@@ -14,8 +14,7 @@ export const cite: ReplyParser.CiteParser = creation(1, false, line(fmap(validat
|
|
|
14
14
|
// リンクの実装は後で検討
|
|
15
15
|
focus(/^>>\.(?=\s*$)/, () => [[html('a', { class: 'anchor' }, '>>.')], '']),
|
|
16
16
|
focus(/^>>#\S*(?=\s*$)/, ({ source }) => [[html('a', { class: 'anchor' }, source)], '']),
|
|
17
|
-
|
|
18
|
-
focus(/^>>https?:\/\/[^\p{C}\p{S}\p{P}\s]\S*(?=\s*$)/u, ({ source }) => [[html('a', { class: 'anchor', href: source.slice(2).trimEnd(), target: '_blank' }, source)], '']),
|
|
17
|
+
focus(/^>>https?:\/\/(?:[[]|[^\p{C}\p{S}\p{P}\s])\S*(?=\s*$)/u, ({ source }) => [[html('a', { class: 'anchor', href: source.slice(2).trimEnd(), target: '_blank' }, source)], '']),
|
|
19
18
|
]),
|
|
20
19
|
]))),
|
|
21
20
|
([el, quotes = '']: [HTMLElement, string?]) => [
|
|
@@ -55,6 +55,9 @@ describe('Unit: parser/block/reply/quote', () => {
|
|
|
55
55
|
assert.deepStrictEqual(inspect(parser('> $-a, $-b')), [['<span class="quote">> $-a, $-b</span>', '<br>'], '']);
|
|
56
56
|
assert.deepStrictEqual(inspect(parser('> $a=b$')), [['<span class="quote">> <span class="math" translate="no" data-src="$a=b$">$a=b$</span></span>', '<br>'], '']);
|
|
57
57
|
assert.deepStrictEqual(inspect(parser('> ${a}$')), [['<span class="quote">> <span class="math" translate="no" data-src="${a}$">${a}$</span></span>', '<br>'], '']);
|
|
58
|
+
assert.deepStrictEqual(inspect(parser('> http://host#!')), [['<span class="quote">> <a class="url" href="http://host#!" target="_blank">http://host#!</a></span>', '<br>'], '']);
|
|
59
|
+
assert.deepStrictEqual(inspect(parser('> a\n> http://host#\\ \n> b')), [['<span class="quote">> a<br>> <a class="url" href="http://host#\\" target="_blank">http://host#\\</a> <br>> b</span>', '<br>'], '']);
|
|
60
|
+
assert.deepStrictEqual(inspect(parser('> !http://host#!')), [['<span class="quote">> !<a class="url" href="http://host#!" target="_blank">http://host#!</a></span>', '<br>'], '']);
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ReplyParser } from '../../block';
|
|
2
2
|
import { eval } from '../../../combinator/data/parser';
|
|
3
|
-
import { union, some, creation, block, line, validate, rewrite, lazy, fmap } from '../../../combinator';
|
|
3
|
+
import { union, subsequence, some, creation, block, line, validate, rewrite, lazy, fmap } from '../../../combinator';
|
|
4
4
|
import { math } from '../../inline/math';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { autolink } from '../../inline/autolink';
|
|
6
|
+
import { linebreak, unescsource, str, anyline } from '../../source';
|
|
7
|
+
import { lineurl } from '../../autolink';
|
|
7
8
|
import { html, defrag } from 'typed-dom/dom';
|
|
8
9
|
|
|
9
10
|
export const syntax = /^>+(?=[^\S\n])|^>(?=[^\s>])|^>+(?=[^\s>])(?![0-9a-z]+(?:-[0-9a-z]+)*(?![0-9A-Za-z@#:]))/;
|
|
@@ -40,8 +41,8 @@ const qblock: ReplyParser.QuoteParser.BlockParser = ({ source, context }) => {
|
|
|
40
41
|
const quotes = source.match(/^>+[^\S\n]/mg)!;
|
|
41
42
|
assert(quotes);
|
|
42
43
|
assert(quotes.length > 0);
|
|
43
|
-
const content = lines.reduce((acc, line,
|
|
44
|
-
const nodes = eval(
|
|
44
|
+
const content = lines.reduce((acc, line, i) => acc + line.slice(quotes[i].length), '');
|
|
45
|
+
const nodes = eval(text({ source: content, context }), []);
|
|
45
46
|
nodes.unshift(quotes.shift()!);
|
|
46
47
|
for (let i = 0; i < nodes.length; ++i) {
|
|
47
48
|
const child = nodes[i] as string | Text | Element;
|
|
@@ -71,7 +72,12 @@ const qblock: ReplyParser.QuoteParser.BlockParser = ({ source, context }) => {
|
|
|
71
72
|
return [nodes, ''];
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
const text: ReplyParser.QuoteParser.TextParser =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const text: ReplyParser.QuoteParser.TextParser = some(line(subsequence([
|
|
76
|
+
lineurl,
|
|
77
|
+
some(union([
|
|
78
|
+
math, // quote補助関数が残した数式をパースする。他の構文で数式を残す場合はソーステキストを直接使用する。
|
|
79
|
+
autolink,
|
|
80
|
+
linebreak,
|
|
81
|
+
unescsource,
|
|
82
|
+
])),
|
|
83
|
+
])));
|
|
@@ -26,6 +26,6 @@ const source: SidefenceParser.SourceParser = lazy(() => fmap(
|
|
|
26
26
|
convert(unindent, source)),
|
|
27
27
|
rewrite(
|
|
28
28
|
some(contentline, opener),
|
|
29
|
-
convert(unindent, fmap(
|
|
29
|
+
convert(unindent, fmap(autolink, ns => [html('pre', defrag(ns))]))),
|
|
30
30
|
]))),
|
|
31
31
|
ns => [html('blockquote', ns)]));
|
|
@@ -17,7 +17,7 @@ describe('Unit: parser/block/ulist', () => {
|
|
|
17
17
|
assert.deepStrictEqual(inspect(parser('-[ ]')), undefined);
|
|
18
18
|
assert.deepStrictEqual(inspect(parser('-[x]')), undefined);
|
|
19
19
|
assert.deepStrictEqual(inspect(parser('-\n')), undefined);
|
|
20
|
-
assert.deepStrictEqual(inspect(parser(' -')), undefined);
|
|
20
|
+
assert.deepStrictEqual(inspect(parser(' - ')), undefined);
|
|
21
21
|
assert.deepStrictEqual(inspect(parser('+')), undefined);
|
|
22
22
|
assert.deepStrictEqual(inspect(parser('*')), undefined);
|
|
23
23
|
});
|
|
@@ -26,8 +26,8 @@ describe('Unit: parser/block/ulist', () => {
|
|
|
26
26
|
// pending
|
|
27
27
|
assert.deepStrictEqual(inspect(parser('- ')), [['<ul><li></li></ul>'], '']);
|
|
28
28
|
// filled
|
|
29
|
-
assert.deepStrictEqual(inspect(parser('- \\')), [['<ul><li
|
|
30
|
-
assert.deepStrictEqual(inspect(parser('- \\\n')), [['<ul><li
|
|
29
|
+
assert.deepStrictEqual(inspect(parser('- \\')), [['<ul><li id="index::\\">\\</li></ul>'], '']);
|
|
30
|
+
assert.deepStrictEqual(inspect(parser('- \\\n')), [['<ul><li id="index::\\">\\</li></ul>'], '']);
|
|
31
31
|
assert.deepStrictEqual(inspect(parser('- -')), [['<ul><li id="index::-">-</li></ul>'], '']);
|
|
32
32
|
assert.deepStrictEqual(inspect(parser('- -\n')), [['<ul><li id="index::-">-</li></ul>'], '']);
|
|
33
33
|
});
|
|
@@ -4,7 +4,7 @@ import { olist_, invalid } from './olist';
|
|
|
4
4
|
import { ilist_ } from './ilist';
|
|
5
5
|
import { inline, indexer, indexee } from '../inline';
|
|
6
6
|
import { State } from '../context';
|
|
7
|
-
import { trimBlank } from '../visibility';
|
|
7
|
+
import { visualize, trimBlank } from '../visibility';
|
|
8
8
|
import { unshift } from 'spica/array';
|
|
9
9
|
import { html, defrag } from 'typed-dom/dom';
|
|
10
10
|
|
|
@@ -18,7 +18,7 @@ export const ulist_: UListParser = lazy(() => block(fmap(validate(
|
|
|
18
18
|
some(creation(1, false, union([
|
|
19
19
|
indexee(fmap(fallback(
|
|
20
20
|
inits([
|
|
21
|
-
line(open(/^-(?:$|\s)/, subsequence([checkbox, trimBlank(some(union([indexer, inline])))]), true)),
|
|
21
|
+
line(open(/^-(?:$|\s)/, subsequence([checkbox, trimBlank(visualize(some(union([indexer, inline]))))]), true)),
|
|
22
22
|
indent(union([ulist_, olist_, ilist_])),
|
|
23
23
|
]),
|
|
24
24
|
invalid),
|
|
@@ -40,9 +40,9 @@ export function fillFirstLine(ns: (HTMLElement | string)[]): (HTMLElement | stri
|
|
|
40
40
|
: ns;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function format(
|
|
44
|
-
if (
|
|
45
|
-
|
|
43
|
+
function format(list: HTMLUListElement): HTMLUListElement {
|
|
44
|
+
if (list.firstElementChild?.firstElementChild?.classList.contains('checkbox')) {
|
|
45
|
+
list.classList.add('checklist');
|
|
46
46
|
}
|
|
47
|
-
return
|
|
47
|
+
return list;
|
|
48
48
|
}
|
|
@@ -14,7 +14,7 @@ export const index: IndexParser = lazy(() => validate('[#', fmap(indexee(surroun
|
|
|
14
14
|
constraint(State.index, false,
|
|
15
15
|
syntax(Syntax.index, 2, 1, State.linkers | State.media,
|
|
16
16
|
startTight(
|
|
17
|
-
open(stropt(
|
|
17
|
+
open(stropt('|'), trimBlankEnd(some(union([
|
|
18
18
|
signature,
|
|
19
19
|
inline,
|
|
20
20
|
]), ']', [[/^\\?\n/, 9], [']', 2]])), true)))),
|
|
@@ -9,7 +9,7 @@ export function indexee(parser: Parser<HTMLElement, MarkdownParser.Context>, opt
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function identity(id: string | undefined, text: string, name: 'index' | 'mark' = 'index'): string | undefined {
|
|
12
|
-
assert(!id?.match(/[^0-9a-z
|
|
12
|
+
assert(!id?.match(/[^0-9a-z/-]/i));
|
|
13
13
|
assert(!text.includes('\n'));
|
|
14
14
|
if (id === '') return undefined;
|
|
15
15
|
text &&= text.trim().replace(/\s+/g, '_');
|
|
@@ -33,6 +33,7 @@ assert(identity(undefined, '0'.repeat(200) + 1, 'mark')!.slice(6) === '0'.repeat
|
|
|
33
33
|
export function text(source: HTMLElement | DocumentFragment, optional = false): string {
|
|
34
34
|
assert(source instanceof DocumentFragment || !source.matches('.indexer'));
|
|
35
35
|
assert(source.querySelectorAll(':scope > .indexer').length <= 1);
|
|
36
|
+
if (!source.firstChild) return '';
|
|
36
37
|
const indexer = source.querySelector(':scope > .indexer');
|
|
37
38
|
const index = indexer?.getAttribute('data-index');
|
|
38
39
|
if (index) return index;
|
|
@@ -139,8 +139,7 @@ export function* figure(
|
|
|
139
139
|
}
|
|
140
140
|
labels.add(label);
|
|
141
141
|
opts.id !== '' && def.setAttribute('id', `label:${opts.id ? `${opts.id}:` : ''}${label}`);
|
|
142
|
-
for (
|
|
143
|
-
const ref = rs[i];
|
|
142
|
+
for (const ref of refs.take(label, Infinity)) {
|
|
144
143
|
if (ref.getAttribute('data-invalid-message') === messages.reference) {
|
|
145
144
|
define(ref, {
|
|
146
145
|
class: void ref.classList.remove('invalid'),
|
|
@@ -22,7 +22,7 @@ describe('Unit: parser/processor/footnote', () => {
|
|
|
22
22
|
const target = parse('((a b))');
|
|
23
23
|
const footnote = html('ol');
|
|
24
24
|
for (let i = 0; i < 3; ++i) {
|
|
25
|
-
assert.deepStrictEqual([...annotation(target, footnote)].length, i === 0 ?
|
|
25
|
+
assert.deepStrictEqual([...annotation(target, footnote)].length, i === 0 ? 2 : 1);
|
|
26
26
|
assert.deepStrictEqual(
|
|
27
27
|
[...target.children].map(el => el.outerHTML),
|
|
28
28
|
[
|
|
@@ -48,7 +48,7 @@ describe('Unit: parser/processor/footnote', () => {
|
|
|
48
48
|
const target = parse('((1))((12345678901234567890))');
|
|
49
49
|
const footnote = html('ol');
|
|
50
50
|
for (let i = 0; i < 3; ++i) {
|
|
51
|
-
assert.deepStrictEqual([...annotation(target, footnote)].length, i === 0 ?
|
|
51
|
+
assert.deepStrictEqual([...annotation(target, footnote)].length, i === 0 ? 4 : 2);
|
|
52
52
|
assert.deepStrictEqual(
|
|
53
53
|
[...target.children].map(el => el.outerHTML),
|
|
54
54
|
[
|
|
@@ -165,7 +165,7 @@ describe('Unit: parser/processor/footnote', () => {
|
|
|
165
165
|
const target = parse('((a b))');
|
|
166
166
|
const footnote = html('ol');
|
|
167
167
|
for (let i = 0; i < 3; ++i) {
|
|
168
|
-
assert.deepStrictEqual([...annotation(target, footnote, { id: '0' })].length, i === 0 ?
|
|
168
|
+
assert.deepStrictEqual([...annotation(target, footnote, { id: '0' })].length, i === 0 ? 2 : 1);
|
|
169
169
|
assert.deepStrictEqual(
|
|
170
170
|
[...target.children].map(el => el.outerHTML),
|
|
171
171
|
[
|