extramark 2.1.0 → 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/bin/extramark.js CHANGED
@@ -41,8 +41,8 @@ try {
41
41
  data.parsed = html`
42
42
  <!DOCTYPE html>
43
43
  <meta charset="utf-8" />
44
- <title>${program.title || `Markdown document`}</title>
45
- ${program.css ? `<link rel="stylesheet" href="${program.css}">` : ``} ${await render(data.raw)}
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.`);
@@ -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/lib/index.js CHANGED
@@ -2,13 +2,12 @@ import MarkdownIt from "markdown-it";
2
2
 
3
3
  import abbr from "markdown-it-abbr";
4
4
  import anchor from "markdown-it-anchor";
5
+ import critic from "markdown-it-critic";
5
6
  import deflist from "markdown-it-deflist";
6
7
  import footnote from "markdown-it-footnote";
7
8
  import sub from "markdown-it-sub";
8
9
  import sup from "markdown-it-sup";
9
10
 
10
- import critic from "./critic.js";
11
-
12
11
  const parser = new MarkdownIt("commonmark");
13
12
 
14
13
  parser.set({ typographer: true });
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "extramark",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "CommonMark superset with widely used syntax extensions",
5
5
  "author": "Márton Visnovitz <vimtaai@pm.me>",
6
6
  "license": "MIT",
@@ -12,18 +12,20 @@
12
12
  "{lib,bin}/**/*.js"
13
13
  ],
14
14
  "scripts": {
15
+ "test": "node --test",
16
+ "test:watch": "node --watch --test",
15
17
  "check": "biome check",
16
18
  "check:ci": "biome ci",
17
19
  "check:fix": "biome check --write"
18
20
  },
19
21
  "dependencies": {
20
- "commander": "^14.0.3",
22
+ "commander": "^15.0.0",
21
23
  "common-tags": "^1.8.2",
22
- "critic-markup": "^2.0.1",
23
- "markdown-it": "^14.1.0",
24
+ "markdown-it": "^15.0.2",
25
+ "markdown-it-critic": "^1.0.1",
24
26
  "markdown-it-abbr": "^2.0.0",
25
- "markdown-it-anchor": "^9.2.0",
26
- "markdown-it-deflist": "^3.0.0",
27
+ "markdown-it-anchor": "^10.0.0",
28
+ "markdown-it-deflist": "^4.0.0",
27
29
  "markdown-it-footnote": "^4.0.0",
28
30
  "markdown-it-sub": "^2.0.0",
29
31
  "markdown-it-sup": "^2.0.0"
package/lib/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;