pantsdown 1.0.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/.eslintrc.json +26 -0
- package/.github/workflows/release.yaml +44 -0
- package/.prettierrc.json +11 -0
- package/LICENSE.md +67 -0
- package/README.md +5 -0
- package/bun.lockb +0 -0
- package/package.json +46 -0
- package/src/index.ts +2 -0
- package/src/lexer.ts +369 -0
- package/src/pantsdown.ts +19 -0
- package/src/parser.ts +216 -0
- package/src/renderer.ts +145 -0
- package/src/rules/block.ts +129 -0
- package/src/rules/inline.ts +146 -0
- package/src/rules/utils.ts +18 -0
- package/src/sample.test.ts +5 -0
- package/src/tokenizer.ts +773 -0
- package/src/types.ts +183 -0
- package/src/utils.ts +231 -0
- package/tsconfig.json +33 -0
package/src/parser.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Renderer } from "./renderer.ts";
|
|
2
|
+
import { type RendererProps, type Token, type Tokens } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parsing & Compiling
|
|
6
|
+
*/
|
|
7
|
+
export class Parser {
|
|
8
|
+
private renderer: Renderer;
|
|
9
|
+
|
|
10
|
+
constructor({ renderer }: { renderer: RendererProps }) {
|
|
11
|
+
this.renderer = new Renderer(renderer);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse Loop
|
|
16
|
+
*/
|
|
17
|
+
parse(tokens: Token[], top = true): string {
|
|
18
|
+
let out = "";
|
|
19
|
+
|
|
20
|
+
for (let i = 0, len = tokens.length; i < len; i++) {
|
|
21
|
+
const token = tokens[i]!;
|
|
22
|
+
|
|
23
|
+
switch (token.type) {
|
|
24
|
+
case "space": {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
case "hr": {
|
|
28
|
+
out += this.renderer.hr();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
case "heading": {
|
|
32
|
+
out += this.renderer.heading(this.parseInline(token.tokens), token.depth);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
case "code": {
|
|
36
|
+
out += this.renderer.code(token.text, token.lang);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
case "table": {
|
|
40
|
+
let header = "";
|
|
41
|
+
|
|
42
|
+
// header
|
|
43
|
+
let cell = "";
|
|
44
|
+
for (let j = 0, len = token.header.length; j < len; j++) {
|
|
45
|
+
cell += this.renderer.tablecell(this.parseInline(token.header[j]!.tokens), {
|
|
46
|
+
header: true,
|
|
47
|
+
align: token.align[j]!,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
header += this.renderer.tablerow(cell);
|
|
51
|
+
|
|
52
|
+
let body = "";
|
|
53
|
+
for (let j = 0, rowsLen = token.rows.length; j < rowsLen; j++) {
|
|
54
|
+
const row = token.rows[j]!;
|
|
55
|
+
|
|
56
|
+
cell = "";
|
|
57
|
+
for (let k = 0, rowLen = row.length; k < rowLen; k++) {
|
|
58
|
+
cell += this.renderer.tablecell(this.parseInline(row[k]!.tokens), {
|
|
59
|
+
header: false,
|
|
60
|
+
align: token.align[k]!,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
body += this.renderer.tablerow(cell);
|
|
65
|
+
}
|
|
66
|
+
out += this.renderer.table(header, body);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
case "blockquote": {
|
|
70
|
+
const body = this.parse(token.tokens);
|
|
71
|
+
out += this.renderer.blockquote(body);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
case "list": {
|
|
75
|
+
const ordered = token.ordered;
|
|
76
|
+
const start = token.start;
|
|
77
|
+
const loose = token.loose;
|
|
78
|
+
|
|
79
|
+
let body = "";
|
|
80
|
+
for (let j = 0, itemsLen = token.items.length; j < itemsLen; j++) {
|
|
81
|
+
const item = token.items[j]!;
|
|
82
|
+
const checked = item.checked;
|
|
83
|
+
const task = item.task;
|
|
84
|
+
|
|
85
|
+
let itemBody = "";
|
|
86
|
+
if (item.task) {
|
|
87
|
+
const checkbox = this.renderer.checkbox(!!checked);
|
|
88
|
+
if (loose) {
|
|
89
|
+
if (
|
|
90
|
+
item.tokens.length > 0 &&
|
|
91
|
+
item.tokens[0]!.type === "paragraph"
|
|
92
|
+
) {
|
|
93
|
+
item.tokens[0]!.text = checkbox + " " + item.tokens[0]!.text;
|
|
94
|
+
if (
|
|
95
|
+
item.tokens[0]!.tokens.length > 0 &&
|
|
96
|
+
item.tokens[0]!.tokens[0]!.type === "text"
|
|
97
|
+
) {
|
|
98
|
+
item.tokens[0]!.tokens[0]!.text =
|
|
99
|
+
checkbox + " " + item.tokens[0]!.tokens[0]!.text;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
item.tokens.unshift({
|
|
103
|
+
type: "text",
|
|
104
|
+
text: checkbox + " ",
|
|
105
|
+
} as Tokens["Text"]);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
itemBody += checkbox + " ";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
itemBody += this.parse(item.tokens, loose);
|
|
113
|
+
body += this.renderer.listitem(itemBody, task, !!checked);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
out += this.renderer.list(body, ordered, start);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
case "html": {
|
|
120
|
+
out += this.renderer.html(token.text, token.block);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
case "paragraph": {
|
|
124
|
+
out += this.renderer.paragraph(this.parseInline(token.tokens));
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
case "text": {
|
|
128
|
+
let textToken = token as Tokens["Text"];
|
|
129
|
+
let body = textToken.tokens
|
|
130
|
+
? this.parseInline(textToken.tokens)
|
|
131
|
+
: textToken.text;
|
|
132
|
+
while (i + 1 < tokens.length && tokens[i + 1]?.type === "text") {
|
|
133
|
+
textToken = tokens[++i] as Tokens["Text"];
|
|
134
|
+
body +=
|
|
135
|
+
"\n" +
|
|
136
|
+
(textToken.tokens
|
|
137
|
+
? this.parseInline(textToken.tokens)
|
|
138
|
+
: textToken.text);
|
|
139
|
+
}
|
|
140
|
+
out += top ? this.renderer.paragraph(body) : body;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
default: {
|
|
145
|
+
const errMsg = 'Token with "' + token.type + '" type was not found.';
|
|
146
|
+
throw new Error(errMsg);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parse Inline Tokens
|
|
156
|
+
*/
|
|
157
|
+
parseInline(tokens: Token[]): string {
|
|
158
|
+
let out = "";
|
|
159
|
+
|
|
160
|
+
for (let i = 0, len = tokens.length; i < len; i++) {
|
|
161
|
+
const token = tokens[i]!;
|
|
162
|
+
|
|
163
|
+
switch (token.type) {
|
|
164
|
+
case "escape": {
|
|
165
|
+
out += this.renderer.text(token.text);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case "html": {
|
|
169
|
+
out += this.renderer.html(token.text);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case "link": {
|
|
173
|
+
out += this.renderer.link(
|
|
174
|
+
token.href,
|
|
175
|
+
token.title,
|
|
176
|
+
this.parseInline(token.tokens),
|
|
177
|
+
);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "image": {
|
|
181
|
+
out += this.renderer.image(token.href, token.title, token.text);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case "strong": {
|
|
185
|
+
out += this.renderer.strong(this.parseInline(token.tokens));
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "em": {
|
|
189
|
+
out += this.renderer.em(this.parseInline(token.tokens));
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "codespan": {
|
|
193
|
+
out += this.renderer.codespan(token.text);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case "br": {
|
|
197
|
+
out += this.renderer.br();
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "del": {
|
|
201
|
+
out += this.renderer.del(this.parseInline(token.tokens));
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "text": {
|
|
205
|
+
out += this.renderer.text(token.text);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default: {
|
|
209
|
+
const errMsg = 'Token with "' + token.type + '" type was not found.';
|
|
210
|
+
throw new Error(errMsg);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
}
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import hljs from "highlight.js";
|
|
2
|
+
import { type RendererProps } from "./types.ts";
|
|
3
|
+
import { cleanUrl, escape, fixHtmlLocalImageHref, fixLocalImageHref } from "./utils.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An object containing functions to render tokens to HTML.
|
|
7
|
+
*/
|
|
8
|
+
export class Renderer {
|
|
9
|
+
private props: RendererProps;
|
|
10
|
+
|
|
11
|
+
constructor(props: RendererProps) {
|
|
12
|
+
this.props = props;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
code(code: string, infostring: string | undefined): string {
|
|
16
|
+
const lang = (infostring ?? "").match(/^\S*/)?.[0];
|
|
17
|
+
|
|
18
|
+
code = code.replace(/\n$/, "") + "\n";
|
|
19
|
+
|
|
20
|
+
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
21
|
+
const highlightedCode = hljs.highlight(code, { language }).value;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
'<pre><code class="hljs language-' +
|
|
25
|
+
escape(language) +
|
|
26
|
+
'">' +
|
|
27
|
+
highlightedCode +
|
|
28
|
+
"</code></pre>\n"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
blockquote(quote: string): string {
|
|
33
|
+
return `<blockquote>\n${quote}</blockquote>\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
html(html: string, _block?: boolean): string {
|
|
37
|
+
return fixHtmlLocalImageHref(html, this.props.localImageUrlPrefix);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
heading(text: string, level: number): string {
|
|
41
|
+
// ignore IDs
|
|
42
|
+
return `<h${level}>${text}</h${level}>\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
hr(): string {
|
|
46
|
+
return "<hr>\n";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
list(body: string, ordered: boolean, start: number | ""): string {
|
|
50
|
+
const type = ordered ? "ol" : "ul";
|
|
51
|
+
const startatt = ordered && start !== 1 ? ' start="' + start + '"' : "";
|
|
52
|
+
return "<" + type + startatt + ">\n" + body + "</" + type + ">\n";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
listitem(text: string, _task: boolean, _checked: boolean): string {
|
|
56
|
+
return `<li>${text}</li>\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
checkbox(checked: boolean): string {
|
|
60
|
+
return "<input " + (checked ? 'checked="" ' : "") + 'disabled="" type="checkbox">';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
paragraph(text: string): string {
|
|
64
|
+
return `<p>${text}</p>\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
table(header: string, body: string): string {
|
|
68
|
+
if (body) body = `<tbody>${body}</tbody>`;
|
|
69
|
+
|
|
70
|
+
return "<table>\n" + "<thead>\n" + header + "</thead>\n" + body + "</table>\n";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
tablerow(content: string): string {
|
|
74
|
+
return `<tr>\n${content}</tr>\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
tablecell(
|
|
78
|
+
content: string,
|
|
79
|
+
flags: {
|
|
80
|
+
header: boolean;
|
|
81
|
+
align: "center" | "left" | "right" | null;
|
|
82
|
+
},
|
|
83
|
+
): string {
|
|
84
|
+
const type = flags.header ? "th" : "td";
|
|
85
|
+
const tag = flags.align ? `<${type} align="${flags.align}">` : `<${type}>`;
|
|
86
|
+
return tag + content + `</${type}>\n`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* span level renderer
|
|
91
|
+
*/
|
|
92
|
+
strong(text: string): string {
|
|
93
|
+
return `<strong>${text}</strong>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
em(text: string): string {
|
|
97
|
+
return `<em>${text}</em>`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
codespan(text: string): string {
|
|
101
|
+
return `<code>${text}</code>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
br(): string {
|
|
105
|
+
return "<br>";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
del(text: string): string {
|
|
109
|
+
return `<del>${text}</del>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
link(href: string, title: string | null | undefined, text: string): string {
|
|
113
|
+
const cleanHref = cleanUrl(href);
|
|
114
|
+
if (cleanHref === null) {
|
|
115
|
+
return text;
|
|
116
|
+
}
|
|
117
|
+
href = cleanHref;
|
|
118
|
+
let out = '<a href="' + href + '"';
|
|
119
|
+
if (title) {
|
|
120
|
+
out += ' title="' + title + '"';
|
|
121
|
+
}
|
|
122
|
+
out += ">" + text + "</a>";
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
image(href: string, title: string | null, text: string): string {
|
|
127
|
+
const cleanHref = cleanUrl(href);
|
|
128
|
+
if (cleanHref === null) {
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
href = fixLocalImageHref(cleanHref, this.props.localImageUrlPrefix);
|
|
132
|
+
|
|
133
|
+
let out = `<img src="${href}" alt="${text}"`;
|
|
134
|
+
|
|
135
|
+
if (title) {
|
|
136
|
+
out += ` title="${title}"`;
|
|
137
|
+
}
|
|
138
|
+
out += ">";
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
text(text: string): string {
|
|
143
|
+
return text;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { edit } from "./utils.ts";
|
|
2
|
+
|
|
3
|
+
type BlockRuleNames =
|
|
4
|
+
| "newline"
|
|
5
|
+
| "code"
|
|
6
|
+
| "fences"
|
|
7
|
+
| "hr"
|
|
8
|
+
| "heading"
|
|
9
|
+
| "blockquote"
|
|
10
|
+
| "list"
|
|
11
|
+
| "html"
|
|
12
|
+
| "def"
|
|
13
|
+
| "table"
|
|
14
|
+
| "lheading"
|
|
15
|
+
| "text"
|
|
16
|
+
| "bullet"
|
|
17
|
+
| "listItemStart"
|
|
18
|
+
| "paragraph";
|
|
19
|
+
|
|
20
|
+
export const label = /(?!\s*\])(?:\\.|[^\[\]\\])+/;
|
|
21
|
+
|
|
22
|
+
const tag =
|
|
23
|
+
"address|article|aside|base|basefont|blockquote|body|caption" +
|
|
24
|
+
"|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption" +
|
|
25
|
+
"|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe" +
|
|
26
|
+
"|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option" +
|
|
27
|
+
"|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr" +
|
|
28
|
+
"|track|ul";
|
|
29
|
+
|
|
30
|
+
const title = /(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/;
|
|
31
|
+
|
|
32
|
+
const block_def = edit(
|
|
33
|
+
/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,
|
|
34
|
+
)
|
|
35
|
+
.replace("label", label)
|
|
36
|
+
.replace("title", title)
|
|
37
|
+
.getRegex();
|
|
38
|
+
|
|
39
|
+
const block_bullet = /(?:[*+-]|\d{1,9}[.)])/;
|
|
40
|
+
|
|
41
|
+
const block_listItemStart = edit(/^( *)(bull) */)
|
|
42
|
+
.replace("bull", block_bullet)
|
|
43
|
+
.getRegex();
|
|
44
|
+
|
|
45
|
+
const block_list = edit(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/)
|
|
46
|
+
.replace(/bull/g, block_bullet)
|
|
47
|
+
.replace("hr", "\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))")
|
|
48
|
+
.replace("def", "\\n+(?=" + block_def.source + ")")
|
|
49
|
+
.getRegex();
|
|
50
|
+
|
|
51
|
+
export const comment = /<!--(?!-?>)[\s\S]*?(?:-->|$)/;
|
|
52
|
+
|
|
53
|
+
const block_html = edit(
|
|
54
|
+
"^ {0,3}(?:" + // optional indentation
|
|
55
|
+
"<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)" + // (1)
|
|
56
|
+
"|comment[^\\n]*(\\n+|$)" + // (2)
|
|
57
|
+
"|<\\?[\\s\\S]*?(?:\\?>\\n*|$)" + // (3)
|
|
58
|
+
"|<![A-Z][\\s\\S]*?(?:>\\n*|$)" + // (4)
|
|
59
|
+
"|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)" + // (5)
|
|
60
|
+
"|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (6)
|
|
61
|
+
"|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (7) open tag
|
|
62
|
+
"|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)" + // (7) closing tag
|
|
63
|
+
")",
|
|
64
|
+
"i",
|
|
65
|
+
)
|
|
66
|
+
.replace("comment", comment)
|
|
67
|
+
.replace("tag", tag)
|
|
68
|
+
.replace(
|
|
69
|
+
"attribute",
|
|
70
|
+
/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/,
|
|
71
|
+
)
|
|
72
|
+
.getRegex();
|
|
73
|
+
|
|
74
|
+
const block_lheading = edit(/^(?!bull )((?:.|\n(?!\s*?\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/)
|
|
75
|
+
.replace(/bull/g, block_bullet) // lists can interrupt
|
|
76
|
+
.getRegex();
|
|
77
|
+
|
|
78
|
+
const block_hr = /^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/;
|
|
79
|
+
|
|
80
|
+
const block_table = edit(
|
|
81
|
+
"^ *([^\\n ].*)\\n" + // Header
|
|
82
|
+
" {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)" + // Align
|
|
83
|
+
"(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)", // Cells
|
|
84
|
+
)
|
|
85
|
+
.replace("hr", block_hr)
|
|
86
|
+
.replace("heading", " {0,3}#{1,6} ")
|
|
87
|
+
.replace("blockquote", " {0,3}>")
|
|
88
|
+
.replace("code", " {4}[^\\n]")
|
|
89
|
+
.replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n")
|
|
90
|
+
.replace("list", " {0,3}(?:[*+-]|1[.)]) ") // only lists starting from 1 can interrupt
|
|
91
|
+
.replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)")
|
|
92
|
+
.replace("tag", tag) // tables can be interrupted by type (6) html blocks
|
|
93
|
+
.getRegex();
|
|
94
|
+
|
|
95
|
+
const block_paragraph = edit(
|
|
96
|
+
/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,
|
|
97
|
+
)
|
|
98
|
+
.replace("hr", block_hr)
|
|
99
|
+
.replace("heading", " {0,3}#{1,6} ")
|
|
100
|
+
.replace("|lheading", "") // setex headings don't interrupt commonmark paragraphs
|
|
101
|
+
.replace("table", block_table) // interrupt paragraphs with table
|
|
102
|
+
.replace("blockquote", " {0,3}>")
|
|
103
|
+
.replace("fences", " {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n")
|
|
104
|
+
.replace("list", " {0,3}(?:[*+-]|1[.)]) ") // only lists starting from 1 can interrupt
|
|
105
|
+
.replace("html", "</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)")
|
|
106
|
+
.replace("tag", tag) // pars can be interrupted by type (6) html blocks
|
|
107
|
+
.getRegex();
|
|
108
|
+
|
|
109
|
+
const block_blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/)
|
|
110
|
+
.replace("paragraph", block_paragraph)
|
|
111
|
+
.getRegex();
|
|
112
|
+
|
|
113
|
+
export const block: Record<BlockRuleNames, RegExp> = {
|
|
114
|
+
newline: /^(?: *(?:\n|$))+/,
|
|
115
|
+
code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,
|
|
116
|
+
fences: /^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,
|
|
117
|
+
hr: block_hr,
|
|
118
|
+
heading: /^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,
|
|
119
|
+
blockquote: block_blockquote,
|
|
120
|
+
list: block_list,
|
|
121
|
+
html: block_html,
|
|
122
|
+
bullet: block_bullet,
|
|
123
|
+
listItemStart: block_listItemStart,
|
|
124
|
+
def: block_def,
|
|
125
|
+
table: block_table,
|
|
126
|
+
lheading: block_lheading,
|
|
127
|
+
paragraph: block_paragraph,
|
|
128
|
+
text: /^[^\n]+/,
|
|
129
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { comment as blockComment, label as blockLabel } from "./block.ts";
|
|
2
|
+
import { edit } from "./utils.ts";
|
|
3
|
+
|
|
4
|
+
type InlineRuleNames =
|
|
5
|
+
| "escape"
|
|
6
|
+
| "autolink"
|
|
7
|
+
| "tag"
|
|
8
|
+
| "link"
|
|
9
|
+
| "reflink"
|
|
10
|
+
| "nolink"
|
|
11
|
+
| "reflinkSearch"
|
|
12
|
+
| "code"
|
|
13
|
+
| "br"
|
|
14
|
+
| "del"
|
|
15
|
+
| "url"
|
|
16
|
+
| "text"
|
|
17
|
+
| "emStrong"
|
|
18
|
+
| "anyPunctuation"
|
|
19
|
+
| "punctuation"
|
|
20
|
+
| "blockSkip"
|
|
21
|
+
| "escapes"
|
|
22
|
+
| "backpedal";
|
|
23
|
+
|
|
24
|
+
// list of unicode punctuation marks, plus any missing characters from CommonMark spec
|
|
25
|
+
const punctuation = "\\p{P}$+<=>`^|~";
|
|
26
|
+
const title = /"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/;
|
|
27
|
+
const href = /<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/;
|
|
28
|
+
const scheme = /[a-zA-Z][a-zA-Z0-9+.-]{1,31}/;
|
|
29
|
+
const comment = edit(blockComment).replace("(?:-->|$)", "-->").getRegex();
|
|
30
|
+
const attribute = /\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/;
|
|
31
|
+
const label = /(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/;
|
|
32
|
+
const email =
|
|
33
|
+
/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/;
|
|
34
|
+
const extended_email = /[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/;
|
|
35
|
+
|
|
36
|
+
const inline_punctuation = edit(/^((?![*_])[\spunctuation])/, "u")
|
|
37
|
+
.replace(/punctuation/g, punctuation)
|
|
38
|
+
.getRegex();
|
|
39
|
+
|
|
40
|
+
// sequences em should skip over [title](link), `code`, <html>
|
|
41
|
+
const inline_blockSkip = /\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g;
|
|
42
|
+
|
|
43
|
+
const inline_emStrong = {
|
|
44
|
+
lDelim: edit(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/, "u")
|
|
45
|
+
.replace(/punct/g, punctuation)
|
|
46
|
+
.getRegex(),
|
|
47
|
+
// (1) and (2) can only be a Right Delimiter. (3) and (4) can only be Left. (5) and (6) can be either Left or Right.
|
|
48
|
+
// | Skip orphan inside strong | Consume to delim | (1) #*** | (2) a***#, a*** | (3) #***a, ***a | (4) ***# | (5) #***# | (6) a***a
|
|
49
|
+
rDelimAst: edit(
|
|
50
|
+
/^[^_*]*?__[^_*]*?\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\*)[punct](\*+)(?=[\s]|$)|[^punct\s](\*+)(?!\*)(?=[punct\s]|$)|(?!\*)[punct\s](\*+)(?=[^punct\s])|[\s](\*+)(?!\*)(?=[punct])|(?!\*)[punct](\*+)(?!\*)(?=[punct])|[^punct\s](\*+)(?=[^punct\s])/,
|
|
51
|
+
"gu",
|
|
52
|
+
)
|
|
53
|
+
.replace(/punct/g, punctuation)
|
|
54
|
+
.getRegex(),
|
|
55
|
+
rDelimUnd: edit(
|
|
56
|
+
// ^- Not allowed for _
|
|
57
|
+
/^[^_*]*?\*\*[^_*]*?_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\s]|$)|[^punct\s](_+)(?!_)(?=[punct\s]|$)|(?!_)[punct\s](_+)(?=[^punct\s])|[\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])/,
|
|
58
|
+
"gu",
|
|
59
|
+
)
|
|
60
|
+
.replace(/punct/g, punctuation)
|
|
61
|
+
.getRegex(),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const inline_anyPunctuation = edit(/\\[punct]/g, "gu")
|
|
65
|
+
.replace(/punct/g, punctuation)
|
|
66
|
+
.getRegex();
|
|
67
|
+
|
|
68
|
+
const inline_escapes = edit(/\\([punct])/g, "gu")
|
|
69
|
+
.replace(/punct/g, punctuation)
|
|
70
|
+
.getRegex();
|
|
71
|
+
|
|
72
|
+
const inline_autolink = edit(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/)
|
|
73
|
+
.replace("scheme", scheme)
|
|
74
|
+
.replace("email", email)
|
|
75
|
+
.getRegex();
|
|
76
|
+
|
|
77
|
+
const inline_tag = edit(
|
|
78
|
+
"^comment" +
|
|
79
|
+
"|^</[a-zA-Z][\\w:-]*\\s*>" + // self-closing tag
|
|
80
|
+
"|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>" + // open tag
|
|
81
|
+
"|^<\\?[\\s\\S]*?\\?>" + // processing instruction, e.g. <?php ?>
|
|
82
|
+
"|^<![a-zA-Z]+\\s[\\s\\S]*?>" + // declaration, e.g. <!DOCTYPE html>
|
|
83
|
+
"|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>", // CDATA section
|
|
84
|
+
)
|
|
85
|
+
.replace("comment", comment)
|
|
86
|
+
.replace("attribute", attribute)
|
|
87
|
+
.getRegex();
|
|
88
|
+
|
|
89
|
+
const inline_link = edit(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/)
|
|
90
|
+
.replace("label", label)
|
|
91
|
+
.replace("href", href)
|
|
92
|
+
.replace("title", title)
|
|
93
|
+
.getRegex();
|
|
94
|
+
|
|
95
|
+
const inline_reflink = edit(/^!?\[(label)\]\[(ref)\]/)
|
|
96
|
+
.replace("label", label)
|
|
97
|
+
.replace("ref", blockLabel)
|
|
98
|
+
.getRegex();
|
|
99
|
+
|
|
100
|
+
const inline_nolink = edit(/^!?\[(ref)\](?:\[\])?/)
|
|
101
|
+
.replace("ref", blockLabel)
|
|
102
|
+
.getRegex();
|
|
103
|
+
|
|
104
|
+
const inline_reflinkSearch = edit("reflink|nolink(?!\\()", "g")
|
|
105
|
+
.replace("reflink", inline_reflink)
|
|
106
|
+
.replace("nolink", inline_nolink)
|
|
107
|
+
.getRegex();
|
|
108
|
+
|
|
109
|
+
const inline_escape = edit(/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/)
|
|
110
|
+
.replace("])", "~|])")
|
|
111
|
+
.getRegex();
|
|
112
|
+
|
|
113
|
+
const inline_backpedal =
|
|
114
|
+
/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/;
|
|
115
|
+
|
|
116
|
+
const inline_del = /^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/;
|
|
117
|
+
|
|
118
|
+
const inline_text =
|
|
119
|
+
/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/;
|
|
120
|
+
|
|
121
|
+
const inline_url = edit(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/, "i")
|
|
122
|
+
.replace("email", extended_email)
|
|
123
|
+
.getRegex();
|
|
124
|
+
|
|
125
|
+
export const inline: Omit<Record<InlineRuleNames, RegExp>, "emStrong"> & {
|
|
126
|
+
["emStrong"]: Record<keyof typeof inline_emStrong, RegExp>;
|
|
127
|
+
} = {
|
|
128
|
+
escape: inline_escape,
|
|
129
|
+
autolink: inline_autolink,
|
|
130
|
+
url: inline_url,
|
|
131
|
+
tag: inline_tag,
|
|
132
|
+
link: inline_link,
|
|
133
|
+
reflink: inline_reflink,
|
|
134
|
+
nolink: inline_nolink,
|
|
135
|
+
reflinkSearch: inline_reflinkSearch,
|
|
136
|
+
anyPunctuation: inline_anyPunctuation,
|
|
137
|
+
emStrong: inline_emStrong,
|
|
138
|
+
code: /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,
|
|
139
|
+
br: /^( {2,}|\\)\n(?!\s*$)/,
|
|
140
|
+
del: inline_del,
|
|
141
|
+
text: inline_text,
|
|
142
|
+
punctuation: inline_punctuation,
|
|
143
|
+
blockSkip: inline_blockSkip,
|
|
144
|
+
escapes: inline_escapes,
|
|
145
|
+
backpedal: inline_backpedal,
|
|
146
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const caret = /(^|[^\[])\^/g;
|
|
2
|
+
|
|
3
|
+
export function edit(rule: RegExp | string, opt?: string) {
|
|
4
|
+
rule = typeof rule === "string" ? rule : rule.source;
|
|
5
|
+
opt = opt ?? "";
|
|
6
|
+
const obj = {
|
|
7
|
+
replace: (name: string | RegExp, val: string | RegExp) => {
|
|
8
|
+
val = typeof val === "object" && "source" in val ? val.source : val;
|
|
9
|
+
val = val.replace(caret, "$1");
|
|
10
|
+
rule = (rule as string).replace(name, val);
|
|
11
|
+
return obj;
|
|
12
|
+
},
|
|
13
|
+
getRegex: () => {
|
|
14
|
+
return new RegExp(rule, opt);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
return obj;
|
|
18
|
+
}
|