pantsdown 1.5.2 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pantsdown",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "author": "wallpants",
5
5
  "description": "Markdown to 'GitHub HTML' parser",
6
6
  "license": "MIT",
@@ -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
@@ -18,7 +18,6 @@ export class Pantsdown {
18
18
  this.parser.renderer.slugger = new GithubSlugger();
19
19
 
20
20
  const tokens = this.lexer.lex(src);
21
- const html = this.parser.parse(tokens);
22
- return html;
21
+ return this.parser.parse(tokens);
23
22
  }
24
23
  }
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
  }
@@ -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
  };
@@ -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;
@@ -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