extramark 2.0.3 → 3.0.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/LICENSE.md +1 -1
- package/README.md +13 -10
- package/bin/extramark.js +5 -5
- package/bin/extramark.test.js +93 -0
- package/{src → lib}/index.js +4 -5
- package/lib/index.test.js +176 -0
- package/package.json +16 -20
- package/src/critic.js +0 -11
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2018-
|
|
3
|
+
Copyright (c) 2018-2026 Márton Visnovitz
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
6
|
|
package/README.md
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# ExtraMark
|
|
2
2
|
|
|
3
|
-
[![License][badge
|
|
4
|
-
[![Version][badge
|
|
3
|
+
[![License][license-badge]][license-link]
|
|
4
|
+
[![Version][version-badge]][version-link]
|
|
5
|
+
[![Build][build-badge]][build-link]
|
|
5
6
|
|
|
6
|
-
[
|
|
7
|
-
[link-license]: https://github.com/vimtaai/extramark/blob/master/LICENSE.md
|
|
8
|
-
[badge-version]: https://img.shields.io/npm/v/extramark.svg
|
|
9
|
-
[link-version]: https://npmjs.com/package/extramark
|
|
7
|
+
Superset of [CommonMark](https://commonmark.org/) that includes syntax extensions that are commonly used by other Markdown dialects. The list and syntax for the extensions are decided by looking at the most popular Markdown dialects and selecting the most common syntax for each feature. The goal of the project is to create a (somewhat) standard superset of CommonMark that supports the features that are requested by most. The parser is based on [markdown-it](https://github.com/markdown-it/markdown-it) and uses plugins for syntax extensions.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
9
|
+
## Setup
|
|
14
10
|
|
|
15
11
|
Install via `npm`:
|
|
16
12
|
|
|
@@ -85,4 +81,11 @@ extramark --help
|
|
|
85
81
|
|
|
86
82
|
## Contributing
|
|
87
83
|
|
|
88
|
-
All ideas, recommendations, bug reports, pull requests are welcome.
|
|
84
|
+
All ideas, recommendations, bug reports, pull requests are welcome. 😊
|
|
85
|
+
|
|
86
|
+
[license-badge]: https://img.shields.io/npm/l/extramark.svg
|
|
87
|
+
[license-link]: https://github.com/vimtaai/extramark/blob/master/LICENSE.md
|
|
88
|
+
[version-badge]: https://img.shields.io/npm/v/extramark.svg
|
|
89
|
+
[version-link]: https://npmjs.com/package/extramark
|
|
90
|
+
[build-badge]: https://github.com/vimtaai/extramark/actions/workflows/main.yaml/badge.svg
|
|
91
|
+
[build-link]: https://github.com/vimtaai/extramark/actions/workflows/main.yaml
|
package/bin/extramark.js
CHANGED
|
@@ -9,11 +9,11 @@ import { styleText } from "node:util";
|
|
|
9
9
|
import { program } from "commander";
|
|
10
10
|
import { html } from "common-tags";
|
|
11
11
|
|
|
12
|
-
import { render } from "../
|
|
12
|
+
import { render } from "../lib/index.js";
|
|
13
13
|
|
|
14
14
|
program
|
|
15
15
|
.description("CLI for converting Markdown documents to HTML with ExtraMark")
|
|
16
|
-
.
|
|
16
|
+
.argument("<input>", "path of the input .md file")
|
|
17
17
|
.option("-o --output [path]", "path of the output .html file")
|
|
18
18
|
.option("-c --css [css]", "path to the CSS file to use for formatting")
|
|
19
19
|
.option("-t --title [title]", "title of the output document")
|
|
@@ -41,8 +41,8 @@ try {
|
|
|
41
41
|
data.parsed = html`
|
|
42
42
|
<!DOCTYPE html>
|
|
43
43
|
<meta charset="utf-8" />
|
|
44
|
-
<title>${
|
|
45
|
-
${
|
|
44
|
+
<title>${options.title || `Markdown document`}</title>
|
|
45
|
+
${options.css ? `<link rel="stylesheet" href="${options.css}">` : ``} ${await render(data.raw)}
|
|
46
46
|
`;
|
|
47
47
|
} catch {
|
|
48
48
|
error(`${styleText("red", "!")} Could not parse input data.`);
|
|
@@ -54,7 +54,7 @@ try {
|
|
|
54
54
|
await mkdir(dirname(outputFile), { recursive: true });
|
|
55
55
|
await writeFile(outputFile, data.parsed);
|
|
56
56
|
} else {
|
|
57
|
-
stdout.write(data.parsed
|
|
57
|
+
stdout.write(`${data.parsed}\n`);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (outputFile && !options.quiet) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
|
|
6
|
+
import { createCliRunner } from "../test/cli-runner.js";
|
|
7
|
+
import { setupTempDir, writeInputFile } from "../test/temp-dir.js";
|
|
8
|
+
|
|
9
|
+
const cliPath = new URL("./extramark.js", import.meta.url).pathname;
|
|
10
|
+
const runCli = createCliRunner(cliPath);
|
|
11
|
+
const getTempDir = setupTempDir();
|
|
12
|
+
|
|
13
|
+
describe("extramark CLI", () => {
|
|
14
|
+
it("renders Markdown to HTML with no options", async () => {
|
|
15
|
+
const inputFile = await writeInputFile(getTempDir(), "# Heading");
|
|
16
|
+
|
|
17
|
+
const result = await runCli([inputFile]);
|
|
18
|
+
|
|
19
|
+
assert.equal(result.exitCode, 0);
|
|
20
|
+
assert.ok(result.stdout.includes("<title>Markdown document</title>"));
|
|
21
|
+
assert.match(result.stdout, /<h1[^>]*>Heading<\/h1>/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("fails with a clear error and exit 1 on a nonexistent input file", async () => {
|
|
25
|
+
const inputFile = join(getTempDir(), "does-not-exist.md");
|
|
26
|
+
|
|
27
|
+
const result = await runCli([inputFile]);
|
|
28
|
+
|
|
29
|
+
assert.equal(result.exitCode, 1);
|
|
30
|
+
assert.ok(result.stderr.includes(`Could not read input file '${inputFile}'`));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("uses the --title option for the output document's title", async () => {
|
|
34
|
+
const inputFile = await writeInputFile(getTempDir(), "# Heading");
|
|
35
|
+
|
|
36
|
+
const result = await runCli([inputFile, "--title", "My Title"]);
|
|
37
|
+
|
|
38
|
+
assert.ok(result.stdout.includes("<title>My Title</title>"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("uses the --css option to link a stylesheet in the output document", async () => {
|
|
42
|
+
const inputFile = await writeInputFile(getTempDir(), "# Heading");
|
|
43
|
+
|
|
44
|
+
const result = await runCli([inputFile, "--css", "style.css"]);
|
|
45
|
+
|
|
46
|
+
assert.ok(result.stdout.includes('<link rel="stylesheet" href="style.css">'));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("writes the rendered output to disk with -o, creating missing parent directories", async () => {
|
|
50
|
+
const dir = getTempDir();
|
|
51
|
+
const inputFile = await writeInputFile(dir, "# Heading");
|
|
52
|
+
const outputFile = join(dir, "nested", "output.html");
|
|
53
|
+
|
|
54
|
+
const result = await runCli([inputFile, "-o", outputFile]);
|
|
55
|
+
|
|
56
|
+
assert.equal(result.exitCode, 0);
|
|
57
|
+
const written = await readFile(outputFile, "utf-8");
|
|
58
|
+
assert.match(written, /<h1[^>]*>Heading<\/h1>/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("logs a confirmation after writing with -o by default", async () => {
|
|
62
|
+
const dir = getTempDir();
|
|
63
|
+
const inputFile = await writeInputFile(dir, "# Heading");
|
|
64
|
+
const outputFile = join(dir, "output.html");
|
|
65
|
+
|
|
66
|
+
const result = await runCli([inputFile, "-o", outputFile]);
|
|
67
|
+
|
|
68
|
+
assert.ok(result.stdout.includes(`Created: ${outputFile}`));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("suppresses the confirmation with -o and --quiet", async () => {
|
|
72
|
+
const dir = getTempDir();
|
|
73
|
+
const inputFile = await writeInputFile(dir, "# Heading");
|
|
74
|
+
const outputFile = join(dir, "output.html");
|
|
75
|
+
|
|
76
|
+
const result = await runCli([inputFile, "-o", outputFile, "-q"]);
|
|
77
|
+
|
|
78
|
+
assert.ok(!result.stdout.includes("Created:"));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("fails with a clear error and exit 1 when -o's directory is blocked by an existing file", async () => {
|
|
82
|
+
const dir = getTempDir();
|
|
83
|
+
const blockerFile = join(dir, "blocker");
|
|
84
|
+
await writeFile(blockerFile, "");
|
|
85
|
+
const inputFile = await writeInputFile(dir, "# Heading");
|
|
86
|
+
const outputFile = join(blockerFile, "output.html");
|
|
87
|
+
|
|
88
|
+
const result = await runCli([inputFile, "-o", outputFile]);
|
|
89
|
+
|
|
90
|
+
assert.equal(result.exitCode, 1);
|
|
91
|
+
assert.ok(result.stderr.includes(`Could not write output file '${outputFile}'`));
|
|
92
|
+
});
|
|
93
|
+
});
|
package/{src → lib}/index.js
RENAMED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import MarkdownIt from "markdown-it";
|
|
2
2
|
|
|
3
|
+
import abbr from "markdown-it-abbr";
|
|
3
4
|
import anchor from "markdown-it-anchor";
|
|
5
|
+
import critic from "markdown-it-critic";
|
|
4
6
|
import deflist from "markdown-it-deflist";
|
|
5
|
-
import sup from "markdown-it-sup";
|
|
6
|
-
import sub from "markdown-it-sub";
|
|
7
|
-
import abbr from "markdown-it-abbr";
|
|
8
7
|
import footnote from "markdown-it-footnote";
|
|
9
|
-
|
|
10
|
-
import
|
|
8
|
+
import sub from "markdown-it-sub";
|
|
9
|
+
import sup from "markdown-it-sup";
|
|
11
10
|
|
|
12
11
|
const parser = new MarkdownIt("commonmark");
|
|
13
12
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { parse, render } from "./index.js";
|
|
5
|
+
|
|
6
|
+
function inlineChildTypes(tokens) {
|
|
7
|
+
const inline = tokens.find((token) => token.type === "inline");
|
|
8
|
+
return inline.children.map((child) => child.type);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function topLevelTypes(tokens) {
|
|
12
|
+
return tokens.map((token) => token.type);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("extramark", () => {
|
|
16
|
+
describe("markdown-it-critic", () => {
|
|
17
|
+
it("renders CriticMarkup additions as <ins>", () => {
|
|
18
|
+
const input = "Lorem {++added++} dolor.";
|
|
19
|
+
const expectedOutput = "<p>Lorem <ins>added</ins> dolor.</p>\n";
|
|
20
|
+
|
|
21
|
+
assert.equal(render(input), expectedOutput);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("parses CriticMarkup additions as first-class tokens", () => {
|
|
25
|
+
const input = "Lorem {++added++} dolor.";
|
|
26
|
+
|
|
27
|
+
const childTypes = inlineChildTypes(parse(input, {}));
|
|
28
|
+
|
|
29
|
+
assert.ok(childTypes.includes("critic_ins_open"));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("markdown-it-abbr", () => {
|
|
34
|
+
it("renders abbreviations as <abbr>", () => {
|
|
35
|
+
const input = "*[HTML]: HyperText Markup Language\n\nHTML is great.";
|
|
36
|
+
const expectedOutput =
|
|
37
|
+
'<p><abbr title="HyperText Markup Language">HTML</abbr> is great.</p>\n';
|
|
38
|
+
|
|
39
|
+
assert.equal(render(input), expectedOutput);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("parses abbreviations as first-class tokens", () => {
|
|
43
|
+
const input = "*[HTML]: HyperText Markup Language\n\nHTML is great.";
|
|
44
|
+
|
|
45
|
+
const childTypes = inlineChildTypes(parse(input, {}));
|
|
46
|
+
|
|
47
|
+
assert.ok(childTypes.includes("abbr_open"));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("markdown-it-anchor", () => {
|
|
52
|
+
it("renders heading anchors with an id", () => {
|
|
53
|
+
const input = "# My Heading";
|
|
54
|
+
const expectedOutput = '<h1 id="my-heading" tabindex="-1">My Heading</h1>\n';
|
|
55
|
+
|
|
56
|
+
assert.equal(render(input), expectedOutput);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("parses headings with an id attribute", () => {
|
|
60
|
+
const input = "# My Heading";
|
|
61
|
+
|
|
62
|
+
const tokens = parse(input, {});
|
|
63
|
+
const heading = tokens.find((token) => token.type === "heading_open");
|
|
64
|
+
|
|
65
|
+
assert.equal(heading.attrGet("id"), "my-heading");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("markdown-it-deflist", () => {
|
|
70
|
+
it("renders definition lists as <dl>", () => {
|
|
71
|
+
const input = "Term\n: Definition";
|
|
72
|
+
const expectedOutput = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>\n";
|
|
73
|
+
|
|
74
|
+
assert.equal(render(input), expectedOutput);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("parses definition lists as first-class tokens", () => {
|
|
78
|
+
const input = "Term\n: Definition";
|
|
79
|
+
|
|
80
|
+
const types = topLevelTypes(parse(input, {}));
|
|
81
|
+
|
|
82
|
+
assert.ok(types.includes("dl_open"));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("markdown-it-footnote", () => {
|
|
87
|
+
it("renders footnotes with backrefs", () => {
|
|
88
|
+
const input = "Note.[^1]\n\n[^1]: Footnote text.";
|
|
89
|
+
|
|
90
|
+
const output = render(input);
|
|
91
|
+
|
|
92
|
+
assert.ok(output.includes('class="footnote-ref"'));
|
|
93
|
+
assert.ok(output.includes('class="footnote-item"'));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("parses footnote references as first-class tokens", () => {
|
|
97
|
+
const input = "Note.[^1]\n\n[^1]: Footnote text.";
|
|
98
|
+
|
|
99
|
+
const childTypes = inlineChildTypes(parse(input, {}));
|
|
100
|
+
|
|
101
|
+
assert.ok(childTypes.includes("footnote_ref"));
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("markdown-it-sub", () => {
|
|
106
|
+
it("renders subscript as <sub>", () => {
|
|
107
|
+
const input = "H~2~O";
|
|
108
|
+
const expectedOutput = "<p>H<sub>2</sub>O</p>\n";
|
|
109
|
+
|
|
110
|
+
assert.equal(render(input), expectedOutput);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("parses subscript as first-class tokens", () => {
|
|
114
|
+
const input = "H~2~O";
|
|
115
|
+
|
|
116
|
+
const childTypes = inlineChildTypes(parse(input, {}));
|
|
117
|
+
|
|
118
|
+
assert.ok(childTypes.includes("sub_open"));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("markdown-it-sup", () => {
|
|
123
|
+
it("renders superscript as <sup>", () => {
|
|
124
|
+
const input = "29^th^";
|
|
125
|
+
const expectedOutput = "<p>29<sup>th</sup></p>\n";
|
|
126
|
+
|
|
127
|
+
assert.equal(render(input), expectedOutput);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("parses superscript as first-class tokens", () => {
|
|
131
|
+
const input = "29^th^";
|
|
132
|
+
|
|
133
|
+
const childTypes = inlineChildTypes(parse(input, {}));
|
|
134
|
+
|
|
135
|
+
assert.ok(childTypes.includes("sup_open"));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("typographic replacements", () => {
|
|
140
|
+
it("renders (c)/--/... in their typographic form", () => {
|
|
141
|
+
const input = "(c) 2024";
|
|
142
|
+
const expectedOutput = "<p>© 2024</p>\n";
|
|
143
|
+
|
|
144
|
+
assert.equal(render(input), expectedOutput);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("parses replacements into the text token's content", () => {
|
|
148
|
+
const input = "(c) 2024";
|
|
149
|
+
|
|
150
|
+
const tokens = parse(input, {});
|
|
151
|
+
const inline = tokens.find((token) => token.type === "inline");
|
|
152
|
+
const text = inline.children.find((child) => child.type === "text");
|
|
153
|
+
|
|
154
|
+
assert.equal(text.content, "© 2024");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("table syntax", () => {
|
|
159
|
+
it("renders tables as <table>", () => {
|
|
160
|
+
const input = "| a |\n| - |\n| 1 |";
|
|
161
|
+
const expectedOutput =
|
|
162
|
+
"<table>\n<thead>\n<tr>\n<th>a</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>1</td>\n" +
|
|
163
|
+
"</tr>\n</tbody>\n</table>\n";
|
|
164
|
+
|
|
165
|
+
assert.equal(render(input), expectedOutput);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("parses tables as first-class tokens", () => {
|
|
169
|
+
const input = "| a |\n| - |\n| 1 |";
|
|
170
|
+
|
|
171
|
+
const types = topLevelTypes(parse(input, {}));
|
|
172
|
+
|
|
173
|
+
assert.ok(types.includes("table_open"));
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
package/package.json
CHANGED
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extramark",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CommonMark superset with
|
|
5
|
-
"author": "Márton Visnovitz <
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "CommonMark superset with widely used syntax extensions",
|
|
5
|
+
"author": "Márton Visnovitz <vimtaai@pm.me>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:vimtaai/extramark",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"main": "
|
|
9
|
+
"main": "lib/index.js",
|
|
10
10
|
"bin": "bin/extramark.js",
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"bin/**/*.js"
|
|
12
|
+
"{lib,bin}/**/*.js"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"test:watch": "node --watch --test",
|
|
17
|
+
"check": "biome check",
|
|
18
|
+
"check:ci": "biome ci",
|
|
19
|
+
"check:fix": "biome check --write"
|
|
19
20
|
},
|
|
20
21
|
"dependencies": {
|
|
21
|
-
"commander": "^
|
|
22
|
+
"commander": "^15.0.0",
|
|
22
23
|
"common-tags": "^1.8.2",
|
|
23
|
-
"
|
|
24
|
-
"markdown-it": "^
|
|
24
|
+
"markdown-it": "^15.0.2",
|
|
25
|
+
"markdown-it-critic": "^1.0.1",
|
|
25
26
|
"markdown-it-abbr": "^2.0.0",
|
|
26
|
-
"markdown-it-anchor": "^
|
|
27
|
-
"markdown-it-deflist": "^
|
|
27
|
+
"markdown-it-anchor": "^10.0.0",
|
|
28
|
+
"markdown-it-deflist": "^4.0.0",
|
|
28
29
|
"markdown-it-footnote": "^4.0.0",
|
|
29
30
|
"markdown-it-sub": "^2.0.0",
|
|
30
31
|
"markdown-it-sup": "^2.0.0"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"@semantic-release/changelog": "^6.0.3",
|
|
35
|
-
"@semantic-release/git": "^10.0.1",
|
|
36
|
-
"eslint": "^9.17.0",
|
|
37
|
-
"prettier": "^3.4.2",
|
|
38
|
-
"semantic-release": "^24.2.2"
|
|
34
|
+
"@biomejs/biome": "^2.4.2"
|
|
39
35
|
}
|
|
40
36
|
}
|
package/src/critic.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { render } from "critic-markup";
|
|
2
|
-
|
|
3
|
-
function criticMarkupRule(state) {
|
|
4
|
-
state.src = render(state.src);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function markdownItCriticMarkup(md) {
|
|
8
|
-
md.core.ruler.before("block", "critic-markup", criticMarkupRule);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default markdownItCriticMarkup;
|