pantsdown 2.2.6 → 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/src/types.ts CHANGED
@@ -9,7 +9,7 @@ type BaseToken = {
9
9
 
10
10
  export type SourceMap = [start: number, end: number] | undefined;
11
11
 
12
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
12
+ // eslint-disable-next-line typescript/consistent-type-definitions
13
13
  export interface Tokens extends Record<string, BaseToken> {
14
14
  Space: {
15
15
  type: "space";
@@ -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"]
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,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
 
@@ -37,10 +34,7 @@ export function getHtmlElementText(html: string) {
37
34
  try {
38
35
  const parser = new DOMParser();
39
36
  const doc = parser.parseFromString(html, "text/html");
40
- // eslint-disable-next-line
41
- if (!doc.body) throw Error("Invalid HTML");
42
- const element = doc.body.firstChild as HTMLElement;
43
- // eslint-disable-next-line
37
+ const element = doc.body.firstChild;
44
38
  if (!element) throw Error("No valid element found");
45
39
  return element.textContent || html;
46
40
  } catch (_) {
@@ -56,12 +50,15 @@ export function injectHtmlAttributes(html: string, attrs: HTMLAttrs, sourceMap?:
56
50
 
57
51
  if (!attrs.length) return html;
58
52
 
53
+ // closing tags cannot carry attributes
54
+ if (/^\s*<\//.test(html)) return html;
55
+
59
56
  const closingBracket = /[a-zA-Z0-9\/"]>/;
60
57
  const match = closingBracket.exec(html);
61
58
  if (match) {
62
59
  let htmlAttrs = "";
63
60
  attrs.forEach((atrr) => (htmlAttrs += ` ${atrr[0]}="${atrr[1]}"`));
64
- const sliceIdx = match.index + (match[0] === "/>" ? -1 : 1);
61
+ const sliceIdx = match.index + (match[0] === "/>" ? 0 : 1);
65
62
  return html.slice(0, sliceIdx) + htmlAttrs + html.slice(sliceIdx);
66
63
  }
67
64
  return html;
@@ -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(/%25/g, "%");
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(/\|/g, (_match, offset: number, str: string) => {
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(/\\\|/g, "|");
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 ? 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",
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: escape(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 = /^(\s+)(?:```)/.exec(raw);
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 = /^\s+/.exec(node);
305
+ const matchIndentInNode = other.beginningSpace.exec(node);
301
306
  if (matchIndentInNode === null) {
302
307
  return node;
303
308
  }
@@ -314,7 +319,7 @@ export function indentCodeCompensation(raw: string, text: string) {
314
319
  }
315
320
 
316
321
  function makeAlertRegex(type: string) {
317
- return new RegExp(`^(?:\\[\\!${type.toUpperCase()}\\]|[\\*]{2}${type}[\\*]{2})[\s]*?\n?`);
322
+ return new RegExp(`^(?:\\[\\!${type.toUpperCase()}\\]|[\\*]{2}${type}[\\*]{2})[ \\t]*\\n?`);
318
323
  }
319
324
 
320
325
  export const ALERTS = [
@@ -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
+ }
package/.oxfmtrc.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
- "ignorePatterns": [".github"],
4
- "printWidth": 100,
5
- "tabWidth": 3,
6
- "sortImports": {
7
- "newlinesBetween": false
8
- },
9
- "overrides": [
10
- {
11
- "files": ["*.json", "*.md"],
12
- "options": {
13
- "tabWidth": 2
14
- }
15
- }
16
- ]
17
- }
package/CLAUDE.md DELETED
@@ -1,73 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Project Overview
6
-
7
- Pantsdown is a Markdown to HTML converter that renders markdown similar to GitHub's styling. It was built specifically for [github-preview.nvim](https://github.com/wallpants/github-preview.nvim). Based on [Marked](https://github.com/markedjs/marked).
8
-
9
- ## Commands
10
-
11
- ```bash
12
- # Type checking
13
- bun run typecheck
14
-
15
- # Linting
16
- bun run lint
17
-
18
- # Both typecheck and lint
19
- bun run check
20
-
21
- # Run tests (uses bun:test with happy-dom)
22
- bun test
23
-
24
- # Run a single test file
25
- bun test tests/parse.test.ts
26
-
27
- # Update test snapshots
28
- bun test --update-snapshots
29
-
30
- # Format code
31
- bun run format
32
-
33
- # Build docs
34
- bun run docs:build
35
- ```
36
-
37
- ## Architecture
38
-
39
- The parsing pipeline follows a classic compiler pattern:
40
-
41
- 1. **Lexer** (`src/lexer.ts`) - Tokenizes markdown source into an array of tokens
42
- - Uses `Tokenizer` for the actual token creation
43
- - Processes block-level tokens first, then inline tokens
44
- - Tracks source maps for line number references
45
- - Collects footnotes separately
46
-
47
- 2. **Tokenizer** (`src/tokenizer.ts`) - Creates tokens from markdown patterns
48
- - Uses regex rules from `src/rules/block.ts` and `src/rules/inline.ts`
49
-
50
- 3. **Parser** (`src/parser.ts`) - Converts tokens to HTML by dispatching to the Renderer
51
- - Recursively processes nested tokens
52
-
53
- 4. **Renderer** (`src/renderer.ts`) - Produces HTML output for each token type
54
- - Uses highlight.js for syntax highlighting
55
- - Uses github-slugger for heading anchors
56
- - Handles special cases like mermaid diagrams and alerts
57
-
58
- Entry point: `Pantsdown` class in `src/pantsdown.ts` coordinates the pipeline.
59
-
60
- ## Key Types
61
-
62
- All token types are defined in `src/types.ts`. The `Token` union type covers all possible markdown elements (headings, code blocks, lists, tables, footnotes, alerts, etc.).
63
-
64
- ## Output
65
-
66
- `pantsdown.parse(markdown)` returns `{ html, javascript }`:
67
-
68
- - `html`: The rendered HTML string
69
- - `javascript`: A script for interactive features (task list checkboxes, copy buttons)
70
-
71
- ## Styling
72
-
73
- CSS is in `src/css/styles.css`. Requires a parent element with classes `pantsdown light` or `pantsdown dark` (optionally with `high-contrast`).