fix-md-tables 1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JoobyPM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # fix-md-tables
2
+
3
+ Fix markdown table alignment for emoji using ideographic spaces (U+3000).
4
+
5
+ ## The Problem
6
+
7
+ Emoji display as 2 columns in terminals but count as 1 character. When Prettier formats markdown tables, it aligns by character count, so headers (no emoji) appear shorter than data rows (with emoji):
8
+
9
+ ```markdown
10
+ | Status | Description | Comments |
11
+ | ------ | -------------- | -------- |
12
+ | ✅ | ✅ Complete | ❌ |
13
+ | 🚧 | 🚧 In Progress | ⚠️ |
14
+ ```
15
+
16
+ Renders misaligned:
17
+
18
+ ```
19
+ | Status | Description | Comments |
20
+ | ------ | -------------- | -------- |
21
+ | ✅ | ✅ Complete | ❌ | ← emoji takes 2 cols but counts as 1 char
22
+ | 🚧 | 🚧 In Progress | ⚠️ |
23
+ ```
24
+
25
+ ## The Solution
26
+
27
+ Add ideographic spaces (U+3000) to cells with fewer emoji. Ideographic spaces also display as 2 columns, compensating for the width difference:
28
+
29
+ ```markdown
30
+ | Status  | Description  | Comments  |
31
+ | -------- | -------------- | ---------- |
32
+ | ✅ | ✅ Complete | ❌ |
33
+ | 🚧 | 🚧 In Progress | ⚠️ |
34
+ ```
35
+
36
+ Now renders aligned:
37
+
38
+ ```
39
+ | Status  | Description  | Comments  |
40
+ | ------  | --------------  | --------  |
41
+ | ✅ | ✅ Complete | ❌ |
42
+ | 🚧 | 🚧 In Progress | ⚠️ |
43
+ ```
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ # One-off via npx (no install)
49
+ npx fix-md-tables
50
+
51
+ # Or with bun
52
+ bunx fix-md-tables
53
+
54
+ # Install globally
55
+ npm install -g fix-md-tables
56
+ fix-md-tables
57
+
58
+ # As dev dependency in project
59
+ npm install -D fix-md-tables
60
+ pnpm add -D fix-md-tables
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ ### CLI
66
+
67
+ ```bash
68
+ # Process default files (*.md, *.mdx in root + docs/)
69
+ fix-md-tables
70
+
71
+ # Process specific files
72
+ fix-md-tables README.md docs/guide.mdx
73
+
74
+ # Via npx
75
+ npx fix-md-tables
76
+ ```
77
+
78
+ ### Programmatic
79
+
80
+ ```javascript
81
+ import { fixTableAlignment } from "fix-md-tables";
82
+
83
+ const markdown = `| Status | Description | Comments |
84
+ | ------ | -------------- | -------- |
85
+ | ✅ | ✅ Complete | ❌ |
86
+ | 🚧 | 🚧 In Progress | ⚠️ |`;
87
+
88
+ const fixed = fixTableAlignment(markdown);
89
+ console.log(fixed);
90
+ ```
91
+
92
+ ### With Prettier (recommended)
93
+
94
+ Run after Prettier to fix table alignment:
95
+
96
+ ```bash
97
+ prettier --write "*.md" "docs/**/*.md" && npx fix-md-tables
98
+ ```
99
+
100
+ Or in your `package.json`:
101
+
102
+ ```json
103
+ {
104
+ "scripts": {
105
+ "format": "prettier --write . && fix-md-tables"
106
+ }
107
+ }
108
+ ```
109
+
110
+ Or in a Makefile:
111
+
112
+ ```makefile
113
+ docs-format:
114
+ @prettier --write "*.md" "docs/**/*.md"
115
+ @npx fix-md-tables
116
+ ```
117
+
118
+ ## How It Works
119
+
120
+ 1. Finds all markdown tables in content
121
+ 2. For each table, calculates max emoji count per column (from data rows only)
122
+ 3. Adds ideographic spaces to compensate:
123
+ - Header cells: adds spaces after text
124
+ - Separator cells: removes dashes, adds ideographic spaces
125
+ - Data cells with fewer emoji: adds compensating spaces
126
+
127
+ ## API
128
+
129
+ ### `fixTableAlignment(content: string): string`
130
+
131
+ Main function to fix table alignment in markdown content.
132
+
133
+ ### `countEmoji(str: string): number`
134
+
135
+ Count emoji characters in a string.
136
+
137
+ ### `stripIdeographicSpaces(str: string): string`
138
+
139
+ Remove all ideographic spaces from a string.
140
+
141
+ ### `processTable(tableRows: string[]): string[]`
142
+
143
+ Process a complete table, applying emoji compensation to all rows.
144
+
145
+ ### `run(args?: string[]): number`
146
+
147
+ CLI runner. Returns count of fixed files.
148
+
149
+ ## Supported Emoji Ranges
150
+
151
+ - U+1F300-U+1F9FF (Miscellaneous Symbols and Pictographs, Emoticons, etc.)
152
+ - U+2600-U+26FF (Miscellaneous Symbols)
153
+ - U+2700-U+27BF (Dingbats)
154
+ - U+231A-U+23FA (Miscellaneous Technical)
155
+ - U+2B50-U+2B55 (Miscellaneous Symbols and Arrows)
156
+
157
+ ## File Types
158
+
159
+ Supports `.md` and `.mdx` files.
160
+
161
+ ## Requirements
162
+
163
+ - Node.js >= 18
164
+ - Also works with Bun
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point for fix-md-tables
4
+ *
5
+ * Usage: fix-md-tables [file.md|file.mdx...]
6
+ * npx fix-md-tables
7
+ * bunx fix-md-tables
8
+ */
9
+
10
+ import { run } from "../lib/index.mjs";
11
+
12
+ run(process.argv.slice(2));
13
+
package/lib/index.mjs ADDED
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Fix markdown/MDX table alignment for emoji by adding ideographic spaces (U+3000).
3
+ *
4
+ * Problem: Emoji display as 2 columns but count as 1 char. Prettier aligns by
5
+ * char count, so headers (no emoji) appear shorter than data rows (with emoji).
6
+ *
7
+ * Solution: Add ideographic spaces (U+3000, also 2 cols wide) to cells with
8
+ * fewer emoji to compensate. 1 ideographic space = 1 emoji worth of width.
9
+ *
10
+ * Compatible with Node.js and Bun. Supports both .md and .mdx files.
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+
16
+ // === Constants ===
17
+
18
+ export const IDEOGRAPHIC_SPACE = "\u3000"; // Displays as 2 columns, like emoji
19
+ export const EMOJI_REGEX = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{231A}-\u{23FA}]|[\u{2B50}-\u{2B55}]/gu;
20
+ export const SEPARATOR_REGEX = /^(\s*)(:*)(-+)(:*)(\s*)$/;
21
+ export const TABLE_SEPARATOR_LINE_REGEX = /^\s*\|[\s:|\-\u3000]+\|\s*$/;
22
+ export const MARKDOWN_EXTENSIONS = [".md", ".mdx"];
23
+
24
+ // === Pure Helper Functions ===
25
+
26
+ /** Count emoji characters in a string */
27
+ export function countEmoji(str) {
28
+ return (str.match(EMOJI_REGEX) || []).length;
29
+ }
30
+
31
+ /** Remove all ideographic spaces from a string */
32
+ export function stripIdeographicSpaces(str) {
33
+ return str.replaceAll("\u3000", "");
34
+ }
35
+
36
+ /** Check if a line is a markdown table separator */
37
+ export function isTableSeparatorLine(line) {
38
+ return TABLE_SEPARATOR_LINE_REGEX.test(line);
39
+ }
40
+
41
+ /** Check if a line is a table row (starts and ends with |) */
42
+ export function isTableRow(line) {
43
+ const trimmed = line.trim();
44
+ return trimmed.startsWith("|") && trimmed.endsWith("|");
45
+ }
46
+
47
+ /** Parse a table row into cells (preserving internal spacing) */
48
+ export function parseTableRow(row) {
49
+ const trimmed = row.trim();
50
+ const inner = trimmed.slice(1, -1); // Remove outer |
51
+ return inner.split("|");
52
+ }
53
+
54
+ /** Calculate max emoji count per column from data rows */
55
+ export function calculateMaxEmojiPerColumn(parsedRows, numCols) {
56
+ const maxEmojiPerCol = new Array(numCols).fill(0);
57
+
58
+ // Skip header (row 0) and separator (row 1), only look at data rows
59
+ for (let rowIdx = 2; rowIdx < parsedRows.length; rowIdx++) {
60
+ const row = parsedRows[rowIdx];
61
+ for (let col = 0; col < row.length; col++) {
62
+ const emojiCount = countEmoji(row[col] || "");
63
+ maxEmojiPerCol[col] = Math.max(maxEmojiPerCol[col], emojiCount);
64
+ }
65
+ }
66
+
67
+ return maxEmojiPerCol;
68
+ }
69
+
70
+ /** Compensate a separator cell by removing dashes and adding ideographic spaces */
71
+ export function compensateSeparatorCell(cell, compensation) {
72
+ const match = cell.match(SEPARATOR_REGEX);
73
+ if (!match) {
74
+ return cell;
75
+ }
76
+
77
+ const [, leadingSpace, leftColon, dashes, rightColon, trailingSpace] = match;
78
+ // Remove 2 dashes per ideographic space (both are 2 cols visually)
79
+ const dashesToRemove = compensation * 2;
80
+ const newDashes = dashes.length > dashesToRemove ? dashes.slice(0, -dashesToRemove) : dashes;
81
+
82
+ return leadingSpace + leftColon + newDashes + IDEOGRAPHIC_SPACE.repeat(compensation) + rightColon + trailingSpace;
83
+ }
84
+
85
+ /** Split cell into [leadingSpace, content, trailingSpace] without regex (ReDoS-safe) */
86
+ export function splitCellContent(cell) {
87
+ let leadingEnd = 0;
88
+ while (leadingEnd < cell.length && /\s/.test(cell[leadingEnd])) {
89
+ leadingEnd++;
90
+ }
91
+
92
+ let trailingStart = cell.length;
93
+ while (trailingStart > leadingEnd && /\s/.test(cell[trailingStart - 1])) {
94
+ trailingStart--;
95
+ }
96
+
97
+ return [cell.slice(0, leadingEnd), cell.slice(leadingEnd, trailingStart), cell.slice(trailingStart)];
98
+ }
99
+
100
+ /** Compensate a regular cell by adding ideographic spaces before trailing whitespace */
101
+ export function compensateRegularCell(cell, compensation) {
102
+ const [leadingSpace, content, trailingSpace] = splitCellContent(cell);
103
+ return leadingSpace + content + IDEOGRAPHIC_SPACE.repeat(compensation) + trailingSpace;
104
+ }
105
+
106
+ /** Process a single cell, applying emoji compensation */
107
+ export function processCell(cell, col, isSeparatorRow, maxEmojiPerCol) {
108
+ const maxEmoji = maxEmojiPerCol[col] || 0;
109
+ const cellEmoji = countEmoji(cell);
110
+ const compensation = maxEmoji - cellEmoji;
111
+
112
+ if (compensation <= 0) {
113
+ return cell;
114
+ }
115
+
116
+ return isSeparatorRow ? compensateSeparatorCell(cell, compensation) : compensateRegularCell(cell, compensation);
117
+ }
118
+
119
+ /** Build a table row string from cells */
120
+ export function buildTableRow(cells) {
121
+ return "|" + cells.join("|") + "|";
122
+ }
123
+
124
+ /** Process a complete table, applying emoji compensation to all rows */
125
+ export function processTable(tableRows) {
126
+ // Strip existing ideographic spaces to start fresh
127
+ const cleanedRows = tableRows.map(stripIdeographicSpaces);
128
+ const parsedRows = cleanedRows.map(parseTableRow);
129
+
130
+ // Check if table has any emoji
131
+ const hasEmoji = cleanedRows.some((row) => countEmoji(row) > 0);
132
+ if (!hasEmoji || parsedRows.length < 2) {
133
+ return cleanedRows;
134
+ }
135
+
136
+ const numCols = Math.max(...parsedRows.map((r) => r.length));
137
+ const maxEmojiPerCol = calculateMaxEmojiPerColumn(parsedRows, numCols);
138
+
139
+ // Rebuild all rows with compensation
140
+ return parsedRows.map((row, rowIdx) => {
141
+ const isSeparatorRow = rowIdx === 1;
142
+ const processedCells = row.map((cell, col) => processCell(cell, col, isSeparatorRow, maxEmojiPerCol));
143
+ return buildTableRow(processedCells);
144
+ });
145
+ }
146
+
147
+ /** Check if a line starts a fenced code block */
148
+ function isCodeFenceStart(line) {
149
+ const trimmed = line.trim();
150
+ return trimmed.startsWith("```") || trimmed.startsWith("~~~");
151
+ }
152
+
153
+ /** Check if a line ends a fenced code block */
154
+ function isCodeFenceEnd(line, fence) {
155
+ const trimmed = line.trim();
156
+ return trimmed === fence || trimmed.startsWith(fence);
157
+ }
158
+
159
+ /** Main function to fix table alignment in markdown content */
160
+ export function fixTableAlignment(content) {
161
+ const lines = content.split("\n");
162
+ const result = [];
163
+ let i = 0;
164
+ let inCodeBlock = false;
165
+ let codeFence = "";
166
+
167
+ while (i < lines.length) {
168
+ const line = lines[i];
169
+
170
+ // Track fenced code blocks to skip tables inside them
171
+ if (!inCodeBlock && isCodeFenceStart(line)) {
172
+ inCodeBlock = true;
173
+ codeFence = line.trim().match(/^(`{3,}|~{3,})/)[1];
174
+ result.push(line);
175
+ i++;
176
+ continue;
177
+ }
178
+
179
+ if (inCodeBlock) {
180
+ result.push(line);
181
+ if (isCodeFenceEnd(line, codeFence)) {
182
+ inCodeBlock = false;
183
+ codeFence = "";
184
+ }
185
+ i++;
186
+ continue;
187
+ }
188
+
189
+ // Detect table start: line with | followed by separator line
190
+ if (line.includes("|") && i + 1 < lines.length && isTableSeparatorLine(lines[i + 1])) {
191
+ const tableRows = [];
192
+
193
+ // Collect all table rows
194
+ while (i < lines.length && isTableRow(lines[i])) {
195
+ tableRows.push(lines[i]);
196
+ i++;
197
+ }
198
+
199
+ result.push(...processTable(tableRows));
200
+ } else {
201
+ result.push(line);
202
+ i++;
203
+ }
204
+ }
205
+
206
+ return result.join("\n");
207
+ }
208
+
209
+ // === File System Functions ===
210
+
211
+ /** Check if a filename has a markdown extension */
212
+ export function isMarkdownFile(filename) {
213
+ return MARKDOWN_EXTENSIONS.some((ext) => filename.endsWith(ext));
214
+ }
215
+
216
+ /** Recursively find all markdown/MDX files in a directory */
217
+ export function findMarkdownFiles(dir, files = []) {
218
+ let entries;
219
+ try {
220
+ entries = fs.readdirSync(dir, { withFileTypes: true });
221
+ } catch {
222
+ return files; // Directory not readable, skip silently
223
+ }
224
+
225
+ for (const entry of entries) {
226
+ const fullPath = path.join(dir, entry.name);
227
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
228
+ findMarkdownFiles(fullPath, files);
229
+ } else if (entry.isFile() && isMarkdownFile(entry.name)) {
230
+ files.push(fullPath);
231
+ }
232
+ }
233
+
234
+ return files;
235
+ }
236
+
237
+ /** Get default files to process (root .md/.mdx files + docs directory) */
238
+ export function getDefaultFiles(cwd) {
239
+ const files = [];
240
+
241
+ // Root markdown/MDX files
242
+ let rootEntries;
243
+ try {
244
+ rootEntries = fs.readdirSync(cwd, { withFileTypes: true });
245
+ } catch {
246
+ return files; // Can't read cwd, return empty
247
+ }
248
+
249
+ for (const entry of rootEntries) {
250
+ if (entry.isFile() && isMarkdownFile(entry.name)) {
251
+ files.push(path.join(cwd, entry.name));
252
+ }
253
+ }
254
+
255
+ // Docs directory
256
+ const docsDir = path.join(cwd, "docs");
257
+ if (fs.existsSync(docsDir)) {
258
+ findMarkdownFiles(docsDir, files);
259
+ }
260
+
261
+ return files;
262
+ }
263
+
264
+ /** Process a single file, applying table alignment fixes */
265
+ export function processFile(filePath) {
266
+ try {
267
+ const content = fs.readFileSync(filePath, "utf8");
268
+ const fixed = fixTableAlignment(content);
269
+
270
+ if (content !== fixed) {
271
+ fs.writeFileSync(filePath, fixed, "utf8");
272
+ console.log(` ✓ Fixed: ${filePath}`);
273
+ return true;
274
+ }
275
+ return false;
276
+ } catch (err) {
277
+ console.error(` ✗ Error processing ${filePath}: ${err.message}`);
278
+ return false;
279
+ }
280
+ }
281
+
282
+ /** Main CLI runner */
283
+ export function run(args = []) {
284
+ const files = args.length > 0 ? args : getDefaultFiles(process.cwd());
285
+
286
+ console.log(` Processing ${files.length} markdown/MDX file(s)...`);
287
+
288
+ let fixedCount = 0;
289
+ for (const file of files) {
290
+ if (processFile(file)) {
291
+ fixedCount++;
292
+ }
293
+ }
294
+
295
+ if (fixedCount > 0) {
296
+ console.log(` Fixed ${fixedCount} file(s) with ideographic space compensation.`);
297
+ }
298
+
299
+ return fixedCount;
300
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "fix-md-tables",
3
+ "version": "1.0.0",
4
+ "description": "Fix markdown table alignment for emoji using ideographic spaces",
5
+ "type": "module",
6
+ "bin": {
7
+ "fix-md-tables": "./bin/fix-md-tables.mjs"
8
+ },
9
+ "main": "./lib/index.mjs",
10
+ "exports": {
11
+ ".": "./lib/index.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "lib"
16
+ ],
17
+ "scripts": {
18
+ "check": "pnpm run format && pnpm run lint && pnpm run test",
19
+ "format": "prettier --write . && node bin/fix-md-tables.mjs",
20
+ "lint": "eslint .",
21
+ "lint:fix": "eslint . --fix",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "keywords": [
26
+ "markdown",
27
+ "table",
28
+ "emoji",
29
+ "alignment",
30
+ "prettier",
31
+ "formatter"
32
+ ],
33
+ "author": "",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/yourname/fix-md-tables"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "^9.39.2",
44
+ "eslint": "^9.39.2",
45
+ "globals": "^16.5.0",
46
+ "prettier": "^3.4.2",
47
+ "vitest": "^4.0.16"
48
+ },
49
+ "packageManager": "pnpm@9.15.4"
50
+ }