pantsdown 2.2.7 → 2.2.8
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/README.md +2 -0
- package/package.json +5 -3
- package/src/lexer.ts +42 -51
- package/src/parser.ts +51 -125
- package/src/renderer.ts +130 -62
- package/src/rules/block.ts +41 -34
- package/src/rules/inline.ts +93 -29
- package/src/rules/other.ts +79 -0
- package/src/rules/utils.ts +2 -2
- package/src/text-renderer.ts +57 -0
- package/src/tokenizer.ts +347 -165
- package/src/types.ts +11 -1
- package/src/utils.ts +56 -34
package/src/types.ts
CHANGED
|
@@ -52,6 +52,8 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
52
52
|
raw: string;
|
|
53
53
|
text: string;
|
|
54
54
|
tokens: Token[];
|
|
55
|
+
header: boolean;
|
|
56
|
+
align: "center" | "left" | "right" | null;
|
|
55
57
|
};
|
|
56
58
|
Hr: {
|
|
57
59
|
type: "hr";
|
|
@@ -103,6 +105,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
103
105
|
raw: string;
|
|
104
106
|
text: string;
|
|
105
107
|
tokens?: Token[];
|
|
108
|
+
escaped?: boolean;
|
|
106
109
|
sourceMap?: SourceMap;
|
|
107
110
|
};
|
|
108
111
|
Def: {
|
|
@@ -119,13 +122,18 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
119
122
|
text: string;
|
|
120
123
|
};
|
|
121
124
|
Tag: {
|
|
122
|
-
type: "
|
|
125
|
+
type: "html";
|
|
123
126
|
raw: string;
|
|
124
127
|
text: string;
|
|
125
128
|
inLink: boolean;
|
|
126
129
|
inRawBlock: boolean;
|
|
127
130
|
block: boolean;
|
|
128
131
|
};
|
|
132
|
+
Checkbox: {
|
|
133
|
+
type: "checkbox";
|
|
134
|
+
raw: string;
|
|
135
|
+
checked: boolean;
|
|
136
|
+
};
|
|
129
137
|
Link: {
|
|
130
138
|
type: "link";
|
|
131
139
|
raw: string;
|
|
@@ -140,6 +148,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
140
148
|
text: string;
|
|
141
149
|
href: string;
|
|
142
150
|
title: string | null;
|
|
151
|
+
tokens: Token[];
|
|
143
152
|
};
|
|
144
153
|
Strong: {
|
|
145
154
|
type: "strong";
|
|
@@ -222,6 +231,7 @@ export type Token =
|
|
|
222
231
|
| Tokens["Codespan"]
|
|
223
232
|
| Tokens["Br"]
|
|
224
233
|
| Tokens["Del"]
|
|
234
|
+
| Tokens["Checkbox"]
|
|
225
235
|
| Tokens["Alert"]
|
|
226
236
|
| Tokens["Footnote"]
|
|
227
237
|
| Tokens["FootnoteRef"]
|
package/src/utils.ts
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { type Lexer } from "./lexer.ts";
|
|
2
|
-
import {
|
|
2
|
+
import { other } from "./rules/other.ts";
|
|
3
|
+
import { type HTMLAttrs, type PantsdownConfig, type SourceMap, type Tokens } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Helpers
|
|
6
7
|
*/
|
|
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
8
|
const escapeReplacements: Record<string, string> = {
|
|
12
9
|
"&": "&",
|
|
13
10
|
"<": "<",
|
|
@@ -21,12 +18,12 @@ const getEscapeReplacement = (ch: string) => escapeReplacements[ch]!;
|
|
|
21
18
|
// https://bun.sh/docs/api/utils#bun-escapehtml
|
|
22
19
|
export function escape(html: string, encode?: boolean) {
|
|
23
20
|
if (encode) {
|
|
24
|
-
if (escapeTest.test(html)) {
|
|
25
|
-
return html.replace(escapeReplace, getEscapeReplacement);
|
|
21
|
+
if (other.escapeTest.test(html)) {
|
|
22
|
+
return html.replace(other.escapeReplace, getEscapeReplacement);
|
|
26
23
|
}
|
|
27
24
|
} else {
|
|
28
|
-
if (escapeTestNoEncode.test(html)) {
|
|
29
|
-
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
|
25
|
+
if (other.escapeTestNoEncode.test(html)) {
|
|
26
|
+
return html.replace(other.escapeReplaceNoEncode, getEscapeReplacement);
|
|
30
27
|
}
|
|
31
28
|
}
|
|
32
29
|
|
|
@@ -150,7 +147,7 @@ export function fixLocalImageHref(href: string, config: PantsdownConfig): string
|
|
|
150
147
|
|
|
151
148
|
export function cleanUrl(href: string) {
|
|
152
149
|
try {
|
|
153
|
-
href = encodeURI(href).replace(
|
|
150
|
+
href = encodeURI(href).replace(other.percentDecode, "%");
|
|
154
151
|
} catch (_) {
|
|
155
152
|
return null;
|
|
156
153
|
}
|
|
@@ -162,7 +159,7 @@ export const noopTest = { exec: () => null };
|
|
|
162
159
|
export function splitCells(tableRow: string, count?: number) {
|
|
163
160
|
// ensure that every cell-delimiting pipe has a space
|
|
164
161
|
// before it to distinguish it from an escaped pipe
|
|
165
|
-
const row = tableRow.replace(
|
|
162
|
+
const row = tableRow.replace(other.findPipe, (_match, offset: number, str: string) => {
|
|
166
163
|
let escaped = false;
|
|
167
164
|
let curr = offset;
|
|
168
165
|
while (--curr >= 0 && str[curr] === "\\") escaped = !escaped;
|
|
@@ -175,7 +172,7 @@ export function splitCells(tableRow: string, count?: number) {
|
|
|
175
172
|
return " |";
|
|
176
173
|
}
|
|
177
174
|
}),
|
|
178
|
-
cells = row.split(
|
|
175
|
+
cells = row.split(other.splitPipe);
|
|
179
176
|
|
|
180
177
|
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
|
|
181
178
|
if (!cells[0]?.trim()) {
|
|
@@ -195,7 +192,7 @@ export function splitCells(tableRow: string, count?: number) {
|
|
|
195
192
|
|
|
196
193
|
for (let i = 0, len = cells.length; i < len; i++) {
|
|
197
194
|
// leading or trailing whitespace is ignored per the gfm spec
|
|
198
|
-
cells[i] = cells[i]!.trim().replace(
|
|
195
|
+
cells[i] = cells[i]!.trim().replace(other.slashPipe, "|");
|
|
199
196
|
}
|
|
200
197
|
return cells;
|
|
201
198
|
}
|
|
@@ -232,6 +229,20 @@ export function rtrim(str: string, c: string, invert?: boolean) {
|
|
|
232
229
|
return str.slice(0, l - suffLen);
|
|
233
230
|
}
|
|
234
231
|
|
|
232
|
+
export function trimTrailingBlankLines(str: string) {
|
|
233
|
+
const lines = str.split("\n");
|
|
234
|
+
let end = lines.length - 1;
|
|
235
|
+
while (end >= 0 && other.blankLine.test(lines[end]!)) {
|
|
236
|
+
end--;
|
|
237
|
+
}
|
|
238
|
+
if (lines.length - end <= 2) {
|
|
239
|
+
// we want to keep single trailing blank lines
|
|
240
|
+
return str;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return lines.slice(0, end + 1).join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
235
246
|
export function findClosingBracket(str: string, b: string) {
|
|
236
247
|
if (!b[1] || !str.includes(b[1])) {
|
|
237
248
|
return -1;
|
|
@@ -250,6 +261,9 @@ export function findClosingBracket(str: string, b: string) {
|
|
|
250
261
|
}
|
|
251
262
|
}
|
|
252
263
|
}
|
|
264
|
+
if (level > 0) {
|
|
265
|
+
return -2;
|
|
266
|
+
}
|
|
253
267
|
return -1;
|
|
254
268
|
}
|
|
255
269
|
|
|
@@ -260,33 +274,24 @@ export function outputLink(
|
|
|
260
274
|
lexer: Lexer,
|
|
261
275
|
): Tokens["Link"] | Tokens["Image"] {
|
|
262
276
|
const href = link.href;
|
|
263
|
-
const title = link.title
|
|
264
|
-
const text = cap[1]?.replace(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
type: "link",
|
|
270
|
-
raw,
|
|
271
|
-
href,
|
|
272
|
-
title,
|
|
273
|
-
text,
|
|
274
|
-
tokens: lexer.inlineTokens(text),
|
|
275
|
-
};
|
|
276
|
-
lexer.state.inLink = false;
|
|
277
|
-
return token;
|
|
278
|
-
}
|
|
279
|
-
return {
|
|
280
|
-
type: "image",
|
|
277
|
+
const title = link.title || null;
|
|
278
|
+
const text = cap[1]?.replace(other.outputLinkReplace, "$1") ?? "";
|
|
279
|
+
|
|
280
|
+
lexer.state.inLink = true;
|
|
281
|
+
const token: Tokens["Link"] | Tokens["Image"] = {
|
|
282
|
+
type: cap[0]?.charAt(0) === "!" ? "image" : "link",
|
|
281
283
|
raw,
|
|
282
284
|
href,
|
|
283
285
|
title,
|
|
284
|
-
text
|
|
286
|
+
text,
|
|
287
|
+
tokens: lexer.inlineTokens(text),
|
|
285
288
|
};
|
|
289
|
+
lexer.state.inLink = false;
|
|
290
|
+
return token;
|
|
286
291
|
}
|
|
287
292
|
|
|
288
293
|
export function indentCodeCompensation(raw: string, text: string) {
|
|
289
|
-
const matchIndentToCode =
|
|
294
|
+
const matchIndentToCode = other.indentCodeCompensation.exec(raw);
|
|
290
295
|
|
|
291
296
|
if (matchIndentToCode === null) {
|
|
292
297
|
return text;
|
|
@@ -297,7 +302,7 @@ export function indentCodeCompensation(raw: string, text: string) {
|
|
|
297
302
|
return text
|
|
298
303
|
.split("\n")
|
|
299
304
|
.map((node) => {
|
|
300
|
-
const matchIndentInNode =
|
|
305
|
+
const matchIndentInNode = other.beginningSpace.exec(node);
|
|
301
306
|
if (matchIndentInNode === null) {
|
|
302
307
|
return node;
|
|
303
308
|
}
|
|
@@ -344,3 +349,20 @@ export const ALERTS = [
|
|
|
344
349
|
icon: '<svg class="octicon octicon-alert" style="margin-right: 0.5rem;" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>',
|
|
345
350
|
},
|
|
346
351
|
];
|
|
352
|
+
|
|
353
|
+
export function expandTabs(line: string, indent = 0) {
|
|
354
|
+
let col = indent;
|
|
355
|
+
let expanded = "";
|
|
356
|
+
for (const char of line) {
|
|
357
|
+
if (char === "\t") {
|
|
358
|
+
const added = 4 - (col % 4);
|
|
359
|
+
expanded += " ".repeat(added);
|
|
360
|
+
col += added;
|
|
361
|
+
} else {
|
|
362
|
+
expanded += char;
|
|
363
|
+
col++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return expanded;
|
|
368
|
+
}
|