pantsdown 1.0.1 → 1.1.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/.github/workflows/release.yaml +1 -0
- package/package.json +7 -6
- package/src/lexer.ts +18 -1
- package/src/parser.ts +26 -14
- package/src/renderer.ts +45 -20
- package/src/sample.test.ts +1 -1
- package/src/tokenizer.ts +17 -1
- package/src/types.ts +13 -0
- package/src/utils.ts +27 -1
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantsdown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"author": "wallpants",
|
|
4
5
|
"description": "The markdown parser",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
5
8
|
"repository": {
|
|
6
9
|
"type": "git",
|
|
7
10
|
"url": "git://github.com/wallpants/pantsdown.git"
|
|
8
11
|
},
|
|
9
|
-
"license": "MIT",
|
|
10
|
-
"type": "module",
|
|
11
12
|
"exports": {
|
|
12
13
|
"types": "./src/index.ts",
|
|
13
14
|
"bun": "./src/index.ts",
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
"main"
|
|
29
30
|
]
|
|
30
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"highlight.js": "^11.9.0"
|
|
34
|
+
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
|
33
37
|
"@typescript-eslint/parser": "^6.7.3",
|
|
@@ -39,8 +43,5 @@
|
|
|
39
43
|
"prettier-plugin-organize-imports": "^3.2.3",
|
|
40
44
|
"semantic-release": "^22.0.5",
|
|
41
45
|
"typescript": "5.2.2"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"highlight.js": "^11.9.0"
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/lexer.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { inline } from "./rules/inline.ts";
|
|
2
2
|
import { Tokenizer } from "./tokenizer.ts";
|
|
3
|
-
import { type Links, type Token } from "./types.ts";
|
|
3
|
+
import { type Links, type SourceMap, type Token } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export class Lexer {
|
|
6
6
|
private tokenizer: Tokenizer;
|
|
7
7
|
private inlineQueue: { src: string; tokens: Token[] }[];
|
|
8
8
|
private tokens: Token[] = [];
|
|
9
9
|
private links: Links = {};
|
|
10
|
+
private line = 1;
|
|
10
11
|
|
|
11
12
|
state = {
|
|
12
13
|
inLink: false,
|
|
@@ -26,6 +27,7 @@ export class Lexer {
|
|
|
26
27
|
// reset values from previous parse
|
|
27
28
|
this.tokens = [];
|
|
28
29
|
this.links = {};
|
|
30
|
+
this.line = 1;
|
|
29
31
|
|
|
30
32
|
src = src.replace(/\r\n|\r/g, "\n");
|
|
31
33
|
|
|
@@ -39,6 +41,21 @@ export class Lexer {
|
|
|
39
41
|
return this.tokens;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
getSourceMap(rawToken: string): SourceMap | undefined {
|
|
45
|
+
if (!this.state.top) return;
|
|
46
|
+
const tokenLines = rawToken.split("\n");
|
|
47
|
+
const sourceMap: SourceMap = [this.line, (this.line += tokenLines.length - 1)];
|
|
48
|
+
|
|
49
|
+
while (tokenLines.slice(-1)[0] === "") {
|
|
50
|
+
// token.raw sometimes includes newline chars at the end of the string
|
|
51
|
+
// which we don't want to include in the sourceMap
|
|
52
|
+
tokenLines.pop();
|
|
53
|
+
sourceMap[1]--;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return sourceMap;
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
/**
|
|
43
60
|
* Lexing
|
|
44
61
|
*/
|
package/src/parser.ts
CHANGED
|
@@ -25,15 +25,19 @@ export class Parser {
|
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
27
|
case "hr": {
|
|
28
|
-
out += this.renderer.hr();
|
|
28
|
+
out += this.renderer.hr(token.sourceMap);
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
case "heading": {
|
|
32
|
-
out += this.renderer.heading(
|
|
32
|
+
out += this.renderer.heading(
|
|
33
|
+
this.parseInline(token.tokens),
|
|
34
|
+
token.depth,
|
|
35
|
+
token.sourceMap,
|
|
36
|
+
);
|
|
33
37
|
continue;
|
|
34
38
|
}
|
|
35
39
|
case "code": {
|
|
36
|
-
out += this.renderer.code(token.text, token.lang);
|
|
40
|
+
out += this.renderer.code(token.text, token.lang, token.sourceMap);
|
|
37
41
|
continue;
|
|
38
42
|
}
|
|
39
43
|
case "table": {
|
|
@@ -47,7 +51,8 @@ export class Parser {
|
|
|
47
51
|
align: token.align[j]!,
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
|
-
|
|
54
|
+
const sourceMapLineStart = token.sourceMap?.[0];
|
|
55
|
+
header += this.renderer.tablerow(cell, sourceMapLineStart);
|
|
51
56
|
|
|
52
57
|
let body = "";
|
|
53
58
|
for (let j = 0, rowsLen = token.rows.length; j < rowsLen; j++) {
|
|
@@ -61,14 +66,17 @@ export class Parser {
|
|
|
61
66
|
});
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
body += this.renderer.tablerow(
|
|
69
|
+
body += this.renderer.tablerow(
|
|
70
|
+
cell,
|
|
71
|
+
sourceMapLineStart ? sourceMapLineStart + 2 + j : undefined,
|
|
72
|
+
);
|
|
65
73
|
}
|
|
66
|
-
out += this.renderer.table(header, body);
|
|
74
|
+
out += this.renderer.table(header, body, token.sourceMap);
|
|
67
75
|
continue;
|
|
68
76
|
}
|
|
69
77
|
case "blockquote": {
|
|
70
78
|
const body = this.parse(token.tokens);
|
|
71
|
-
out += this.renderer.blockquote(body);
|
|
79
|
+
out += this.renderer.blockquote(body, token.sourceMap);
|
|
72
80
|
continue;
|
|
73
81
|
}
|
|
74
82
|
case "list": {
|
|
@@ -84,7 +92,7 @@ export class Parser {
|
|
|
84
92
|
|
|
85
93
|
let itemBody = "";
|
|
86
94
|
if (item.task) {
|
|
87
|
-
const checkbox = this.renderer.checkbox(
|
|
95
|
+
const checkbox = this.renderer.checkbox(Boolean(checked));
|
|
88
96
|
if (loose) {
|
|
89
97
|
if (
|
|
90
98
|
item.tokens.length > 0 &&
|
|
@@ -110,18 +118,22 @@ export class Parser {
|
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
itemBody += this.parse(item.tokens, loose);
|
|
113
|
-
body += this.renderer.listitem(itemBody, task,
|
|
121
|
+
body += this.renderer.listitem(itemBody, task, Boolean(checked));
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
out += this.renderer.list(body, ordered, start);
|
|
124
|
+
out += this.renderer.list(body, ordered, start, token.sourceMap);
|
|
117
125
|
continue;
|
|
118
126
|
}
|
|
119
127
|
case "html": {
|
|
120
|
-
out += this.renderer.html(
|
|
128
|
+
out += this.renderer.html(
|
|
129
|
+
token.text,
|
|
130
|
+
token.block,
|
|
131
|
+
"sourceMap" in token ? token.sourceMap : undefined,
|
|
132
|
+
);
|
|
121
133
|
continue;
|
|
122
134
|
}
|
|
123
135
|
case "paragraph": {
|
|
124
|
-
out += this.renderer.paragraph(this.parseInline(token.tokens));
|
|
136
|
+
out += this.renderer.paragraph(this.parseInline(token.tokens), token.sourceMap);
|
|
125
137
|
continue;
|
|
126
138
|
}
|
|
127
139
|
case "text": {
|
|
@@ -137,7 +149,7 @@ export class Parser {
|
|
|
137
149
|
? this.parseInline(textToken.tokens)
|
|
138
150
|
: textToken.text);
|
|
139
151
|
}
|
|
140
|
-
out += top ? this.renderer.paragraph(body) : body;
|
|
152
|
+
out += top ? this.renderer.paragraph(body, textToken.sourceMap) : body;
|
|
141
153
|
continue;
|
|
142
154
|
}
|
|
143
155
|
|
|
@@ -166,7 +178,7 @@ export class Parser {
|
|
|
166
178
|
break;
|
|
167
179
|
}
|
|
168
180
|
case "html": {
|
|
169
|
-
out += this.renderer.html(token.text);
|
|
181
|
+
out += this.renderer.html(token.text, false);
|
|
170
182
|
break;
|
|
171
183
|
}
|
|
172
184
|
case "link": {
|
package/src/renderer.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import hljs from "highlight.js";
|
|
2
|
-
import { type RendererProps } from "./types.ts";
|
|
3
|
-
import {
|
|
2
|
+
import { type RendererProps, type SourceMap } from "./types.ts";
|
|
3
|
+
import {
|
|
4
|
+
cleanUrl,
|
|
5
|
+
escape,
|
|
6
|
+
fixHtmlLocalImageHref,
|
|
7
|
+
fixLocalImageHref,
|
|
8
|
+
injectHtmlAttributes,
|
|
9
|
+
renderSourceMap,
|
|
10
|
+
} from "./utils.ts";
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* An object containing functions to render tokens to HTML.
|
|
@@ -12,7 +19,7 @@ export class Renderer {
|
|
|
12
19
|
this.props = props;
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
code(code: string, infostring: string | undefined): string {
|
|
22
|
+
code(code: string, infostring: string | undefined, sourceMap: SourceMap): string {
|
|
16
23
|
const lang = (infostring ?? "").match(/^\S*/)?.[0];
|
|
17
24
|
|
|
18
25
|
code = code.replace(/\n$/, "") + "\n";
|
|
@@ -21,7 +28,7 @@ export class Renderer {
|
|
|
21
28
|
const highlightedCode = hljs.highlight(code, { language }).value;
|
|
22
29
|
|
|
23
30
|
return (
|
|
24
|
-
|
|
31
|
+
`<pre${renderSourceMap(sourceMap)}><code class="hljs language-` +
|
|
25
32
|
escape(language) +
|
|
26
33
|
'">' +
|
|
27
34
|
highlightedCode +
|
|
@@ -29,27 +36,36 @@ export class Renderer {
|
|
|
29
36
|
);
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
blockquote(quote: string): string {
|
|
33
|
-
return `<blockquote>\n${quote}</blockquote>\n`;
|
|
39
|
+
blockquote(quote: string, sourceMap: SourceMap): string {
|
|
40
|
+
return `<blockquote${renderSourceMap(sourceMap)}>\n${quote}</blockquote>\n`;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
html(html: string, _block?:
|
|
37
|
-
|
|
43
|
+
html(html: string, _block: boolean, sourceMap?: SourceMap | undefined): string {
|
|
44
|
+
let result = fixHtmlLocalImageHref(html, this.props.localImageUrlPrefix);
|
|
45
|
+
if (sourceMap) {
|
|
46
|
+
result = injectHtmlAttributes(result, [
|
|
47
|
+
["line-start", sourceMap[0]],
|
|
48
|
+
["line-end", sourceMap[1]],
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
heading(text: string, level: number): string {
|
|
54
|
+
heading(text: string, level: number, sourceMap: SourceMap): string {
|
|
41
55
|
// ignore IDs
|
|
42
|
-
return `<h${level}>${text}</h${level}>\n`;
|
|
56
|
+
return `<h${level}${renderSourceMap(sourceMap)}>${text}</h${level}>\n`;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
hr(): string {
|
|
46
|
-
return
|
|
59
|
+
hr(sourceMap: SourceMap): string {
|
|
60
|
+
return `<hr${renderSourceMap(sourceMap)}>\n`;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
list(body: string, ordered: boolean, start: number | ""): string {
|
|
63
|
+
list(body: string, ordered: boolean, start: number | "", sourceMap: SourceMap): string {
|
|
50
64
|
const type = ordered ? "ol" : "ul";
|
|
51
65
|
const startatt = ordered && start !== 1 ? ' start="' + start + '"' : "";
|
|
52
|
-
return
|
|
66
|
+
return (
|
|
67
|
+
"<" + type + startatt + `${renderSourceMap(sourceMap)}>\n` + body + "</" + type + ">\n"
|
|
68
|
+
);
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
listitem(text: string, _task: boolean, _checked: boolean): string {
|
|
@@ -60,18 +76,27 @@ export class Renderer {
|
|
|
60
76
|
return "<input " + (checked ? 'checked="" ' : "") + 'disabled="" type="checkbox">';
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
paragraph(text: string): string {
|
|
64
|
-
return `<p>${text}</p>\n`;
|
|
79
|
+
paragraph(text: string, sourceMap: SourceMap): string {
|
|
80
|
+
return `<p${renderSourceMap(sourceMap)}>${text}</p>\n`;
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
table(header: string, body: string): string {
|
|
83
|
+
table(header: string, body: string, sourceMap: SourceMap): string {
|
|
68
84
|
if (body) body = `<tbody>${body}</tbody>`;
|
|
69
85
|
|
|
70
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
`<table${renderSourceMap(sourceMap)}>\n` +
|
|
88
|
+
"<thead>\n" +
|
|
89
|
+
header +
|
|
90
|
+
"</thead>\n" +
|
|
91
|
+
body +
|
|
92
|
+
"</table>\n"
|
|
93
|
+
);
|
|
71
94
|
}
|
|
72
95
|
|
|
73
|
-
tablerow(content: string): string {
|
|
74
|
-
return `<tr
|
|
96
|
+
tablerow(content: string, sourceMapStart: number | undefined): string {
|
|
97
|
+
return `<tr${renderSourceMap(
|
|
98
|
+
sourceMapStart ? [sourceMapStart, sourceMapStart] : undefined,
|
|
99
|
+
)}>\n${content}</tr>\n`;
|
|
75
100
|
}
|
|
76
101
|
|
|
77
102
|
tablecell(
|
package/src/sample.test.ts
CHANGED
package/src/tokenizer.ts
CHANGED
|
@@ -24,6 +24,8 @@ export class Tokenizer {
|
|
|
24
24
|
space(src: string): Tokens["Space"] | undefined {
|
|
25
25
|
const cap = block.newline.exec(src);
|
|
26
26
|
if (cap && cap[0].length > 0) {
|
|
27
|
+
// call getSourceMap to increment "this.lexer.line"
|
|
28
|
+
this.lexer.getSourceMap(cap[0]);
|
|
27
29
|
return {
|
|
28
30
|
type: "space",
|
|
29
31
|
raw: cap[0],
|
|
@@ -42,6 +44,7 @@ export class Tokenizer {
|
|
|
42
44
|
raw: cap[0],
|
|
43
45
|
codeBlockStyle: "indented",
|
|
44
46
|
text: rtrim(text, "\n"),
|
|
47
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
45
48
|
};
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -57,6 +60,7 @@ export class Tokenizer {
|
|
|
57
60
|
raw,
|
|
58
61
|
lang: cap[2] ? cap[2].trim().replace(inline.escapes, "$1") : cap[2],
|
|
59
62
|
text,
|
|
63
|
+
sourceMap: this.lexer.getSourceMap(raw),
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -80,6 +84,7 @@ export class Tokenizer {
|
|
|
80
84
|
depth: cap[1]!.length,
|
|
81
85
|
text,
|
|
82
86
|
tokens: this.lexer.inline(text),
|
|
87
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
83
88
|
};
|
|
84
89
|
}
|
|
85
90
|
|
|
@@ -90,6 +95,7 @@ export class Tokenizer {
|
|
|
90
95
|
return {
|
|
91
96
|
type: "hr",
|
|
92
97
|
raw: cap[0],
|
|
98
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -97,7 +103,7 @@ export class Tokenizer {
|
|
|
97
103
|
const cap = block.blockquote.exec(src);
|
|
98
104
|
if (!cap) return undefined;
|
|
99
105
|
|
|
100
|
-
const text = cap[0].replace(/^ *>[ \t]?/gm, "");
|
|
106
|
+
const text = rtrim(cap[0].replace(/^ *>[ \t]?/gm, ""), "\n");
|
|
101
107
|
const top = this.lexer.state.top;
|
|
102
108
|
this.lexer.state.top = true;
|
|
103
109
|
const tokens = this.lexer.blockTokens(text, []);
|
|
@@ -107,6 +113,7 @@ export class Tokenizer {
|
|
|
107
113
|
raw: cap[0],
|
|
108
114
|
tokens,
|
|
109
115
|
text,
|
|
116
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
|
|
@@ -124,6 +131,7 @@ export class Tokenizer {
|
|
|
124
131
|
start: isordered ? +bull.slice(0, -1) : "",
|
|
125
132
|
loose: false,
|
|
126
133
|
items: [] as Tokens["ListItem"][],
|
|
134
|
+
sourceMap: [0, 0], // updated below once token.raw is built
|
|
127
135
|
};
|
|
128
136
|
|
|
129
137
|
bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`;
|
|
@@ -285,6 +293,8 @@ export class Tokenizer {
|
|
|
285
293
|
list.items[list.items.length - 1]!.text = itemContents.trimEnd();
|
|
286
294
|
list.raw = list.raw.trimEnd();
|
|
287
295
|
|
|
296
|
+
list.sourceMap = this.lexer.getSourceMap(list.raw);
|
|
297
|
+
|
|
288
298
|
// Item child tokens handled here at end because we needed to have the final item to trim it first
|
|
289
299
|
for (let i = 0, listItemsLen = list.items.length; i < listItemsLen; i++) {
|
|
290
300
|
this.lexer.state.top = false;
|
|
@@ -321,6 +331,7 @@ export class Tokenizer {
|
|
|
321
331
|
raw: cap[0],
|
|
322
332
|
pre: cap[1] === "pre" || cap[1] === "script" || cap[1] === "style",
|
|
323
333
|
text: cap[0],
|
|
334
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
324
335
|
};
|
|
325
336
|
return token;
|
|
326
337
|
}
|
|
@@ -340,6 +351,7 @@ export class Tokenizer {
|
|
|
340
351
|
raw: cap[0],
|
|
341
352
|
href,
|
|
342
353
|
title,
|
|
354
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
343
355
|
};
|
|
344
356
|
}
|
|
345
357
|
|
|
@@ -363,6 +375,7 @@ export class Tokenizer {
|
|
|
363
375
|
})),
|
|
364
376
|
align: [],
|
|
365
377
|
rows: [],
|
|
378
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
366
379
|
};
|
|
367
380
|
|
|
368
381
|
const align = cap[2].replace(/^\||\| *$/g, "").split("|") as (string | null)[];
|
|
@@ -429,6 +442,7 @@ export class Tokenizer {
|
|
|
429
442
|
depth: cap[2]!.startsWith("=") ? 1 : 2,
|
|
430
443
|
text: cap[1]!,
|
|
431
444
|
tokens: this.lexer.inline(cap[1]!),
|
|
445
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
432
446
|
};
|
|
433
447
|
}
|
|
434
448
|
|
|
@@ -442,6 +456,7 @@ export class Tokenizer {
|
|
|
442
456
|
raw: cap[0],
|
|
443
457
|
text,
|
|
444
458
|
tokens: this.lexer.inline(text),
|
|
459
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
445
460
|
};
|
|
446
461
|
}
|
|
447
462
|
|
|
@@ -454,6 +469,7 @@ export class Tokenizer {
|
|
|
454
469
|
raw: cap[0],
|
|
455
470
|
text: cap[0],
|
|
456
471
|
tokens: this.lexer.inline(cap[0]),
|
|
472
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
457
473
|
};
|
|
458
474
|
}
|
|
459
475
|
|
package/src/types.ts
CHANGED
|
@@ -2,8 +2,11 @@ type BaseToken = {
|
|
|
2
2
|
type: string;
|
|
3
3
|
raw: string;
|
|
4
4
|
text?: string;
|
|
5
|
+
sourceMap?: SourceMap;
|
|
5
6
|
};
|
|
6
7
|
|
|
8
|
+
export type SourceMap = [start: number, end: number] | undefined;
|
|
9
|
+
|
|
7
10
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
8
11
|
export interface Tokens extends Record<string, BaseToken> {
|
|
9
12
|
Space: {
|
|
@@ -16,6 +19,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
16
19
|
text: string;
|
|
17
20
|
codeBlockStyle?: "indented";
|
|
18
21
|
lang?: string | undefined;
|
|
22
|
+
sourceMap: SourceMap;
|
|
19
23
|
};
|
|
20
24
|
Heading: {
|
|
21
25
|
type: "heading";
|
|
@@ -23,6 +27,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
23
27
|
text: string;
|
|
24
28
|
depth: number;
|
|
25
29
|
tokens: Token[];
|
|
30
|
+
sourceMap: SourceMap;
|
|
26
31
|
};
|
|
27
32
|
Table: {
|
|
28
33
|
type: "table";
|
|
@@ -30,6 +35,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
30
35
|
align: ("center" | "left" | "right" | null)[];
|
|
31
36
|
header: Tokens["TableCell"][];
|
|
32
37
|
rows: Tokens["TableCell"][][];
|
|
38
|
+
sourceMap: SourceMap;
|
|
33
39
|
};
|
|
34
40
|
TableCell: {
|
|
35
41
|
type: "tablecell";
|
|
@@ -40,12 +46,14 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
40
46
|
Hr: {
|
|
41
47
|
type: "hr";
|
|
42
48
|
raw: string;
|
|
49
|
+
sourceMap: SourceMap;
|
|
43
50
|
};
|
|
44
51
|
Blockquote: {
|
|
45
52
|
type: "blockquote";
|
|
46
53
|
raw: string;
|
|
47
54
|
text: string;
|
|
48
55
|
tokens: Token[];
|
|
56
|
+
sourceMap: SourceMap;
|
|
49
57
|
};
|
|
50
58
|
List: {
|
|
51
59
|
type: "list";
|
|
@@ -54,6 +62,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
54
62
|
start: number | "";
|
|
55
63
|
loose: boolean;
|
|
56
64
|
items: Tokens["ListItem"][];
|
|
65
|
+
sourceMap: SourceMap;
|
|
57
66
|
};
|
|
58
67
|
ListItem: {
|
|
59
68
|
type: "list_item";
|
|
@@ -70,6 +79,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
70
79
|
text: string;
|
|
71
80
|
pre?: boolean | undefined;
|
|
72
81
|
tokens: Token[];
|
|
82
|
+
sourceMap: SourceMap;
|
|
73
83
|
};
|
|
74
84
|
HTML: {
|
|
75
85
|
type: "html";
|
|
@@ -77,12 +87,14 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
77
87
|
text: string;
|
|
78
88
|
pre: boolean;
|
|
79
89
|
block: boolean;
|
|
90
|
+
sourceMap: SourceMap;
|
|
80
91
|
};
|
|
81
92
|
Text: {
|
|
82
93
|
type: "text";
|
|
83
94
|
raw: string;
|
|
84
95
|
text: string;
|
|
85
96
|
tokens?: Token[];
|
|
97
|
+
sourceMap?: SourceMap;
|
|
86
98
|
};
|
|
87
99
|
Def: {
|
|
88
100
|
type: "def";
|
|
@@ -90,6 +102,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
90
102
|
tag: string;
|
|
91
103
|
href: string;
|
|
92
104
|
title: string;
|
|
105
|
+
sourceMap: SourceMap;
|
|
93
106
|
};
|
|
94
107
|
Escape: {
|
|
95
108
|
type: "escape";
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Lexer } from "./lexer.ts";
|
|
2
|
-
import { type Tokens } from "./types.ts";
|
|
2
|
+
import { type SourceMap, type Tokens } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Helpers
|
|
@@ -31,6 +31,27 @@ export function escape(html: string, encode?: boolean) {
|
|
|
31
31
|
return html;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function injectHtmlAttributes(
|
|
35
|
+
html: string,
|
|
36
|
+
attrs: [name: string, value: string | number][],
|
|
37
|
+
) {
|
|
38
|
+
const parser = new DOMParser();
|
|
39
|
+
try {
|
|
40
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
41
|
+
// eslint-disable-next-line
|
|
42
|
+
if (!doc.body) throw Error("Invalid HTML");
|
|
43
|
+
const element = doc.body.firstChild as HTMLElement;
|
|
44
|
+
// eslint-disable-next-line
|
|
45
|
+
if (!element) throw Error("No valid element found");
|
|
46
|
+
attrs.forEach(([name, value]) => {
|
|
47
|
+
element.setAttribute(name, JSON.stringify(value));
|
|
48
|
+
});
|
|
49
|
+
return element.outerHTML;
|
|
50
|
+
} catch (_) {
|
|
51
|
+
return html;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
export function fixHtmlLocalImageHref(html: string, localImageUrlPrefix: string): string {
|
|
35
56
|
return html.replace(
|
|
36
57
|
/<img\s+([^>]*?)src\s*=\s*(["'])([^\2>]+?)\2([^>]*)>/gm,
|
|
@@ -65,6 +86,11 @@ export function fixLocalImageHref(href: string, localImageUrlPrefix: string): st
|
|
|
65
86
|
}
|
|
66
87
|
}
|
|
67
88
|
|
|
89
|
+
export function renderSourceMap(sourceMap: SourceMap) {
|
|
90
|
+
if (!sourceMap) return "";
|
|
91
|
+
return ` line-start=${sourceMap[0]} line-end=${sourceMap[1]}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
68
94
|
export function cleanUrl(href: string) {
|
|
69
95
|
try {
|
|
70
96
|
href = encodeURI(href).replace(/%25/g, "%");
|