pantsdown 1.5.1 → 1.6.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 +4 -5
- package/bunfig.toml +2 -0
- package/package.json +2 -1
- package/src/css/styles.css +0 -5
- package/src/lexer.ts +25 -1
- package/src/pantsdown.ts +1 -2
- package/src/parser.ts +19 -0
- package/src/renderer.ts +19 -1
- package/src/rules/block.ts +6 -0
- package/src/rules/inline.ts +2 -0
- package/src/tokenizer.ts +41 -4
- package/src/types.ts +22 -1
- package/src/sample.test.ts +0 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Up to marked@9.1.
|
|
1
|
+
<!-- Up to marked@9.1.3 -->
|
|
2
2
|
|
|
3
3
|
# Pantsdown
|
|
4
4
|
|
|
@@ -73,7 +73,7 @@ const pantsdown = new Pantsdown();
|
|
|
73
73
|
|
|
74
74
|
function App() {
|
|
75
75
|
useEffect(() => {
|
|
76
|
-
const markdown = "# Hello world\n- [ ]
|
|
76
|
+
const markdown = "# Hello world\n- [ ] Task 1\n- [x] Task 2";
|
|
77
77
|
const html = pantsdown.parse(markdown);
|
|
78
78
|
const container = document.getElementById("markdown-container");
|
|
79
79
|
if (container) container.innerHTML = html;
|
|
@@ -92,7 +92,7 @@ export default App;
|
|
|
92
92
|
The Pantsdown constructor accepts an optional configuration object:
|
|
93
93
|
|
|
94
94
|
```typescript
|
|
95
|
-
import { PantsdownConfig } from "pantsdown";
|
|
95
|
+
import { Pantsdown, PantsdownConfig } from "pantsdown";
|
|
96
96
|
|
|
97
97
|
const config: PantsdownConfig = {
|
|
98
98
|
renderer: {
|
|
@@ -114,7 +114,7 @@ const config: PantsdownConfig = {
|
|
|
114
114
|
relativeImageUrlPrefix: "/__localimage__/",
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Whether to render <details> html tags with attribute
|
|
117
|
+
* Whether to render <details> html tags with attribute open=""
|
|
118
118
|
*
|
|
119
119
|
* @default
|
|
120
120
|
* false
|
|
@@ -133,4 +133,3 @@ console.log(html);
|
|
|
133
133
|
|
|
134
134
|
- [ ] Add button to copy code in fences
|
|
135
135
|
- [ ] [Mermaid](https://mermaid.js.org/) usage docs
|
|
136
|
-
- [ ] [Footnote](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes) support [Base](https://github.com/bent10/marked-extensions/tree/main/packages/footnote)
|
package/bunfig.toml
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantsdown",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"author": "wallpants",
|
|
5
5
|
"description": "Markdown to 'GitHub HTML' parser",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"highlight.js": "^11.9.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
+
"@happy-dom/global-registrator": "^12.10.3",
|
|
45
46
|
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
|
46
47
|
"@typescript-eslint/parser": "^6.7.3",
|
|
47
48
|
"bun-types": "latest",
|
package/src/css/styles.css
CHANGED
|
@@ -116,7 +116,6 @@
|
|
|
116
116
|
.pantsdown a {
|
|
117
117
|
background-color: transparent;
|
|
118
118
|
color: var(--color-accent-fg);
|
|
119
|
-
text-decoration: none;
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
.pantsdown abbr[title] {
|
|
@@ -240,10 +239,6 @@
|
|
|
240
239
|
font: inherit;
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
.pantsdown a:hover {
|
|
244
|
-
text-decoration: underline;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
242
|
.pantsdown ::placeholder {
|
|
248
243
|
color: var(--color-fg-subtle);
|
|
249
244
|
opacity: 1;
|
package/src/lexer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { inline } from "./rules/inline.ts";
|
|
2
2
|
import { Tokenizer } from "./tokenizer.ts";
|
|
3
|
-
import { type Links, type SourceMap, type Token } from "./types.ts";
|
|
3
|
+
import { type Links, type SourceMap, type Token, type Tokens } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export class Lexer {
|
|
6
6
|
private tokenizer: Tokenizer;
|
|
@@ -8,6 +8,7 @@ export class Lexer {
|
|
|
8
8
|
private links: Links = {};
|
|
9
9
|
|
|
10
10
|
tokens: Token[] = [];
|
|
11
|
+
footnoteTokens: Tokens["Footnote"][] = [];
|
|
11
12
|
line = 1;
|
|
12
13
|
state = {
|
|
13
14
|
inLink: false,
|
|
@@ -26,6 +27,7 @@ export class Lexer {
|
|
|
26
27
|
lex(src: string) {
|
|
27
28
|
// reset values from previous parse
|
|
28
29
|
this.tokenizer.pendingHtmlClose = [];
|
|
30
|
+
this.footnoteTokens = [];
|
|
29
31
|
this.tokens = [];
|
|
30
32
|
this.links = {};
|
|
31
33
|
this.line = 1;
|
|
@@ -39,6 +41,14 @@ export class Lexer {
|
|
|
39
41
|
this.inlineTokens(next.src, next.tokens);
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
const footnotesToken: Tokens["Footnotes"] = {
|
|
45
|
+
type: "footnotes",
|
|
46
|
+
raw: "",
|
|
47
|
+
items: this.footnoteTokens,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.tokens.push(footnotesToken);
|
|
51
|
+
|
|
42
52
|
return this.tokens;
|
|
43
53
|
}
|
|
44
54
|
|
|
@@ -108,6 +118,13 @@ export class Lexer {
|
|
|
108
118
|
continue;
|
|
109
119
|
}
|
|
110
120
|
|
|
121
|
+
// footnote
|
|
122
|
+
if ((token = this.tokenizer.footnote(src))) {
|
|
123
|
+
src = src.substring(token.raw.length);
|
|
124
|
+
this.footnoteTokens.push(token);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
// heading
|
|
112
129
|
if ((token = this.tokenizer.heading(src))) {
|
|
113
130
|
src = src.substring(token.raw.length);
|
|
@@ -294,6 +311,13 @@ export class Lexer {
|
|
|
294
311
|
continue;
|
|
295
312
|
}
|
|
296
313
|
|
|
314
|
+
// footnoteRef
|
|
315
|
+
if ((token = this.tokenizer.footnoteRef(src))) {
|
|
316
|
+
src = src.substring(token.raw.length);
|
|
317
|
+
tokens.push(token);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
297
321
|
// link
|
|
298
322
|
if ((token = this.tokenizer.link(src))) {
|
|
299
323
|
src = src.substring(token.raw.length);
|
package/src/pantsdown.ts
CHANGED
package/src/parser.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Renderer } from "./renderer.ts";
|
|
2
2
|
import { type PantsdownConfig, type Token, type Tokens } from "./types.ts";
|
|
3
|
+
import { injectHtmlAttributes } from "./utils.ts";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Parsing & Compiling
|
|
@@ -143,6 +144,20 @@ export class Parser {
|
|
|
143
144
|
);
|
|
144
145
|
continue;
|
|
145
146
|
}
|
|
147
|
+
case "footnotes": {
|
|
148
|
+
const body = token.items.reduce((acc, { label, content, sourceMap }) => {
|
|
149
|
+
let footnoteItem = `<li id="footnote-${encodeURIComponent(label)}">\n`;
|
|
150
|
+
footnoteItem += this.parse(content);
|
|
151
|
+
footnoteItem += "</li>\n";
|
|
152
|
+
|
|
153
|
+
footnoteItem = injectHtmlAttributes(footnoteItem, [], sourceMap);
|
|
154
|
+
|
|
155
|
+
return acc + footnoteItem;
|
|
156
|
+
}, "");
|
|
157
|
+
|
|
158
|
+
out += this.renderer.footnotes(token, body);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
146
161
|
case "paragraph": {
|
|
147
162
|
out += this.renderer.paragraph(this.parseInline(token.tokens), token.sourceMap);
|
|
148
163
|
continue;
|
|
@@ -212,6 +227,10 @@ export class Parser {
|
|
|
212
227
|
out += this.renderer.em(this.parseInline(token.tokens));
|
|
213
228
|
break;
|
|
214
229
|
}
|
|
230
|
+
case "footnoteRef": {
|
|
231
|
+
out += this.renderer.footnoteRef(token);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
215
234
|
case "codespan": {
|
|
216
235
|
out += this.renderer.codespan(token.text);
|
|
217
236
|
break;
|
package/src/renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import GithubSlugger from "github-slugger";
|
|
2
2
|
import hljs from "highlight.js";
|
|
3
3
|
import { inline } from "./rules/inline.ts";
|
|
4
|
-
import { type HTMLAttrs, type PantsdownConfig, type SourceMap } from "./types.ts";
|
|
4
|
+
import { type HTMLAttrs, type PantsdownConfig, type SourceMap, type Tokens } from "./types.ts";
|
|
5
5
|
import {
|
|
6
6
|
cleanUrl,
|
|
7
7
|
escape,
|
|
@@ -190,6 +190,24 @@ export class Renderer {
|
|
|
190
190
|
return injectHtmlAttributes("<img>", attrs);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
footnoteRef(token: Tokens["FootnoteRef"]) {
|
|
194
|
+
const encodedLabel = encodeURIComponent(token.label);
|
|
195
|
+
|
|
196
|
+
return `<sup><a id="footnote-ref-${encodedLabel}" href="#${
|
|
197
|
+
"footnote-" + encodedLabel
|
|
198
|
+
}" data-footnote-ref aria-describedby="footnote-label">${token.label}</a></sup>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
footnotes(token: Tokens["Footnotes"], body: string) {
|
|
202
|
+
if (!token.items.length) return "";
|
|
203
|
+
|
|
204
|
+
let footnotesHTML = '<section class="footnotes" data-footnotes>\n';
|
|
205
|
+
footnotesHTML += `<ol>\n${body}\n</ol>\n`;
|
|
206
|
+
footnotesHTML += "</section>\n";
|
|
207
|
+
|
|
208
|
+
return footnotesHTML;
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
text(text: string): string {
|
|
194
212
|
return text;
|
|
195
213
|
}
|
package/src/rules/block.ts
CHANGED
|
@@ -15,6 +15,7 @@ type BlockRuleNames =
|
|
|
15
15
|
| "text"
|
|
16
16
|
| "bullet"
|
|
17
17
|
| "listItemStart"
|
|
18
|
+
| "footnote"
|
|
18
19
|
| "paragraph";
|
|
19
20
|
|
|
20
21
|
export const label = /(?!\s*\])(?:\\.|[^\[\]\\])+/;
|
|
@@ -110,6 +111,10 @@ const block_blockquote = edit(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/)
|
|
|
110
111
|
.replace("paragraph", block_paragraph)
|
|
111
112
|
.getRegex();
|
|
112
113
|
|
|
114
|
+
const block_footnote = edit(/^( {0,3}\[\^(\d+)\]:(?:[ \t]+|[\n]*?|$)?(paragraph|[^\n]*))+/)
|
|
115
|
+
.replace("paragraph", block_paragraph)
|
|
116
|
+
.getRegex();
|
|
117
|
+
|
|
113
118
|
export const block: Record<BlockRuleNames, RegExp> = {
|
|
114
119
|
newline: /^(?: *(?:\n|$))+/,
|
|
115
120
|
code: /^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,
|
|
@@ -125,5 +130,6 @@ export const block: Record<BlockRuleNames, RegExp> = {
|
|
|
125
130
|
table: block_table,
|
|
126
131
|
lheading: block_lheading,
|
|
127
132
|
paragraph: block_paragraph,
|
|
133
|
+
footnote: block_footnote,
|
|
128
134
|
text: /^[^\n]+/,
|
|
129
135
|
};
|
package/src/rules/inline.ts
CHANGED
|
@@ -19,6 +19,7 @@ type InlineRuleNames =
|
|
|
19
19
|
| "punctuation"
|
|
20
20
|
| "blockSkip"
|
|
21
21
|
| "escapes"
|
|
22
|
+
| "footnoteRef"
|
|
22
23
|
| "backpedal";
|
|
23
24
|
|
|
24
25
|
// list of unicode punctuation marks, plus any missing characters from CommonMark spec
|
|
@@ -143,4 +144,5 @@ export const inline: Omit<Record<InlineRuleNames, RegExp>, "emStrong"> & {
|
|
|
143
144
|
blockSkip: inline_blockSkip,
|
|
144
145
|
escapes: inline_escapes,
|
|
145
146
|
backpedal: inline_backpedal,
|
|
147
|
+
footnoteRef: /^\[\^([^\]\n]+)\]/,
|
|
146
148
|
};
|
package/src/tokenizer.ts
CHANGED
|
@@ -320,6 +320,32 @@ export class Tokenizer {
|
|
|
320
320
|
return list;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
footnote(src: string): Tokens["Footnote"] | undefined {
|
|
324
|
+
const cap = block.footnote.exec(src);
|
|
325
|
+
if (!cap) return undefined;
|
|
326
|
+
|
|
327
|
+
const label = cap[2] ?? "";
|
|
328
|
+
let text = cap[3] ? rtrim(cap[3].replace(/^ *[ \t]?/gm, ""), "\n") : "";
|
|
329
|
+
|
|
330
|
+
text += `<a href="#footnote-ref-${encodeURIComponent(
|
|
331
|
+
label,
|
|
332
|
+
)}" data-footnote-backref aria-label="Back to reference ${label}"> ↩</a>`;
|
|
333
|
+
|
|
334
|
+
this.lexer.state.top = false;
|
|
335
|
+
const tokens = this.lexer.blockTokens(text, []);
|
|
336
|
+
|
|
337
|
+
const token: Tokens["Footnote"] = {
|
|
338
|
+
type: "footnote",
|
|
339
|
+
raw: cap[0],
|
|
340
|
+
text: text,
|
|
341
|
+
label: label,
|
|
342
|
+
content: tokens,
|
|
343
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
return token;
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
html(src: string): Tokens["HTML"] | undefined {
|
|
324
350
|
const cap = block.html.exec(src);
|
|
325
351
|
if (!cap) return undefined;
|
|
@@ -369,16 +395,16 @@ export class Tokenizer {
|
|
|
369
395
|
* and update their sourceMap once they're closed.
|
|
370
396
|
*/
|
|
371
397
|
|
|
372
|
-
const capEndsWith = (str
|
|
398
|
+
const capEndsWith = (str?: string) => str && cap[0].trimEnd().endsWith(str);
|
|
373
399
|
|
|
374
400
|
const tag = inline.tag.exec(src);
|
|
375
|
-
const isHtmlClosed = capEndsWith(tag
|
|
401
|
+
const isHtmlClosed = capEndsWith(tag?.[0].slice(1));
|
|
376
402
|
|
|
377
|
-
if (!isHtmlClosed) {
|
|
403
|
+
if (tag?.[0] && !isHtmlClosed) {
|
|
378
404
|
// index where the token we just created will be inserted
|
|
379
405
|
const tokenIdx = this.lexer.tokens.length;
|
|
380
406
|
// first in last out
|
|
381
|
-
this.pendingHtmlClose.unshift([tag
|
|
407
|
+
this.pendingHtmlClose.unshift([tag[0], tokenIdx]);
|
|
382
408
|
} else if (this.pendingHtmlClose.length) {
|
|
383
409
|
for (const [pendingTag, index] of this.pendingHtmlClose) {
|
|
384
410
|
if (capEndsWith(pendingTag.slice(1))) {
|
|
@@ -720,6 +746,17 @@ export class Tokenizer {
|
|
|
720
746
|
return undefined;
|
|
721
747
|
}
|
|
722
748
|
|
|
749
|
+
footnoteRef(src: string): Tokens["FootnoteRef"] | undefined {
|
|
750
|
+
const cap = inline.footnoteRef.exec(src);
|
|
751
|
+
if (!cap) return undefined;
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
type: "footnoteRef",
|
|
755
|
+
raw: cap[0],
|
|
756
|
+
label: cap[1] ?? "",
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
723
760
|
codespan(src: string): Tokens["Codespan"] | undefined {
|
|
724
761
|
const cap = inline.code.exec(src);
|
|
725
762
|
if (!cap) return undefined;
|
package/src/types.ts
CHANGED
|
@@ -160,6 +160,24 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
160
160
|
text: string;
|
|
161
161
|
tokens: Token[];
|
|
162
162
|
};
|
|
163
|
+
Footnote: {
|
|
164
|
+
type: "footnote";
|
|
165
|
+
raw: string;
|
|
166
|
+
text: string;
|
|
167
|
+
label: string;
|
|
168
|
+
content: Token[];
|
|
169
|
+
sourceMap: SourceMap;
|
|
170
|
+
};
|
|
171
|
+
FootnoteRef: {
|
|
172
|
+
type: "footnoteRef";
|
|
173
|
+
raw: string;
|
|
174
|
+
label: string;
|
|
175
|
+
};
|
|
176
|
+
Footnotes: {
|
|
177
|
+
type: "footnotes";
|
|
178
|
+
raw: string;
|
|
179
|
+
items: Tokens["Footnote"][];
|
|
180
|
+
};
|
|
163
181
|
}
|
|
164
182
|
|
|
165
183
|
export type Token =
|
|
@@ -184,7 +202,10 @@ export type Token =
|
|
|
184
202
|
| Tokens["Em"]
|
|
185
203
|
| Tokens["Codespan"]
|
|
186
204
|
| Tokens["Br"]
|
|
187
|
-
| Tokens["Del"]
|
|
205
|
+
| Tokens["Del"]
|
|
206
|
+
| Tokens["Footnote"]
|
|
207
|
+
| Tokens["FootnoteRef"]
|
|
208
|
+
| Tokens["Footnotes"];
|
|
188
209
|
|
|
189
210
|
export type Links = Record<string, { href: string; title: string }>;
|
|
190
211
|
|