pantsdown 1.5.2 → 1.6.1
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 -3
- package/package.json +1 -1
- package/src/css/styles.css +0 -5
- package/src/lexer.ts +26 -2
- 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 +45 -2
- package/src/types.ts +23 -2
- package/bunfig.toml +0 -2
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ const pantsdown = new Pantsdown();
|
|
|
73
73
|
|
|
74
74
|
function App() {
|
|
75
75
|
useEffect(() => {
|
|
76
|
-
const markdown = "# Hello world";
|
|
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: {
|
|
@@ -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/package.json
CHANGED
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
|
|
|
@@ -47,7 +57,7 @@ export class Lexer {
|
|
|
47
57
|
const tokenLines = rawToken.split("\n");
|
|
48
58
|
const sourceMap: SourceMap = [this.line, (this.line += tokenLines.length - 1)];
|
|
49
59
|
|
|
50
|
-
while (tokenLines.
|
|
60
|
+
while (tokenLines[tokenLines.length - 1] === "") {
|
|
51
61
|
// token.raw sometimes includes newline chars at the end of the string
|
|
52
62
|
// which we don't want to include in the sourceMap
|
|
53
63
|
tokenLines.pop();
|
|
@@ -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
|
@@ -283,19 +283,25 @@ export class Tokenizer {
|
|
|
283
283
|
loose: false,
|
|
284
284
|
text: itemContents,
|
|
285
285
|
tokens: [],
|
|
286
|
+
sourceMap: this.lexer.getSourceMap(raw),
|
|
286
287
|
});
|
|
287
288
|
|
|
288
289
|
list.raw += raw;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
// Do not consume newlines at end of final item. Alternatively, make itemRegex *start* with any newlines to simplify/speed up endsWithBlankLine logic
|
|
292
|
-
|
|
293
|
+
const lastTrimmed = raw.trimEnd();
|
|
294
|
+
|
|
295
|
+
if (list.items[list.items.length - 1]!.sourceMap) {
|
|
296
|
+
this.lexer.line -= raw.length - lastTrimmed.length;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
list.items[list.items.length - 1]!.raw = lastTrimmed;
|
|
293
300
|
list.items[list.items.length - 1]!.text = itemContents.trimEnd();
|
|
294
301
|
list.raw = list.raw.trimEnd();
|
|
295
302
|
|
|
296
303
|
// Item child tokens handled here at end because we needed to have the final item to trim it first
|
|
297
304
|
for (let i = 0, listItemsLen = list.items.length; i < listItemsLen; i++) {
|
|
298
|
-
list.items[i]!.sourceMap = this.lexer.getSourceMap(list.items[i]!.raw);
|
|
299
305
|
this.lexer.state.top = false;
|
|
300
306
|
list.items[i]!.tokens = this.lexer.blockTokens(list.items[i]!.text, []);
|
|
301
307
|
|
|
@@ -320,6 +326,32 @@ export class Tokenizer {
|
|
|
320
326
|
return list;
|
|
321
327
|
}
|
|
322
328
|
|
|
329
|
+
footnote(src: string): Tokens["Footnote"] | undefined {
|
|
330
|
+
const cap = block.footnote.exec(src);
|
|
331
|
+
if (!cap) return undefined;
|
|
332
|
+
|
|
333
|
+
const label = cap[2] ?? "";
|
|
334
|
+
let text = cap[3] ? rtrim(cap[3].replace(/^ *[ \t]?/gm, ""), "\n") : "";
|
|
335
|
+
|
|
336
|
+
text += `<a href="#footnote-ref-${encodeURIComponent(
|
|
337
|
+
label,
|
|
338
|
+
)}" data-footnote-backref aria-label="Back to reference ${label}"> ↩</a>`;
|
|
339
|
+
|
|
340
|
+
this.lexer.state.top = false;
|
|
341
|
+
const tokens = this.lexer.blockTokens(text, []);
|
|
342
|
+
|
|
343
|
+
const token: Tokens["Footnote"] = {
|
|
344
|
+
type: "footnote",
|
|
345
|
+
raw: cap[0],
|
|
346
|
+
text: text,
|
|
347
|
+
label: label,
|
|
348
|
+
content: tokens,
|
|
349
|
+
sourceMap: this.lexer.getSourceMap(cap[0]),
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
return token;
|
|
353
|
+
}
|
|
354
|
+
|
|
323
355
|
html(src: string): Tokens["HTML"] | undefined {
|
|
324
356
|
const cap = block.html.exec(src);
|
|
325
357
|
if (!cap) return undefined;
|
|
@@ -720,6 +752,17 @@ export class Tokenizer {
|
|
|
720
752
|
return undefined;
|
|
721
753
|
}
|
|
722
754
|
|
|
755
|
+
footnoteRef(src: string): Tokens["FootnoteRef"] | undefined {
|
|
756
|
+
const cap = inline.footnoteRef.exec(src);
|
|
757
|
+
if (!cap) return undefined;
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
type: "footnoteRef",
|
|
761
|
+
raw: cap[0],
|
|
762
|
+
label: cap[1] ?? "",
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
723
766
|
codespan(src: string): Tokens["Codespan"] | undefined {
|
|
724
767
|
const cap = inline.code.exec(src);
|
|
725
768
|
if (!cap) return undefined;
|
package/src/types.ts
CHANGED
|
@@ -72,7 +72,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
72
72
|
checked?: boolean | undefined;
|
|
73
73
|
loose: boolean;
|
|
74
74
|
tokens: Token[];
|
|
75
|
-
sourceMap
|
|
75
|
+
sourceMap: SourceMap;
|
|
76
76
|
};
|
|
77
77
|
Paragraph: {
|
|
78
78
|
type: "paragraph";
|
|
@@ -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
|
|
package/bunfig.toml
DELETED