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/types.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
type BaseToken = {
|
|
2
|
+
type: string;
|
|
3
|
+
raw: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
8
|
+
export interface Tokens extends Record<string, BaseToken> {
|
|
9
|
+
Space: {
|
|
10
|
+
type: "space";
|
|
11
|
+
raw: string;
|
|
12
|
+
};
|
|
13
|
+
Code: {
|
|
14
|
+
type: "code";
|
|
15
|
+
raw: string;
|
|
16
|
+
text: string;
|
|
17
|
+
codeBlockStyle?: "indented";
|
|
18
|
+
lang?: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
Heading: {
|
|
21
|
+
type: "heading";
|
|
22
|
+
raw: string;
|
|
23
|
+
text: string;
|
|
24
|
+
depth: number;
|
|
25
|
+
tokens: Token[];
|
|
26
|
+
};
|
|
27
|
+
Table: {
|
|
28
|
+
type: "table";
|
|
29
|
+
raw: string;
|
|
30
|
+
align: ("center" | "left" | "right" | null)[];
|
|
31
|
+
header: Tokens["TableCell"][];
|
|
32
|
+
rows: Tokens["TableCell"][][];
|
|
33
|
+
};
|
|
34
|
+
TableCell: {
|
|
35
|
+
type: "tablecell";
|
|
36
|
+
raw: string;
|
|
37
|
+
text: string;
|
|
38
|
+
tokens: Token[];
|
|
39
|
+
};
|
|
40
|
+
Hr: {
|
|
41
|
+
type: "hr";
|
|
42
|
+
raw: string;
|
|
43
|
+
};
|
|
44
|
+
Blockquote: {
|
|
45
|
+
type: "blockquote";
|
|
46
|
+
raw: string;
|
|
47
|
+
text: string;
|
|
48
|
+
tokens: Token[];
|
|
49
|
+
};
|
|
50
|
+
List: {
|
|
51
|
+
type: "list";
|
|
52
|
+
raw: string;
|
|
53
|
+
ordered: boolean;
|
|
54
|
+
start: number | "";
|
|
55
|
+
loose: boolean;
|
|
56
|
+
items: Tokens["ListItem"][];
|
|
57
|
+
};
|
|
58
|
+
ListItem: {
|
|
59
|
+
type: "list_item";
|
|
60
|
+
raw: string;
|
|
61
|
+
text: string;
|
|
62
|
+
task: boolean;
|
|
63
|
+
checked?: boolean | undefined;
|
|
64
|
+
loose: boolean;
|
|
65
|
+
tokens: Token[];
|
|
66
|
+
};
|
|
67
|
+
Paragraph: {
|
|
68
|
+
type: "paragraph";
|
|
69
|
+
raw: string;
|
|
70
|
+
text: string;
|
|
71
|
+
pre?: boolean | undefined;
|
|
72
|
+
tokens: Token[];
|
|
73
|
+
};
|
|
74
|
+
HTML: {
|
|
75
|
+
type: "html";
|
|
76
|
+
raw: string;
|
|
77
|
+
text: string;
|
|
78
|
+
pre: boolean;
|
|
79
|
+
block: boolean;
|
|
80
|
+
};
|
|
81
|
+
Text: {
|
|
82
|
+
type: "text";
|
|
83
|
+
raw: string;
|
|
84
|
+
text: string;
|
|
85
|
+
tokens?: Token[];
|
|
86
|
+
};
|
|
87
|
+
Def: {
|
|
88
|
+
type: "def";
|
|
89
|
+
raw: string;
|
|
90
|
+
tag: string;
|
|
91
|
+
href: string;
|
|
92
|
+
title: string;
|
|
93
|
+
};
|
|
94
|
+
Escape: {
|
|
95
|
+
type: "escape";
|
|
96
|
+
raw: string;
|
|
97
|
+
text: string;
|
|
98
|
+
};
|
|
99
|
+
Tag: {
|
|
100
|
+
type: "text" | "html";
|
|
101
|
+
raw: string;
|
|
102
|
+
text: string;
|
|
103
|
+
inLink: boolean;
|
|
104
|
+
inRawBlock: boolean;
|
|
105
|
+
block: boolean;
|
|
106
|
+
};
|
|
107
|
+
Link: {
|
|
108
|
+
type: "link";
|
|
109
|
+
raw: string;
|
|
110
|
+
text: string;
|
|
111
|
+
href: string;
|
|
112
|
+
title: string | null;
|
|
113
|
+
tokens: Token[];
|
|
114
|
+
};
|
|
115
|
+
Image: {
|
|
116
|
+
type: "image";
|
|
117
|
+
raw: string;
|
|
118
|
+
text: string;
|
|
119
|
+
href: string;
|
|
120
|
+
title: string | null;
|
|
121
|
+
};
|
|
122
|
+
Strong: {
|
|
123
|
+
type: "strong";
|
|
124
|
+
raw: string;
|
|
125
|
+
text: string;
|
|
126
|
+
tokens: Token[];
|
|
127
|
+
};
|
|
128
|
+
Em: {
|
|
129
|
+
type: "em";
|
|
130
|
+
raw: string;
|
|
131
|
+
text: string;
|
|
132
|
+
tokens: Token[];
|
|
133
|
+
};
|
|
134
|
+
Codespan: {
|
|
135
|
+
type: "codespan";
|
|
136
|
+
raw: string;
|
|
137
|
+
text: string;
|
|
138
|
+
};
|
|
139
|
+
Br: {
|
|
140
|
+
type: "br";
|
|
141
|
+
raw: string;
|
|
142
|
+
};
|
|
143
|
+
Del: {
|
|
144
|
+
type: "del";
|
|
145
|
+
raw: string;
|
|
146
|
+
text: string;
|
|
147
|
+
tokens: Token[];
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type Token =
|
|
152
|
+
| Tokens["Space"]
|
|
153
|
+
| Tokens["Code"]
|
|
154
|
+
| Tokens["Heading"]
|
|
155
|
+
| Tokens["Table"]
|
|
156
|
+
| Tokens["TableCell"]
|
|
157
|
+
| Tokens["Hr"]
|
|
158
|
+
| Tokens["Blockquote"]
|
|
159
|
+
| Tokens["List"]
|
|
160
|
+
| Tokens["ListItem"]
|
|
161
|
+
| Tokens["Paragraph"]
|
|
162
|
+
| Tokens["HTML"]
|
|
163
|
+
| Tokens["Text"]
|
|
164
|
+
| Tokens["Def"]
|
|
165
|
+
| Tokens["Escape"]
|
|
166
|
+
| Tokens["Tag"]
|
|
167
|
+
| Tokens["Link"]
|
|
168
|
+
| Tokens["Image"]
|
|
169
|
+
| Tokens["Strong"]
|
|
170
|
+
| Tokens["Em"]
|
|
171
|
+
| Tokens["Codespan"]
|
|
172
|
+
| Tokens["Br"]
|
|
173
|
+
| Tokens["Del"];
|
|
174
|
+
|
|
175
|
+
export type Links = Record<string, { href: string; title: string }>;
|
|
176
|
+
|
|
177
|
+
export type RendererProps = {
|
|
178
|
+
/**
|
|
179
|
+
* url prefix. Must start and end with "/"
|
|
180
|
+
* example: "/__localimage__/"
|
|
181
|
+
*/
|
|
182
|
+
localImageUrlPrefix: string;
|
|
183
|
+
};
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { type Lexer } from "./lexer.ts";
|
|
2
|
+
import { type Tokens } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Helpers
|
|
6
|
+
*/
|
|
7
|
+
const escapeTest = /[&<>"']/;
|
|
8
|
+
const escapeReplace = new RegExp(escapeTest.source, "g");
|
|
9
|
+
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
|
|
10
|
+
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, "g");
|
|
11
|
+
const escapeReplacements: Record<string, string> = {
|
|
12
|
+
"&": "&",
|
|
13
|
+
"<": "<",
|
|
14
|
+
">": ">",
|
|
15
|
+
'"': """,
|
|
16
|
+
"'": "'",
|
|
17
|
+
};
|
|
18
|
+
const getEscapeReplacement = (ch: string) => escapeReplacements[ch]!;
|
|
19
|
+
|
|
20
|
+
export function escape(html: string, encode?: boolean) {
|
|
21
|
+
if (encode) {
|
|
22
|
+
if (escapeTest.test(html)) {
|
|
23
|
+
return html.replace(escapeReplace, getEscapeReplacement);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
if (escapeTestNoEncode.test(html)) {
|
|
27
|
+
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return html;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function fixHtmlLocalImageHref(html: string, localImageUrlPrefix: string): string {
|
|
35
|
+
return html.replace(
|
|
36
|
+
/<img\s+([^>]*?)src\s*=\s*(["'])([^\2>]+?)\2([^>]*)>/gm,
|
|
37
|
+
(_m, g1, _g2, g3: string, g4) => {
|
|
38
|
+
const href = fixLocalImageHref(g3, localImageUrlPrefix);
|
|
39
|
+
return `<img ${g1}src="${href}"${g4}>`;
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function fixLocalImageHref(href: string, localImageUrlPrefix: string): string {
|
|
45
|
+
const reIsAbsolute = /^[\w+]+:\/\//;
|
|
46
|
+
const dummyUrl = "http://__dummy__";
|
|
47
|
+
const dummyBaseUrl = new URL(localImageUrlPrefix, dummyUrl);
|
|
48
|
+
const dummyUrlLength = dummyUrl.length + (localImageUrlPrefix.startsWith("/") ? 0 : 1);
|
|
49
|
+
|
|
50
|
+
if (reIsAbsolute.test(href)) {
|
|
51
|
+
// the URL is absolute, do not touch it
|
|
52
|
+
return href;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (href.startsWith("/")) {
|
|
56
|
+
// the URL is from root
|
|
57
|
+
return href;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const temp = new URL(href, dummyBaseUrl).href;
|
|
62
|
+
return temp.slice(dummyUrlLength);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return href;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function cleanUrl(href: string) {
|
|
69
|
+
try {
|
|
70
|
+
href = encodeURI(href).replace(/%25/g, "%");
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return href;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const noopTest = { exec: () => null };
|
|
78
|
+
|
|
79
|
+
export function splitCells(tableRow: string, count?: number) {
|
|
80
|
+
// ensure that every cell-delimiting pipe has a space
|
|
81
|
+
// before it to distinguish it from an escaped pipe
|
|
82
|
+
const row = tableRow.replace(/\|/g, (_match, offset: number, str: string) => {
|
|
83
|
+
let escaped = false;
|
|
84
|
+
let curr = offset;
|
|
85
|
+
while (--curr >= 0 && str[curr] === "\\") escaped = !escaped;
|
|
86
|
+
if (escaped) {
|
|
87
|
+
// odd number of slashes means | is escaped
|
|
88
|
+
// so we leave it alone
|
|
89
|
+
return "|";
|
|
90
|
+
} else {
|
|
91
|
+
// add space before unescaped |
|
|
92
|
+
return " |";
|
|
93
|
+
}
|
|
94
|
+
}),
|
|
95
|
+
cells = row.split(/ \|/);
|
|
96
|
+
|
|
97
|
+
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
|
|
98
|
+
if (!cells[0]?.trim()) {
|
|
99
|
+
cells.shift();
|
|
100
|
+
}
|
|
101
|
+
if (cells.length > 0 && !cells[cells.length - 1]?.trim()) {
|
|
102
|
+
cells.pop();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (count) {
|
|
106
|
+
if (cells.length > count) {
|
|
107
|
+
cells.splice(count);
|
|
108
|
+
} else {
|
|
109
|
+
while (cells.length < count) cells.push("");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 0, len = cells.length; i < len; i++) {
|
|
114
|
+
// leading or trailing whitespace is ignored per the gfm spec
|
|
115
|
+
cells[i] = cells[i]!.trim().replace(/\\\|/g, "|");
|
|
116
|
+
}
|
|
117
|
+
return cells;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
|
|
122
|
+
* /c*$/ is vulnerable to REDOS.
|
|
123
|
+
*
|
|
124
|
+
* @param str
|
|
125
|
+
* @param c
|
|
126
|
+
* @param invert Remove suffix of non-c chars instead. Default falsey.
|
|
127
|
+
*/
|
|
128
|
+
export function rtrim(str: string, c: string, invert?: boolean) {
|
|
129
|
+
const l = str.length;
|
|
130
|
+
if (l === 0) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Length of suffix matching the invert condition.
|
|
135
|
+
let suffLen = 0;
|
|
136
|
+
|
|
137
|
+
// Step left until we fail to match the invert condition.
|
|
138
|
+
while (suffLen < l) {
|
|
139
|
+
const currChar = str.charAt(l - suffLen - 1);
|
|
140
|
+
if (currChar === c && !invert) {
|
|
141
|
+
suffLen++;
|
|
142
|
+
} else if (currChar !== c && invert) {
|
|
143
|
+
suffLen++;
|
|
144
|
+
} else {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return str.slice(0, l - suffLen);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function findClosingBracket(str: string, b: string) {
|
|
153
|
+
if (!b[1] || !str.includes(b[1])) {
|
|
154
|
+
return -1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let level = 0;
|
|
158
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
159
|
+
if (str[i] === "\\") {
|
|
160
|
+
i++;
|
|
161
|
+
} else if (str[i] === b[0]) {
|
|
162
|
+
level++;
|
|
163
|
+
} else if (str[i] === b[1]) {
|
|
164
|
+
level--;
|
|
165
|
+
if (level < 0) {
|
|
166
|
+
return i;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return -1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function outputLink(
|
|
174
|
+
cap: string[],
|
|
175
|
+
link: Pick<Tokens["Link"], "href" | "title">,
|
|
176
|
+
raw: string,
|
|
177
|
+
lexer: Lexer,
|
|
178
|
+
): Tokens["Link"] | Tokens["Image"] {
|
|
179
|
+
const href = link.href;
|
|
180
|
+
const title = link.title ? escape(link.title) : null;
|
|
181
|
+
const text = cap[1]?.replace(/\\([\[\]])/g, "$1") ?? "";
|
|
182
|
+
|
|
183
|
+
if (cap[0]?.charAt(0) !== "!") {
|
|
184
|
+
lexer.state.inLink = true;
|
|
185
|
+
const token: Tokens["Link"] = {
|
|
186
|
+
type: "link",
|
|
187
|
+
raw,
|
|
188
|
+
href,
|
|
189
|
+
title,
|
|
190
|
+
text,
|
|
191
|
+
tokens: lexer.inlineTokens(text),
|
|
192
|
+
};
|
|
193
|
+
lexer.state.inLink = false;
|
|
194
|
+
return token;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
type: "image",
|
|
198
|
+
raw,
|
|
199
|
+
href,
|
|
200
|
+
title,
|
|
201
|
+
text: escape(text),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function indentCodeCompensation(raw: string, text: string) {
|
|
206
|
+
const matchIndentToCode = raw.match(/^(\s+)(?:```)/);
|
|
207
|
+
|
|
208
|
+
if (matchIndentToCode === null) {
|
|
209
|
+
return text;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const indentToCode = matchIndentToCode[1];
|
|
213
|
+
|
|
214
|
+
return text
|
|
215
|
+
.split("\n")
|
|
216
|
+
.map((node) => {
|
|
217
|
+
const matchIndentInNode = node.match(/^\s+/);
|
|
218
|
+
if (matchIndentInNode === null) {
|
|
219
|
+
return node;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const [indentInNode] = matchIndentInNode;
|
|
223
|
+
|
|
224
|
+
if (indentToCode && indentInNode.length >= indentToCode.length) {
|
|
225
|
+
return node.slice(indentToCode.length);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return node;
|
|
229
|
+
})
|
|
230
|
+
.join("\n");
|
|
231
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"types": ["bun-types"],
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"composite": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"downlevelIteration": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"allowSyntheticDefaultImports": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
|
|
18
|
+
// strict: https://github.com/tsconfig/bases/blob/main/bases/strictest.json
|
|
19
|
+
"allowUnusedLabels": false,
|
|
20
|
+
"allowUnreachableCode": false,
|
|
21
|
+
"exactOptionalPropertyTypes": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
"noImplicitReturns": true,
|
|
25
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
"noUnusedLocals": true,
|
|
28
|
+
"noUnusedParameters": true,
|
|
29
|
+
"isolatedModules": true,
|
|
30
|
+
"checkJs": true,
|
|
31
|
+
"esModuleInterop": true
|
|
32
|
+
}
|
|
33
|
+
}
|