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/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
+ "&": "&amp;",
13
+ "<": "&lt;",
14
+ ">": "&gt;",
15
+ '"': "&quot;",
16
+ "'": "&#39;",
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
+ }