pantsdown 2.2.7 → 2.3.0
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 +38 -2
- package/package.json +10 -8
- package/src/css/styles.css +79 -0
- package/src/javascript.ts +356 -1
- package/src/lexer.ts +42 -51
- package/src/pantsdown.ts +22 -3
- package/src/parser.ts +51 -125
- package/src/renderer.ts +139 -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 +45 -1
- package/src/utils.ts +65 -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"]
|
|
@@ -275,6 +285,40 @@ export type PantsdownConfig = {
|
|
|
275
285
|
* false
|
|
276
286
|
*/
|
|
277
287
|
detailsTagDefaultOpen: boolean;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Interactive controls added to rendered mermaid diagrams.
|
|
291
|
+
*/
|
|
292
|
+
mermaid: {
|
|
293
|
+
/**
|
|
294
|
+
* Control buttons overlaid on the diagram.
|
|
295
|
+
* Set individual buttons to `false` to hide them.
|
|
296
|
+
*
|
|
297
|
+
* @default
|
|
298
|
+
* { zoom: true, reset: true, arrows: true, popover: true }
|
|
299
|
+
*/
|
|
300
|
+
buttons: {
|
|
301
|
+
/** Zoom in & zoom out buttons */
|
|
302
|
+
zoom: boolean;
|
|
303
|
+
/** Reset zoom & pan to the initial position */
|
|
304
|
+
reset: boolean;
|
|
305
|
+
/** Directional pan arrows */
|
|
306
|
+
arrows: boolean;
|
|
307
|
+
/** Open the diagram in a fullscreen popover */
|
|
308
|
+
popover: boolean;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Enable cmd/ctrl + drag to pan and cmd/ctrl + scroll to zoom.
|
|
313
|
+
* Plain click & scroll are never captured, so text selection
|
|
314
|
+
* and page scrolling keep working. Inside the popover, plain
|
|
315
|
+
* drag pans and plain scroll zooms regardless of this setting.
|
|
316
|
+
*
|
|
317
|
+
* @default
|
|
318
|
+
* true
|
|
319
|
+
*/
|
|
320
|
+
mouseActions: boolean;
|
|
321
|
+
};
|
|
278
322
|
};
|
|
279
323
|
};
|
|
280
324
|
|
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,18 +18,27 @@ 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
|
|
|
33
30
|
return html;
|
|
34
31
|
}
|
|
35
32
|
|
|
33
|
+
/** djb2 — stable, collision-tolerant key for identifying content across parses */
|
|
34
|
+
export function hashString(str: string): string {
|
|
35
|
+
let hash = 5381;
|
|
36
|
+
for (let i = 0; i < str.length; i++) {
|
|
37
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
38
|
+
}
|
|
39
|
+
return (hash >>> 0).toString(36);
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export function getHtmlElementText(html: string) {
|
|
37
43
|
try {
|
|
38
44
|
const parser = new DOMParser();
|
|
@@ -150,7 +156,7 @@ export function fixLocalImageHref(href: string, config: PantsdownConfig): string
|
|
|
150
156
|
|
|
151
157
|
export function cleanUrl(href: string) {
|
|
152
158
|
try {
|
|
153
|
-
href = encodeURI(href).replace(
|
|
159
|
+
href = encodeURI(href).replace(other.percentDecode, "%");
|
|
154
160
|
} catch (_) {
|
|
155
161
|
return null;
|
|
156
162
|
}
|
|
@@ -162,7 +168,7 @@ export const noopTest = { exec: () => null };
|
|
|
162
168
|
export function splitCells(tableRow: string, count?: number) {
|
|
163
169
|
// ensure that every cell-delimiting pipe has a space
|
|
164
170
|
// before it to distinguish it from an escaped pipe
|
|
165
|
-
const row = tableRow.replace(
|
|
171
|
+
const row = tableRow.replace(other.findPipe, (_match, offset: number, str: string) => {
|
|
166
172
|
let escaped = false;
|
|
167
173
|
let curr = offset;
|
|
168
174
|
while (--curr >= 0 && str[curr] === "\\") escaped = !escaped;
|
|
@@ -175,7 +181,7 @@ export function splitCells(tableRow: string, count?: number) {
|
|
|
175
181
|
return " |";
|
|
176
182
|
}
|
|
177
183
|
}),
|
|
178
|
-
cells = row.split(
|
|
184
|
+
cells = row.split(other.splitPipe);
|
|
179
185
|
|
|
180
186
|
// First/last cell in a row cannot be empty if it has no leading/trailing pipe
|
|
181
187
|
if (!cells[0]?.trim()) {
|
|
@@ -195,7 +201,7 @@ export function splitCells(tableRow: string, count?: number) {
|
|
|
195
201
|
|
|
196
202
|
for (let i = 0, len = cells.length; i < len; i++) {
|
|
197
203
|
// leading or trailing whitespace is ignored per the gfm spec
|
|
198
|
-
cells[i] = cells[i]!.trim().replace(
|
|
204
|
+
cells[i] = cells[i]!.trim().replace(other.slashPipe, "|");
|
|
199
205
|
}
|
|
200
206
|
return cells;
|
|
201
207
|
}
|
|
@@ -232,6 +238,20 @@ export function rtrim(str: string, c: string, invert?: boolean) {
|
|
|
232
238
|
return str.slice(0, l - suffLen);
|
|
233
239
|
}
|
|
234
240
|
|
|
241
|
+
export function trimTrailingBlankLines(str: string) {
|
|
242
|
+
const lines = str.split("\n");
|
|
243
|
+
let end = lines.length - 1;
|
|
244
|
+
while (end >= 0 && other.blankLine.test(lines[end]!)) {
|
|
245
|
+
end--;
|
|
246
|
+
}
|
|
247
|
+
if (lines.length - end <= 2) {
|
|
248
|
+
// we want to keep single trailing blank lines
|
|
249
|
+
return str;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return lines.slice(0, end + 1).join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
export function findClosingBracket(str: string, b: string) {
|
|
236
256
|
if (!b[1] || !str.includes(b[1])) {
|
|
237
257
|
return -1;
|
|
@@ -250,6 +270,9 @@ export function findClosingBracket(str: string, b: string) {
|
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
}
|
|
273
|
+
if (level > 0) {
|
|
274
|
+
return -2;
|
|
275
|
+
}
|
|
253
276
|
return -1;
|
|
254
277
|
}
|
|
255
278
|
|
|
@@ -260,33 +283,24 @@ export function outputLink(
|
|
|
260
283
|
lexer: Lexer,
|
|
261
284
|
): Tokens["Link"] | Tokens["Image"] {
|
|
262
285
|
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",
|
|
286
|
+
const title = link.title || null;
|
|
287
|
+
const text = cap[1]?.replace(other.outputLinkReplace, "$1") ?? "";
|
|
288
|
+
|
|
289
|
+
lexer.state.inLink = true;
|
|
290
|
+
const token: Tokens["Link"] | Tokens["Image"] = {
|
|
291
|
+
type: cap[0]?.charAt(0) === "!" ? "image" : "link",
|
|
281
292
|
raw,
|
|
282
293
|
href,
|
|
283
294
|
title,
|
|
284
|
-
text
|
|
295
|
+
text,
|
|
296
|
+
tokens: lexer.inlineTokens(text),
|
|
285
297
|
};
|
|
298
|
+
lexer.state.inLink = false;
|
|
299
|
+
return token;
|
|
286
300
|
}
|
|
287
301
|
|
|
288
302
|
export function indentCodeCompensation(raw: string, text: string) {
|
|
289
|
-
const matchIndentToCode =
|
|
303
|
+
const matchIndentToCode = other.indentCodeCompensation.exec(raw);
|
|
290
304
|
|
|
291
305
|
if (matchIndentToCode === null) {
|
|
292
306
|
return text;
|
|
@@ -297,7 +311,7 @@ export function indentCodeCompensation(raw: string, text: string) {
|
|
|
297
311
|
return text
|
|
298
312
|
.split("\n")
|
|
299
313
|
.map((node) => {
|
|
300
|
-
const matchIndentInNode =
|
|
314
|
+
const matchIndentInNode = other.beginningSpace.exec(node);
|
|
301
315
|
if (matchIndentInNode === null) {
|
|
302
316
|
return node;
|
|
303
317
|
}
|
|
@@ -344,3 +358,20 @@ export const ALERTS = [
|
|
|
344
358
|
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
359
|
},
|
|
346
360
|
];
|
|
361
|
+
|
|
362
|
+
export function expandTabs(line: string, indent = 0) {
|
|
363
|
+
let col = indent;
|
|
364
|
+
let expanded = "";
|
|
365
|
+
for (const char of line) {
|
|
366
|
+
if (char === "\t") {
|
|
367
|
+
const added = 4 - (col % 4);
|
|
368
|
+
expanded += " ".repeat(added);
|
|
369
|
+
col += added;
|
|
370
|
+
} else {
|
|
371
|
+
expanded += char;
|
|
372
|
+
col++;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return expanded;
|
|
377
|
+
}
|