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/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: "text" | "html";
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 { type PantsdownConfig, type HTMLAttrs, type SourceMap, type Tokens } from "./types.ts";
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
  "&": "&amp;",
13
10
  "<": "&lt;",
@@ -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(/%25/g, "%");
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(/\|/g, (_match, offset: number, str: string) => {
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(/\\\|/g, "|");
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 ? escape(link.title) : null;
264
- const text = cap[1]?.replace(/\\([\[\]])/g, "$1") ?? "";
265
-
266
- if (cap[0]?.charAt(0) !== "!") {
267
- lexer.state.inLink = true;
268
- const token: Tokens["Link"] = {
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: escape(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 = /^(\s+)(?:```)/.exec(raw);
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 = /^\s+/.exec(node);
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
+ }