markdown-table-formatter 1.7.0 → 2.0.1-202611010112.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
@@ -13,7 +13,11 @@
13
13
  [![License](https://img.shields.io/npm/l/markdown-table-formatter.svg)](https://github.com/nvuillam/markdown-table-formatter/blob/master/LICENSE)
14
14
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
15
15
 
16
- Format markdown tables in files, using embedded [markdown-table-prettify](https://www.npmjs.com/package/markdown-table-prettify)
16
+ Format markdown tables in files, with a fast formatting engine written in Rust and embedded as WebAssembly.
17
+
18
+ No dependencies, the same npm package works on every platform (Windows, macOS, Linux, x64 and arm64) with Node.js 22.17+.
19
+
20
+ The formatting rules come from [markdown-table-prettify](https://github.com/darkriszty/MarkdownTablePrettify-VSCodeExt) 4.x, plus some fixes: tables in code blocks are left untouched and column widths follow the Unicode display width (CJK, kana, hangul, emoji...).
17
21
 
18
22
  [![Banner](https://raw.githubusercontent.com/nvuillam/markdown-table-formatter/master/docs/assets/images/banner.jpg)](https://github.com/nvuillam/markdown-table-formatter#readme)
19
23
 
@@ -26,7 +30,9 @@ Format markdown tables in files, using embedded [markdown-table-prettify](https:
26
30
  - [Options](#options)
27
31
  - [Examples](#examples)
28
32
  - [Continuous Integration](#continuous-integration)
33
+ - [pre-commit hook](#pre-commit-hook)
29
34
  - [Ignoring Code Blocks](#ignoring-code-blocks)
35
+ - [Debug](#debug)
30
36
  - [Contribute](#contribute)
31
37
  - [Release notes](#release-notes)
32
38
 
@@ -52,12 +58,13 @@ markdown-table-formatter [OPTIONS] (file(s) or glob expression)
52
58
 
53
59
  ### Options
54
60
 
55
- | Parameter | Description | Default value |
56
- |----------------|-----------------------------------|---------------|
57
- | -c<br/>--check | Check only, does not update files | `false` |
58
- | -p<br/>--columnpadding | Override column padding number of spaces |
59
- | -h<br/>--help | Displays help | <!-- --> |
60
- | -v<br/>--version | Displays version | <!-- --> |
61
+ | Parameter | Description | Default value |
62
+ |------------------------|------------------------------------------|---------------|
63
+ | -c<br/>--check | Check only, does not update files | `false` |
64
+ | -p<br/>--columnpadding | Override column padding number of spaces | `0` |
65
+ | -z<br/>--verbose | Verbose mode | `false` |
66
+ | -h<br/>--help | Displays help | <!-- --> |
67
+ | -v<br/>--version | Displays version | <!-- --> |
61
68
 
62
69
  ## Examples
63
70
 
@@ -103,12 +110,32 @@ You can see **more examples in** [**test methods**](https://github.com/nvuillam/
103
110
 
104
111
  You can automatically check markdown tables formatting and fix it using [MegaLinter](https://github.com/oxsecurity/megalinter/), that embeds markdown-table-formatter out of the box
105
112
 
113
+ ## pre-commit hook
114
+
115
+ Format markdown tables of committed files with [pre-commit](https://pre-commit.com), by adding to `.pre-commit-config.yaml`:
116
+
117
+ ```yaml
118
+ repos:
119
+ - repo: https://github.com/nvuillam/markdown-table-formatter
120
+ rev: v2.0.0
121
+ hooks:
122
+ - id: markdown-table-formatter
123
+ ```
124
+
125
+ The hook formats tables of staged markdown files in place: the commit stops when a file was updated, so you can review and stage the changes.
126
+
106
127
  ## Ignoring Code Blocks
107
128
 
108
129
  You may wish to ignore specific markdown content, especially when using with [Megalinter](https://github.com/oxsecurity/megalinter) or [terraform-docs](https://github.com/terraform-docs/terraform-docs)
109
130
 
110
131
  To achieve this, place tags `<!-- markdown-table-prettify-ignore-start -->` and `<!-- markdown-table-prettify-ignore-end -->` around the content you want markdown-table-formatter to ignore.
111
132
 
133
+ Tables inside fenced code blocks (` ``` ` or `~~~`) and indented code blocks are never formatted.
134
+
135
+ ## Debug
136
+
137
+ Set environment variable `NODE_DEBUG=markdown-table-formatter` to display debug logs.
138
+
112
139
  ## Contribute
113
140
 
114
141
  Contributions are very welcome !
package/lib/cli.js CHANGED
@@ -1,7 +1,26 @@
1
1
  #! /usr/bin/env node
2
2
  const { MarkdownTableFormatter } = require("./markdown-table-formatter");
3
3
  const optionsDefinition = require("./options");
4
- const glob = require("glob");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const EXCLUDED_DIRS = new Set(["node_modules", ".git"]);
8
+
9
+ /**
10
+ * Lists files matching a glob pattern, skipping node_modules and .git folders
11
+ * @param {string} pattern - Glob pattern
12
+ * @return {string[]} - Sorted file paths
13
+ */
14
+ function findFiles(pattern) {
15
+ const exclude = (entry) =>
16
+ EXCLUDED_DIRS.has(
17
+ path.basename(typeof entry === "string" ? entry : entry.name),
18
+ );
19
+ return fs
20
+ .globSync(pattern, { exclude })
21
+ .filter((file) => fs.statSync(file).isFile())
22
+ .sort();
23
+ }
5
24
 
6
25
  class MarkdownTableFormatterCli {
7
26
  "use strict";
@@ -13,16 +32,7 @@ class MarkdownTableFormatterCli {
13
32
 
14
33
  // Show version
15
34
  if (this.options.version) {
16
- let version = process.env.npm_package_version;
17
- if (!version) {
18
- try {
19
- const FindPackageJson = require("find-package-json");
20
- const finder = FindPackageJson(__dirname);
21
- version = finder.next().value.version;
22
- } catch {
23
- version = "error";
24
- }
25
- }
35
+ const version = require("../package.json").version;
26
36
  const message = `markdown-table-formatter version ${version}`;
27
37
  console.log(message);
28
38
  return { stdout: message };
@@ -47,16 +57,16 @@ class MarkdownTableFormatterCli {
47
57
  let files = this.options._;
48
58
  if (files.length === 0) {
49
59
  // use default glob expression **/*.md
50
- files = glob.sync("**/*.md", {});
60
+ files = findFiles("**/*.md");
51
61
  } else if (files.length === 1 && files[0].includes("*")) {
52
62
  // use sent glob expression
53
- files = glob.sync(files[0], {});
63
+ files = findFiles(files[0]);
54
64
  }
55
65
  const { status, updates } = await formatter.run(files, this.options);
56
66
  // Manage result
57
67
  if (status === 0 && updates.length === 0) {
58
68
  console.log("No markdown table formatting has been applied.");
59
- } else if (status === 0 && updates > 0) {
69
+ } else if (status === 0 && updates.length > 0) {
60
70
  console.log(
61
71
  `Formatted markdown tables in ${
62
72
  updates.length
@@ -74,4 +84,4 @@ class MarkdownTableFormatterCli {
74
84
  }
75
85
  }
76
86
 
77
- module.exports = { MarkdownTableFormatterCli };
87
+ module.exports = { MarkdownTableFormatterCli, findFiles };
package/lib/engine.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ // Loads the Rust formatting engine compiled to WebAssembly (lib/wasm, built by `npm run build:wasm`)
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const WASM_FILE = path.join(__dirname, "wasm", "markdown_table_formatter.wasm");
7
+
8
+ let wasm = null;
9
+
10
+ function getWasm() {
11
+ if (wasm === null) {
12
+ if (!fs.existsSync(WASM_FILE)) {
13
+ throw new Error(`${WASM_FILE} not found: run "npm run build:wasm"`);
14
+ }
15
+ const module = new WebAssembly.Module(fs.readFileSync(WASM_FILE));
16
+ wasm = new WebAssembly.Instance(module, {}).exports;
17
+ }
18
+ return wasm;
19
+ }
20
+
21
+ /**
22
+ * Formats all markdown tables found in a markdown string
23
+ * @param {string} text - Markdown content
24
+ * @param {number} [columnPadding] - Number of spaces around cell values
25
+ * @return {string} - Formatted markdown content
26
+ */
27
+ function formatTables(text, columnPadding = 0) {
28
+ const engine = getWasm();
29
+ const input = Buffer.from(text, "utf8");
30
+ const inputPtr = engine.mtf_alloc(input.length);
31
+ new Uint8Array(engine.memory.buffer, inputPtr, input.length).set(input);
32
+ // mtf_format frees the input buffer
33
+ const outputPtr = engine.mtf_format(inputPtr, input.length, columnPadding);
34
+ const outputLength = engine.mtf_result_len();
35
+ const output = Buffer.from(
36
+ engine.memory.buffer,
37
+ outputPtr,
38
+ outputLength,
39
+ ).toString("utf8");
40
+ engine.mtf_free_result();
41
+ return output;
42
+ }
43
+
44
+ module.exports = { formatTables };
package/lib/index.js CHANGED
@@ -10,7 +10,12 @@ const runningAsScript = !module.parent;
10
10
  // Run asynchronously to use the returned status for process.exit
11
11
  if (runningAsScript) {
12
12
  (async () => {
13
- await new MarkdownTableFormatterCli().run(process.argv);
13
+ try {
14
+ await new MarkdownTableFormatterCli().run(process.argv);
15
+ } catch (error) {
16
+ console.error(`markdown-table-formatter: ${error.message}`);
17
+ process.exitCode = 1;
18
+ }
14
19
  })();
15
20
  }
16
21
 
@@ -1,7 +1,7 @@
1
1
  #! /usr/bin/env node
2
- const debug = require("debug")("markdown-table-formatter");
3
- const fse = require("fs-extra");
4
- const { CliPrettify } = require("markdown-table-prettify");
2
+ const fs = require("fs/promises");
3
+ const debug = require("util").debuglog("markdown-table-formatter");
4
+ const { formatTables } = require("./engine");
5
5
 
6
6
  class MarkdownTableFormatter {
7
7
  "use strict";
@@ -15,6 +15,17 @@ class MarkdownTableFormatter {
15
15
  this.checkOnly = opts.check;
16
16
  }
17
17
 
18
+ /**
19
+ * Formats all markdown tables found in a markdown string
20
+ * @param {string} markdownString - Markdown content
21
+ * @param {object} [options] - Format options
22
+ * @param {number} [options.columnpadding] - Number of spaces around cell values
23
+ * @return {string} - Formatted markdown content
24
+ */
25
+ static formatText(markdownString, options = {}) {
26
+ return formatTables(markdownString, options.columnpadding || 0);
27
+ }
28
+
18
29
  /**
19
30
  * Runs markdown table formatter
20
31
  * @param {string[]} [files] - List of files to format
@@ -33,23 +44,19 @@ class MarkdownTableFormatter {
33
44
  if (debug.enabled) {
34
45
  debug(`[file] ${file}`);
35
46
  }
36
- const markdown = await fse.readFile(file);
47
+ const markdown = await fs.readFile(file);
37
48
  const markdownString = markdown.toString();
38
49
  if (debug.enabled) {
39
50
  debug(`[file before]\n${markdownString}`);
40
51
  }
41
- const prettifyOptions = {};
42
- if (options.columnpadding) {
43
- prettifyOptions.columnPadding = options.columnpadding;
44
- }
45
- const markdownStringPrettified = CliPrettify.prettify(
52
+ const markdownStringPrettified = MarkdownTableFormatter.formatText(
46
53
  markdownString,
47
- prettifyOptions,
54
+ options,
48
55
  );
49
56
  const updated = markdownStringPrettified !== markdownString;
50
57
  // Update file if necessary
51
58
  if (updated && !this.checkOnly) {
52
- await fse.writeFile(file, markdownStringPrettified);
59
+ await fs.writeFile(file, markdownStringPrettified);
53
60
  debug(`[updated] ${file} with:\n${markdownStringPrettified}`);
54
61
  if (options.verbose) {
55
62
  console.info(`- Updated file ${file}`);
package/lib/options.js CHANGED
@@ -1,59 +1,184 @@
1
1
  /**
2
- * @fileoverview Options configuration for optionator.
2
+ * @fileoverview Command line options parsing (same behavior and messages as optionator, without dependency)
3
3
  * @author Nicolas Vuillamy
4
4
  */
5
5
 
6
6
  "use strict";
7
7
 
8
- //------------------------------------------------------------------------------
9
- // Requirements
10
- //------------------------------------------------------------------------------
8
+ const PREPEND = "markdown-table-formatter [options]";
11
9
 
12
- const optionator = require("optionator");
10
+ const OPTIONS = [
11
+ {
12
+ option: "check",
13
+ alias: "c",
14
+ type: "Boolean",
15
+ description:
16
+ "Check only (exits 1 if wrongly formatted markdown tables are found)",
17
+ },
18
+ {
19
+ option: "columnpadding",
20
+ alias: "p",
21
+ type: "Int",
22
+ description: "Override table columns padding number of spaces",
23
+ },
24
+ { option: "help", alias: "h", type: "Boolean", description: "Show help" },
25
+ {
26
+ option: "version",
27
+ alias: "v",
28
+ type: "Boolean",
29
+ description: "Show version",
30
+ },
31
+ {
32
+ option: "verbose",
33
+ alias: "z",
34
+ type: "Boolean",
35
+ description: "Verbose mode",
36
+ },
37
+ ];
13
38
 
14
- //------------------------------------------------------------------------------
15
- // Initialization and Public Interface
16
- //------------------------------------------------------------------------------
39
+ const MUTUALLY_EXCLUSIVE = [["check", "help", "version"]];
17
40
 
18
- // exports "parse(args)", "generateHelp()", and "generateHelpForOption(optionName)"
19
- module.exports = optionator({
20
- prepend: "markdown-table-formatter [options]",
21
- defaults: {
22
- concatRepeatedArrays: true,
23
- mergeRepeatedObjects: true,
24
- },
25
- options: [
26
- {
27
- option: "check",
28
- alias: "c",
29
- type: "Boolean",
30
- description:
31
- "Check only (exits 1 if wrongly formatted markdown tables are found)",
32
- },
33
- {
34
- option: "columnpadding",
35
- alias: "p",
36
- type: "Int",
37
- description: "Override table columns padding number of spaces",
38
- },
39
- {
40
- option: "help",
41
- alias: "h",
42
- type: "Boolean",
43
- description: "Show help",
44
- },
45
- {
46
- option: "version",
47
- alias: "v",
48
- type: "Boolean",
49
- description: "Show version",
50
- },
51
- {
52
- option: "verbose",
53
- alias: "z",
54
- type: "Boolean",
55
- description: "Verbose mode",
56
- },
57
- ],
58
- mutuallyExclusive: [["check", "help", "version"]],
59
- });
41
+ const byName = (name) =>
42
+ OPTIONS.find((opt) => opt.option === name || opt.alias === name);
43
+ const flag = (name) => (name.length === 1 ? `-${name}` : `--${name}`);
44
+ const label = (opt) =>
45
+ `-${opt.alias}, --${opt.option}${opt.type === "Boolean" ? "" : ` ${opt.type}`}`;
46
+
47
+ function levenshtein(a, b) {
48
+ let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
49
+ for (let i = 1; i <= a.length; i++) {
50
+ const current = [i];
51
+ for (let j = 1; j <= b.length; j++) {
52
+ current[j] = Math.min(
53
+ previous[j] + 1,
54
+ current[j - 1] + 1,
55
+ previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
56
+ );
57
+ }
58
+ previous = current;
59
+ }
60
+ return previous[b.length];
61
+ }
62
+
63
+ function invalidOption(name) {
64
+ const candidates = OPTIONS.flatMap((opt) => [opt.option, opt.alias]);
65
+ const closest = candidates.reduce((best, candidate) =>
66
+ levenshtein(name, candidate) < levenshtein(name, best) ? candidate : best,
67
+ );
68
+ return new Error(
69
+ `Invalid option '${flag(name)}' - perhaps you meant '${flag(closest)}'?`,
70
+ );
71
+ }
72
+
73
+ function convert(opt, value) {
74
+ if (opt.type === "Int") {
75
+ if (value === undefined) {
76
+ throw new Error(`Value for '${opt.option}' of type 'Int' required.`);
77
+ }
78
+ if (!/^\d+$/.test(value)) {
79
+ throw new Error(
80
+ `Invalid value for option '${opt.option}' - expected type Int, received value: ${value}.`,
81
+ );
82
+ }
83
+ return parseInt(value, 10);
84
+ }
85
+ if (value === undefined || value === "true") {
86
+ return true;
87
+ }
88
+ if (value === "false") {
89
+ return false;
90
+ }
91
+ throw new Error(
92
+ `Invalid value for option '${opt.option}' - expected type Boolean, received value: ${value}.`,
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Parses command line arguments
98
+ * @param {string[]} input - process.argv-like array (the first 2 items are ignored)
99
+ * @return {object} - Option values, and positional arguments in `_`
100
+ */
101
+ function parse(input) {
102
+ const args = input.slice(2);
103
+ const result = {};
104
+ const positional = [];
105
+ for (let i = 0; i < args.length; i++) {
106
+ const arg = String(args[i]);
107
+ if (arg === "--") {
108
+ positional.push(...args.slice(i + 1));
109
+ break;
110
+ }
111
+ if (arg.startsWith("--")) {
112
+ const [rawName, ...valueParts] = arg.slice(2).split("=");
113
+ let value = valueParts.length > 0 ? valueParts.join("=") : undefined;
114
+ let opt = byName(rawName);
115
+ if (
116
+ !opt &&
117
+ rawName.startsWith("no-") &&
118
+ byName(rawName.slice(3))?.type === "Boolean" &&
119
+ value === undefined
120
+ ) {
121
+ opt = byName(rawName.slice(3));
122
+ value = "false";
123
+ }
124
+ if (!opt || opt.option !== rawName.replace(/^no-/, "")) {
125
+ throw invalidOption(rawName);
126
+ }
127
+ if (opt.type !== "Boolean" && value === undefined) {
128
+ value = args[++i];
129
+ }
130
+ result[opt.option] = convert(opt, value);
131
+ } else if (arg.startsWith("-") && arg.length > 1) {
132
+ const names = arg.slice(1).split("");
133
+ names.forEach((name, index) => {
134
+ const opt = byName(name);
135
+ if (!opt || opt.alias !== name) {
136
+ throw /\d/.test(name)
137
+ ? new Error(`No -NUM option defined.`)
138
+ : invalidOption(name);
139
+ }
140
+ if (opt.type !== "Boolean") {
141
+ if (index !== names.length - 1) {
142
+ throw new Error(
143
+ `Can't set argument '${name}' when not last flag in a group of short flags.`,
144
+ );
145
+ }
146
+ result[opt.option] = convert(opt, args[++i]);
147
+ } else {
148
+ result[opt.option] = true;
149
+ }
150
+ });
151
+ } else {
152
+ positional.push(arg);
153
+ }
154
+ }
155
+ for (const group of MUTUALLY_EXCLUSIVE) {
156
+ const used = group.filter((name) => name in result);
157
+ if (used.length > 1) {
158
+ throw new Error(
159
+ `The options ${used.map((name) => `--${name}`).join(" and ")} are mutually exclusive - you cannot use them at the same time.`,
160
+ );
161
+ }
162
+ }
163
+ result._ = positional;
164
+ return result;
165
+ }
166
+
167
+ function generateHelp() {
168
+ const width = Math.max(...OPTIONS.map((opt) => label(opt).length));
169
+ const lines = OPTIONS.map(
170
+ (opt) => ` ${label(opt).padEnd(width)} ${opt.description}`,
171
+ );
172
+ return `${PREPEND}\n\n${lines.join("\n")}`;
173
+ }
174
+
175
+ function generateHelpForOption(name) {
176
+ const opt = byName(name);
177
+ if (!opt) {
178
+ return invalidOption(name).message;
179
+ }
180
+ const title = label(opt);
181
+ return `${title}\n${"=".repeat(title.length)}\ndescription: ${opt.description}.`;
182
+ }
183
+
184
+ module.exports = { parse, generateHelp, generateHelpForOption };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "markdown-table-formatter",
3
- "version": "1.7.0",
4
- "description": "Format markdown tables in files",
3
+ "version": "2.0.1-202611010112.0",
4
+ "description": "Format markdown tables in files (Rust engine compiled to WebAssembly, no dependencies)",
5
5
  "main": "./lib/index.js",
6
6
  "files": [
7
7
  "lib/"
@@ -10,10 +10,15 @@
10
10
  "markdown-table-formatter": "lib/index.js"
11
11
  },
12
12
  "scripts": {
13
- "lint:fix": "eslint **/*.js --fix && prettier --write \"./lib/**/*.{js,jsx,json}\" --tab-width 4 --print-width 150",
14
- "test": "mocha \"test/**/*.test.js\"",
15
- "test:coverage": "nyc npm run test",
16
- "test:debug": "env DEBUG=markdown-table-formatter mocha --reporter spec \"test/**/*.test.js\""
13
+ "build:wasm": "node scripts/build-wasm.js",
14
+ "lint:fix": "eslint --fix && prettier --write \"./lib/**/*.{js,jsx,json}\" --tab-width 4 --print-width 150",
15
+ "prepack": "node scripts/build-wasm.js --if-needed",
16
+ "pretest": "npm run build:wasm",
17
+ "test": "node --test --test-timeout=300000 \"test/**/*.test.js\"",
18
+ "pretest:coverage": "npm run build:wasm",
19
+ "test:coverage": "node --test --test-timeout=300000 --experimental-test-coverage --test-coverage-include=\"lib/**\" \"test/**/*.test.js\"",
20
+ "test:rust": "cargo test --manifest-path rust/Cargo.toml",
21
+ "test:debug": "env NODE_DEBUG=markdown-table-formatter node --test --test-timeout=300000 --test-reporter=spec \"test/**/*.test.js\""
17
22
  },
18
23
  "repository": {
19
24
  "type": "git",
@@ -32,46 +37,13 @@
32
37
  "url": "https://github.com/nvuillam/markdown-table-formatter/issues"
33
38
  },
34
39
  "homepage": "https://github.com/nvuillam/markdown-table-formatter#readme",
35
- "dependencies": {
36
- "debug": "^4.3.4",
37
- "find-package-json": "^1.2.0",
38
- "fs-extra": "^11.1.1",
39
- "glob": "^13.0.0",
40
- "markdown-table-prettify": "^3.6.0",
41
- "optionator": "^0.9.4"
42
- },
43
40
  "devDependencies": {
44
- "@babel/eslint-parser": "^7.23.3",
45
- "eslint": "^9.0.0",
46
- "mega-linter-runner": "^9.0.0",
47
- "mocha": "^11.0.0",
48
- "nyc": "^17.0.0",
49
- "which": "^6.0.0"
41
+ "eslint": "^10.14.0"
50
42
  },
51
43
  "engines": {
52
- "node": ">=18.0.0"
53
- },
54
- "mocha": {
55
- "require": [
56
- "test/helpers/init.js"
57
- ],
58
- "watch-extensions": [
59
- "js"
60
- ],
61
- "recursive": true,
62
- "reporter": "spec",
63
- "timeout": "300000"
44
+ "node": ">=22.17.0"
64
45
  },
65
- "nyc": {
66
- "include": [
67
- "lib/**"
68
- ],
69
- "extension": [
70
- ".js"
71
- ],
72
- "reporter": [
73
- "html"
74
- ],
75
- "all": true
46
+ "overrides": {
47
+ "minimatch": "^11.0.1"
76
48
  }
77
49
  }