pantsdown 1.4.4 → 1.5.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 +3 -6
- package/package.json +1 -1
- package/src/css/styles.css +7 -0
- package/src/parser.ts +2 -2
- package/src/renderer.ts +59 -76
- package/src/tokenizer.ts +1 -3
- package/src/types.ts +3 -1
- package/src/utils.ts +16 -38
package/README.md
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
<img src="https://raw.githubusercontent.com/wallpants/pantsdown/main/docs/github.svg" height="60px" align="right" />
|
|
6
6
|
<img src="https://raw.githubusercontent.com/wallpants/pantsdown/main/docs/markdown.svg" height="60px" align="right" />
|
|
7
7
|
|
|
8
|
-
Pantsdown is a **Markdown** to **
|
|
9
|
-
Basically this is a non customizable version of Marked, optimized to render markdown similar to how GitHub does it plus
|
|
8
|
+
Pantsdown is a **Markdown** to **HTML** converter. It attempts to render markdown similar to how GitHub does it plus
|
|
10
9
|
some features developed specifically for [github-preview.nvim](https://github.com/wallpants/github-preview.nvim).
|
|
11
10
|
|
|
12
11
|
If you need a feature that's supported by GitHub and is not already listed in the [Roadmap](#roadmap),
|
|
13
|
-
feel free to open an issue.
|
|
12
|
+
feel free to open an issue.
|
|
14
13
|
|
|
15
14
|
### [Demo](https://wallpants.github.io/pantsdown/)
|
|
16
15
|
|
|
@@ -132,8 +131,6 @@ console.log(html);
|
|
|
132
131
|
|
|
133
132
|
## Roadmap
|
|
134
133
|
|
|
135
|
-
- [x] Add id & anchors to headings
|
|
136
|
-
- [ ] Add sourcemap to list items
|
|
137
134
|
- [ ] Add button to copy code in fences
|
|
138
|
-
- [ ] [Mermaid](https://mermaid.js.org/)
|
|
135
|
+
- [ ] [Mermaid](https://mermaid.js.org/) usage docs
|
|
139
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
package/src/parser.ts
CHANGED
|
@@ -126,13 +126,13 @@ export class Parser {
|
|
|
126
126
|
itemBody,
|
|
127
127
|
task,
|
|
128
128
|
Boolean(checked),
|
|
129
|
-
|
|
129
|
+
item.sourceMap,
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const listClasses: string[] = [];
|
|
134
134
|
if (containsTaskList) listClasses.push("contains-task-list");
|
|
135
|
-
out += this.renderer.list(body, ordered, start,
|
|
135
|
+
out += this.renderer.list(body, ordered, start, listClasses);
|
|
136
136
|
continue;
|
|
137
137
|
}
|
|
138
138
|
case "html": {
|
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 PantsdownConfig, type SourceMap } from "./types.ts";
|
|
4
|
+
import { type HTMLAttrs, type PantsdownConfig, type SourceMap } from "./types.ts";
|
|
5
5
|
import {
|
|
6
6
|
cleanUrl,
|
|
7
7
|
escape,
|
|
@@ -9,8 +9,6 @@ import {
|
|
|
9
9
|
fixLocalImageHref,
|
|
10
10
|
getHtmlElementText,
|
|
11
11
|
injectHtmlAttributes,
|
|
12
|
-
renderHtmlClasses,
|
|
13
|
-
renderSourceMap,
|
|
14
12
|
} from "./utils.ts";
|
|
15
13
|
|
|
16
14
|
const defaultConfig: NonNullable<PantsdownConfig["renderer"]> = {
|
|
@@ -32,19 +30,27 @@ export class Renderer {
|
|
|
32
30
|
|
|
33
31
|
code(code: string, infostring: string | undefined, sourceMap: SourceMap): string {
|
|
34
32
|
const lang = (infostring ?? "").match(/^\S*/)?.[0];
|
|
35
|
-
|
|
36
33
|
code = code.replace(/\n$/, "") + "\n";
|
|
37
34
|
|
|
35
|
+
if (lang === "mermaid") {
|
|
36
|
+
return injectHtmlAttributes(
|
|
37
|
+
`<section><div class="mermaid">${code}</div></section>`,
|
|
38
|
+
[["class", "mermaid-container"]],
|
|
39
|
+
sourceMap,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
38
43
|
const language = lang && hljs.getLanguage(lang) ? lang : "plaintext";
|
|
39
44
|
const highlightedCode = hljs.highlight(code, { language }).value;
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
`<pre
|
|
46
|
+
const result =
|
|
47
|
+
`<pre><code class="hljs language-` +
|
|
43
48
|
escape(language) +
|
|
44
49
|
'">' +
|
|
45
50
|
highlightedCode +
|
|
46
|
-
"</code></pre>\n"
|
|
47
|
-
|
|
51
|
+
"</code></pre>\n";
|
|
52
|
+
|
|
53
|
+
return injectHtmlAttributes(result, [], sourceMap);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
blockquote(quote: string): string {
|
|
@@ -54,81 +60,64 @@ export class Renderer {
|
|
|
54
60
|
html(html: string, _block: boolean, sourceMap?: SourceMap | undefined): string {
|
|
55
61
|
const result = fixHtmlLocalImageHref(html, this.rendererConfig.relativeImageUrlPrefix);
|
|
56
62
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (sourceMap) {
|
|
60
|
-
htmlAttributes.push(["line-start", sourceMap[0]], ["line-end", sourceMap[1]]);
|
|
61
|
-
}
|
|
63
|
+
const attrs: HTMLAttrs = [];
|
|
62
64
|
|
|
63
65
|
if (this.rendererConfig.detailsTagDefaultOpen) {
|
|
64
66
|
const tag = inline.tag.exec(html);
|
|
65
67
|
if (tag?.[0] === "<details>") {
|
|
66
|
-
|
|
68
|
+
attrs.push(["open", ""]);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
return injectHtmlAttributes(result,
|
|
72
|
+
return injectHtmlAttributes(result, attrs, sourceMap);
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
heading(text: string, level: number, sourceMap: SourceMap): string {
|
|
74
76
|
const elementText = getHtmlElementText(text);
|
|
75
77
|
const slug = this.slugger.slug(elementText);
|
|
76
|
-
let result = `<h${level}
|
|
78
|
+
let result = `<h${level}>`;
|
|
77
79
|
// span with negative top to add some offset when scrolling to #slug
|
|
78
80
|
result += `<span style="position: absolute; top: -50px;" id="${slug}"></span>`;
|
|
79
81
|
result += `${text}<a class="anchor octicon-link" href="#${slug}"></a>`;
|
|
80
82
|
result += `</h${level}>\n`;
|
|
81
|
-
return result;
|
|
83
|
+
return injectHtmlAttributes(result, [["style", "position: relative;"]], sourceMap);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
hr(sourceMap: SourceMap): string {
|
|
85
|
-
return `<hr
|
|
87
|
+
return injectHtmlAttributes(`<hr>\n`, [], sourceMap);
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
list(
|
|
89
|
-
body: string,
|
|
90
|
-
ordered: boolean,
|
|
91
|
-
start: number | "",
|
|
92
|
-
sourceMap: SourceMap,
|
|
93
|
-
classes: string[] = [],
|
|
94
|
-
): string {
|
|
90
|
+
list(body: string, ordered: boolean, start: number | "", classes: string[] = []): string {
|
|
95
91
|
const type = ordered ? "ol" : "ul";
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
listitem(
|
|
110
|
-
text: string,
|
|
111
|
-
task: boolean,
|
|
112
|
-
_checked: boolean,
|
|
113
|
-
lineStart: number | undefined,
|
|
114
|
-
): string {
|
|
115
|
-
const classes: string[] = [];
|
|
116
|
-
if (task) classes.push("task-list-item");
|
|
117
|
-
return `<li${renderHtmlClasses(classes)}${renderSourceMap(
|
|
118
|
-
lineStart ? [lineStart, lineStart] : undefined,
|
|
119
|
-
)}>${text}</li>\n`;
|
|
92
|
+
const attrs: HTMLAttrs = [["class", classes.join(" ")]];
|
|
93
|
+
if (ordered && start && start !== 1) {
|
|
94
|
+
attrs.push(["start", String(start)]);
|
|
95
|
+
}
|
|
96
|
+
const result = `<${type}>\n${body}</${type}>\n`;
|
|
97
|
+
return injectHtmlAttributes(result, attrs);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
listitem(text: string, task: boolean, _checked: boolean, sourceMap: SourceMap): string {
|
|
101
|
+
const attrs: [string, string][] = [];
|
|
102
|
+
if (task) attrs.push(["class", "task-list-item"]);
|
|
103
|
+
const result = `<li>${text}</li>\n`;
|
|
104
|
+
return injectHtmlAttributes(result, attrs, sourceMap);
|
|
120
105
|
}
|
|
121
106
|
|
|
122
107
|
checkbox(checked: boolean, classes: string[] = []): string {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
108
|
+
const result = "<input>";
|
|
109
|
+
const attrs: HTMLAttrs = [
|
|
110
|
+
["disabled", ""],
|
|
111
|
+
["type", "checkbox"],
|
|
112
|
+
["class", classes.join(" ")],
|
|
113
|
+
];
|
|
114
|
+
if (checked) attrs.push(["checked", ""]);
|
|
115
|
+
return injectHtmlAttributes(result, attrs);
|
|
128
116
|
}
|
|
129
117
|
|
|
130
118
|
paragraph(text: string, sourceMap: SourceMap): string {
|
|
131
|
-
|
|
119
|
+
const result = `<p>${text}</p>\n`;
|
|
120
|
+
return injectHtmlAttributes(result, [], sourceMap);
|
|
132
121
|
}
|
|
133
122
|
|
|
134
123
|
table(header: string, body: string): string {
|
|
@@ -137,9 +126,8 @@ export class Renderer {
|
|
|
137
126
|
}
|
|
138
127
|
|
|
139
128
|
tablerow(content: string, sourceMapStart: number | undefined): string {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)}>\n${content}</tr>\n`;
|
|
129
|
+
const sourceMap: SourceMap = sourceMapStart ? [sourceMapStart, sourceMapStart] : undefined;
|
|
130
|
+
return injectHtmlAttributes(`<tr>\n${content}</tr>\n`, [], sourceMap);
|
|
143
131
|
}
|
|
144
132
|
|
|
145
133
|
tablecell(
|
|
@@ -150,8 +138,10 @@ export class Renderer {
|
|
|
150
138
|
},
|
|
151
139
|
): string {
|
|
152
140
|
const type = flags.header ? "th" : "td";
|
|
153
|
-
const
|
|
154
|
-
|
|
141
|
+
const attrs: HTMLAttrs = [];
|
|
142
|
+
if (flags.align) attrs.push(["align", flags.align]);
|
|
143
|
+
const result = `<${type}>` + content + `</${type}>\n`;
|
|
144
|
+
return injectHtmlAttributes(result, attrs);
|
|
155
145
|
}
|
|
156
146
|
|
|
157
147
|
/**
|
|
@@ -182,13 +172,9 @@ export class Renderer {
|
|
|
182
172
|
if (cleanHref === null) {
|
|
183
173
|
return text;
|
|
184
174
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
out += ' title="' + title + '"';
|
|
189
|
-
}
|
|
190
|
-
out += ">" + text + "</a>";
|
|
191
|
-
return out;
|
|
175
|
+
const attrs: HTMLAttrs = [["href", cleanHref]];
|
|
176
|
+
if (title) attrs.push(["title", title]);
|
|
177
|
+
return injectHtmlAttributes(`<a>${text}</a>`, attrs);
|
|
192
178
|
}
|
|
193
179
|
|
|
194
180
|
image(href: string, title: string | null, text: string): string {
|
|
@@ -196,15 +182,12 @@ export class Renderer {
|
|
|
196
182
|
if (cleanHref === null) {
|
|
197
183
|
return text;
|
|
198
184
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (title)
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
out += ">";
|
|
207
|
-
return out;
|
|
185
|
+
const attrs: HTMLAttrs = [
|
|
186
|
+
["src", fixLocalImageHref(cleanHref, this.rendererConfig.relativeImageUrlPrefix)],
|
|
187
|
+
["alt", text],
|
|
188
|
+
];
|
|
189
|
+
if (title) attrs.push(["title", title]);
|
|
190
|
+
return injectHtmlAttributes("<img>", attrs);
|
|
208
191
|
}
|
|
209
192
|
|
|
210
193
|
text(text: string): string {
|
package/src/tokenizer.ts
CHANGED
|
@@ -132,7 +132,6 @@ export class Tokenizer {
|
|
|
132
132
|
start: isordered ? +bull.slice(0, -1) : "",
|
|
133
133
|
loose: false,
|
|
134
134
|
items: [] as Tokens["ListItem"][],
|
|
135
|
-
sourceMap: [0, 0], // updated below once token.raw is built
|
|
136
135
|
};
|
|
137
136
|
|
|
138
137
|
bull = isordered ? `\\d{1,9}\\${bull.slice(-1)}` : `\\${bull}`;
|
|
@@ -294,10 +293,9 @@ export class Tokenizer {
|
|
|
294
293
|
list.items[list.items.length - 1]!.text = itemContents.trimEnd();
|
|
295
294
|
list.raw = list.raw.trimEnd();
|
|
296
295
|
|
|
297
|
-
list.sourceMap = this.lexer.getSourceMap(list.raw);
|
|
298
|
-
|
|
299
296
|
// Item child tokens handled here at end because we needed to have the final item to trim it first
|
|
300
297
|
for (let i = 0, listItemsLen = list.items.length; i < listItemsLen; i++) {
|
|
298
|
+
list.items[i]!.sourceMap = this.lexer.getSourceMap(list.items[i]!.raw);
|
|
301
299
|
this.lexer.state.top = false;
|
|
302
300
|
list.items[i]!.tokens = this.lexer.blockTokens(list.items[i]!.text, []);
|
|
303
301
|
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type HTMLAttrs = [name: string, value: string][];
|
|
2
|
+
|
|
1
3
|
type BaseToken = {
|
|
2
4
|
type: string;
|
|
3
5
|
raw: string;
|
|
@@ -61,7 +63,6 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
61
63
|
start: number | "";
|
|
62
64
|
loose: boolean;
|
|
63
65
|
items: Tokens["ListItem"][];
|
|
64
|
-
sourceMap: SourceMap;
|
|
65
66
|
};
|
|
66
67
|
ListItem: {
|
|
67
68
|
type: "list_item";
|
|
@@ -71,6 +72,7 @@ export interface Tokens extends Record<string, BaseToken> {
|
|
|
71
72
|
checked?: boolean | undefined;
|
|
72
73
|
loose: boolean;
|
|
73
74
|
tokens: Token[];
|
|
75
|
+
sourceMap?: SourceMap;
|
|
74
76
|
};
|
|
75
77
|
Paragraph: {
|
|
76
78
|
type: "paragraph";
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Lexer } from "./lexer.ts";
|
|
2
|
-
import { type SourceMap, type Tokens } from "./types.ts";
|
|
2
|
+
import { type HTMLAttrs, type SourceMap, type Tokens } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Helpers
|
|
@@ -33,51 +33,34 @@ export function escape(html: string, encode?: boolean) {
|
|
|
33
33
|
return html;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function renderHtmlClasses(classes: string[]) {
|
|
37
|
-
if (!classes.length) return "";
|
|
38
|
-
let result = ' class="';
|
|
39
|
-
result += classes.join(" ");
|
|
40
|
-
result += '"';
|
|
41
|
-
return result;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseHtmlElement(html: string) {
|
|
45
|
-
const parser = new DOMParser();
|
|
46
|
-
const doc = parser.parseFromString(html, "text/html");
|
|
47
|
-
// eslint-disable-next-line
|
|
48
|
-
if (!doc.body) throw Error("Invalid HTML");
|
|
49
|
-
const element = doc.body.firstChild as HTMLElement;
|
|
50
|
-
// eslint-disable-next-line
|
|
51
|
-
if (!element) throw Error("No valid element found");
|
|
52
|
-
return element;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
36
|
export function getHtmlElementText(html: string) {
|
|
56
37
|
try {
|
|
57
|
-
const
|
|
38
|
+
const parser = new DOMParser();
|
|
39
|
+
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
|
|
44
|
+
if (!element) throw Error("No valid element found");
|
|
58
45
|
return element.textContent ?? html;
|
|
59
46
|
} catch (_) {
|
|
60
47
|
return html;
|
|
61
48
|
}
|
|
62
49
|
}
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
export function injectHtmlAttributes(
|
|
71
|
-
html: string,
|
|
72
|
-
attrs: [name: string, value: string | number][],
|
|
73
|
-
) {
|
|
51
|
+
export function injectHtmlAttributes(html: string, attrs: HTMLAttrs, sourceMap?: SourceMap) {
|
|
52
|
+
if (sourceMap) {
|
|
53
|
+
attrs.push(["line-start", String(sourceMap[0])]);
|
|
54
|
+
attrs.push(["line-end", String(sourceMap[1])]);
|
|
55
|
+
}
|
|
56
|
+
|
|
74
57
|
if (!attrs.length) return html;
|
|
75
58
|
|
|
76
|
-
const closingBracket = /[a-zA-Z0-9]
|
|
59
|
+
const closingBracket = /[a-zA-Z0-9\/"]>/;
|
|
77
60
|
const match = html.match(closingBracket);
|
|
78
61
|
if (match) {
|
|
79
62
|
let htmlAttrs = "";
|
|
80
|
-
attrs.forEach((atrr) => (htmlAttrs += ` ${atrr[0]}
|
|
63
|
+
attrs.forEach((atrr) => (htmlAttrs += ` ${atrr[0]}="${atrr[1]}"`));
|
|
81
64
|
const sliceIdx = match.index! + (match[0] === "/>" ? -1 : 1);
|
|
82
65
|
return html.slice(0, sliceIdx) + htmlAttrs + html.slice(sliceIdx);
|
|
83
66
|
}
|
|
@@ -128,11 +111,6 @@ export function fixLocalImageHref(
|
|
|
128
111
|
}
|
|
129
112
|
}
|
|
130
113
|
|
|
131
|
-
export function renderSourceMap(sourceMap: SourceMap) {
|
|
132
|
-
if (!sourceMap) return "";
|
|
133
|
-
return ` line-start=${sourceMap[0]} line-end=${sourceMap[1]}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
114
|
export function cleanUrl(href: string) {
|
|
137
115
|
try {
|
|
138
116
|
href = encodeURI(href).replace(/%25/g, "%");
|