@franlol/opencode-md-table-formatter 0.0.2 → 0.0.4

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.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +88 -97
  3. package/index.ts +235 -230
  4. package/package.json +41 -41
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 franlol
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2025 franlol
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 CHANGED
@@ -1,97 +1,88 @@
1
- # @franlol/opencode-mdtf
2
-
3
- Markdown table formatter plugin for OpenCode with concealment mode support.
4
-
5
- ## Features
6
-
7
- - **Automatic table formatting** - Formats markdown tables after AI text completion
8
- - ✅ **Concealment mode compatible** - Correctly calculates column widths when markdown symbols are hidden
9
- - ✅ **Alignment support** - Left (`:---`), center (`:---:`), and right (`---:`) text alignment
10
- - ✅ **Nested markdown handling** - Strips bold, italic, strikethrough with multi-pass algorithm
11
- - ✅ **Code block preservation** - Preserves markdown symbols inside inline code (`` `**bold**` ``)
12
- - ✅ **Edge case handling** - Emojis, unicode characters, empty cells, long content
13
- - ✅ **Silent operation** - No console logs, errors don't interrupt workflow
14
- - ✅ **Validation feedback** - Invalid tables get helpful error comments
15
-
16
- ## Usage
17
-
18
- Add the plugin to your `.opencode/opencode.jsonc`:
19
-
20
- ```jsonc
21
- {
22
- "plugin": ["@franlol/opencode-mdtf"],
23
- }
24
- ```
25
-
26
- Restart OpenCode. Tables in AI responses will now be automatically formatted!
27
-
28
- ## Example
29
-
30
- The plugin handles complex edge cases with concealment mode enabled:
31
-
32
- **Input** (unformatted table with nested markdown):
33
-
34
- ```
35
- | Feature | Description | Status |
36
- |---|---|
37
- | **Bold text** | Has *italic* content | ✅ Done |
38
- | `Code` | With `**bold**` inside | Progress |
39
- | Unicode | Greek α β γ | 💡 Idea |
40
- ```
41
-
42
- **Output** (automatically formatted):
43
-
44
- ```
45
- | Feature | Description | Status |
46
- | ------------- | ---------------------- | ----------- |
47
- | **Bold text** | Has *italic* content | ✅ Done |
48
- | `Code` | With `**bold**` inside | ⏳ Progress |
49
- | Unicode | Greek α β γ | 💡 Idea |
50
- ```
51
-
52
- **Key behaviors:**
53
-
54
- - Bold/italic symbols outside code are hidden by concealment but width calculated correctly
55
- - `**bold**` inside backticks shows as literal text with proper spacing
56
- - Emojis and unicode characters align properly
57
- - Columns have consistent spacing
58
-
59
- ## How It Works
60
-
61
- This plugin uses OpenCode's `text.complete` hook to format markdown tables after the AI finishes generating text. It intelligently strips markdown symbols (for width calculation) while preserving symbols inside inline code blocks, ensuring tables align correctly even with OpenCode's concealment mode enabled (the default setting).
62
-
63
- The plugin uses a multi-pass regex algorithm to handle nested markdown (like `**bold with `code` inside**`) and caches width calculations for performance.
64
-
65
- ## Troubleshooting
66
-
67
- **Tables not formatting?**
68
-
69
- - Ensure the plugin is listed in your `.opencode/opencode.jsonc` config
70
- - Restart OpenCode after adding the plugin
71
- - Check that tables have a separator row (`|---|---|`)
72
-
73
- **Excessive spacing in columns?**
74
-
75
- - This is fixed in v0.1.0! The plugin now preserves markdown inside inline code correctly.
76
-
77
- **Invalid table structure comment?**
78
-
79
- - The plugin validates tables before formatting
80
- - All rows must have the same number of columns
81
- - Tables must have at least 2 rows including the separator
82
-
83
- ## Requirements
84
-
85
- - OpenCode CLI
86
- - Node.js >= 18.0.0 or Bun runtime
87
- - `@opencode-ai/plugin` >= 0.13.7
88
-
89
- ## License
90
-
91
- MIT © franlol
92
-
93
- ## Links
94
-
95
- - [GitHub Repository](https://github.com/franlol/opencode-mdtf)
96
- - [npm Package](https://www.npmjs.com/package/@franlol/opencode-mdtf)
97
- - [Report Issues](https://github.com/franlol/opencode-mdtf/issues)
1
+ # @franlol/opencode-md-table-formatter
2
+
3
+ Markdown table formatter plugin for Opencode with concealment mode support.
4
+
5
+ ## Usage
6
+
7
+ Add the plugin to your `.opencode/opencode.jsonc`:
8
+
9
+ ```jsonc
10
+ {
11
+ "plugin": ["@franlol/opencode-md-table-formatter@latest"],
12
+ }
13
+ ```
14
+
15
+ ## Example
16
+
17
+ <table>
18
+ <tr>
19
+ <th style="text-align:center;">Original</th>
20
+ <th style="text-align:center;">Formatted</th>
21
+ </tr>
22
+ <tr>
23
+ <td>
24
+ <img src="https://github.com/user-attachments/assets/df71e950-c15d-4a10-8e08-fdd9b0216ba0"
25
+ alt="Screenshot 1"
26
+ style="height:250px; object-fit:cover;">
27
+ </td>
28
+ <td>
29
+ <img src="https://github.com/user-attachments/assets/c6f253e0-350f-487e-8da7-c8c6e8b2cb93"
30
+ alt="Screenshot 2"
31
+ style="height:250px; object-fit:cover;">
32
+ </td>
33
+ </tr>
34
+ </table>
35
+
36
+ ## Features
37
+
38
+ - **Automatic table formatting** - Formats markdown tables after AI text completion
39
+ - **Concealment mode compatible** - Correctly calculates column widths when markdown symbols are hidden
40
+ - **Alignment support** - Left (`:---`), center (`:---:`), and right (`---:`) text alignment
41
+ - **Nested markdown handling** - Strips bold, italic, strikethrough with multi-pass algorithm
42
+ - **Code block preservation** - Preserves markdown symbols inside inline code (`` `**bold**` ``)
43
+ - **Edge case handling** - Emojis, unicode characters, empty cells, long content
44
+ - **Silent operation** - No console logs, errors don't interrupt workflow
45
+ - **Validation feedback** - Invalid tables get helpful error comments
46
+
47
+
48
+ **Key behaviors:**
49
+
50
+ - Bold/italic symbols outside code are hidden by concealment but width calculated correctly
51
+ - `**bold**` inside backticks shows as literal text with proper spacing
52
+ - Emojis and unicode characters align properly
53
+ - Columns have consistent spacing
54
+
55
+ ## How It Works
56
+
57
+ This plugin uses Opencode's `experimental.text.complete` hook to format markdown tables after the AI finishes generating text. It intelligently strips markdown symbols (for width calculation) while preserving symbols inside inline code blocks, ensuring tables align correctly ONLY with Opencode's concealment mode enabled (the default setting).
58
+
59
+ The plugin uses a multi-pass regex algorithm to handle nested markdown (like `**bold with `code` inside**`) and caches width calculations for performance.
60
+
61
+ ## Troubleshooting
62
+
63
+ **Tables not formatting?**
64
+
65
+ - Ensure the plugin is listed in your `.opencode/opencode.jsonc` config
66
+ - Restart Opencode after adding the plugin
67
+ - Check that tables have a separator row (`|---|---|`)
68
+
69
+
70
+ **Invalid table structure comment?**
71
+
72
+ - The plugin validates tables before formatting
73
+ - All rows must have the same number of columns
74
+ - Tables must have at least 2 rows including the separator
75
+
76
+ ## Requirements
77
+
78
+ - Opencode >= 1.0.137
79
+
80
+ ## License
81
+
82
+ MIT © franlol
83
+
84
+ ## Links
85
+
86
+ - [GitHub Repository](https://github.com/franlol/opencode-md-table-formatter)
87
+ - [npm Package](https://www.npmjs.com/package/@franlol/opencode-md-table-formatter)
88
+ - [Report Issues](https://github.com/franlol/opencode-md-table-formatter/issues)
package/index.ts CHANGED
@@ -1,230 +1,235 @@
1
- import type { Plugin, Hooks } from "@opencode-ai/plugin"
2
-
3
- declare const Bun: any
4
-
5
- // Width cache for performance optimization
6
- const widthCache = new Map<string, number>()
7
- let cacheOperationCount = 0
8
-
9
- export const FormatTables: Plugin = async () => {
10
- return {
11
- "text.complete": async (
12
- input: { sessionID: string; messageID: string; partID: string },
13
- output: { text: string },
14
- ) => {
15
- try {
16
- output.text = formatMarkdownTables(output.text)
17
- } catch (error) {
18
- // If formatting fails, keep original md text
19
- output.text = output.text + "\n\n<!-- table formatting failed: " + (error as Error).message + " -->"
20
- }
21
- },
22
- } as Hooks
23
- }
24
-
25
- function formatMarkdownTables(text: string): string {
26
- const lines = text.split("\n")
27
- const result: string[] = []
28
- let i = 0
29
-
30
- while (i < lines.length) {
31
- const line = lines[i]
32
-
33
- if (isTableRow(line)) {
34
- const tableLines: string[] = [line]
35
- i++
36
-
37
- while (i < lines.length && isTableRow(lines[i])) {
38
- tableLines.push(lines[i])
39
- i++
40
- }
41
-
42
- if (isValidTable(tableLines)) {
43
- result.push(...formatTable(tableLines))
44
- } else {
45
- result.push(...tableLines)
46
- result.push("<!-- table not formatted: invalid structure -->")
47
- }
48
- } else {
49
- result.push(line)
50
- i++
51
- }
52
- }
53
-
54
- incrementOperationCount()
55
- return result.join("\n")
56
- }
57
-
58
- function isTableRow(line: string): boolean {
59
- const trimmed = line.trim()
60
- return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.split("|").length > 2
61
- }
62
-
63
- function isSeparatorRow(line: string): boolean {
64
- const trimmed = line.trim()
65
- if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) return false
66
- const cells = trimmed.split("|").slice(1, -1)
67
- return cells.length > 0 && cells.every((cell) => /^\s*:?-+:?\s*$/.test(cell))
68
- }
69
-
70
- function isValidTable(lines: string[]): boolean {
71
- if (lines.length < 2) return false
72
-
73
- const rows = lines.map((line) =>
74
- line
75
- .split("|")
76
- .slice(1, -1)
77
- .map((cell) => cell.trim()),
78
- )
79
-
80
- if (rows.length === 0 || rows[0].length === 0) return false
81
-
82
- const firstRowCellCount = rows[0].length
83
- const allSameColumnCount = rows.every((row) => row.length === firstRowCellCount)
84
- if (!allSameColumnCount) return false
85
-
86
- const hasSeparator = lines.some((line) => isSeparatorRow(line))
87
- return hasSeparator
88
- }
89
-
90
- function formatTable(lines: string[]): string[] {
91
- const separatorIndices = new Set<number>()
92
- for (let i = 0; i < lines.length; i++) {
93
- if (isSeparatorRow(lines[i])) separatorIndices.add(i)
94
- }
95
-
96
- const rows = lines.map((line) =>
97
- line
98
- .split("|")
99
- .slice(1, -1)
100
- .map((cell) => cell.trim()),
101
- )
102
-
103
- if (rows.length === 0) return lines
104
-
105
- const colCount = Math.max(...rows.map((row) => row.length))
106
-
107
- const colAlignments: Array<"left" | "center" | "right"> = Array(colCount).fill("left")
108
- for (const rowIndex of separatorIndices) {
109
- const row = rows[rowIndex]
110
- for (let col = 0; col < row.length; col++) {
111
- colAlignments[col] = getAlignment(row[col])
112
- }
113
- }
114
-
115
- const colWidths: number[] = Array(colCount).fill(3)
116
- for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
117
- if (separatorIndices.has(rowIndex)) continue
118
- const row = rows[rowIndex]
119
- for (let col = 0; col < row.length; col++) {
120
- const displayWidth = calculateDisplayWidth(row[col])
121
- colWidths[col] = Math.max(colWidths[col], displayWidth)
122
- }
123
- }
124
-
125
- return rows.map((row, rowIndex) => {
126
- const cells: string[] = []
127
- for (let col = 0; col < colCount; col++) {
128
- const cell = row[col] ?? ""
129
- const align = colAlignments[col]
130
-
131
- if (separatorIndices.has(rowIndex)) {
132
- cells.push(formatSeparatorCell(colWidths[col], align))
133
- } else {
134
- cells.push(padCell(cell, colWidths[col], align))
135
- }
136
- }
137
- return "| " + cells.join(" | ") + " |"
138
- })
139
- }
140
-
141
- function getAlignment(delimiterCell: string): "left" | "center" | "right" {
142
- const trimmed = delimiterCell.trim()
143
- const hasLeftColon = trimmed.startsWith(":")
144
- const hasRightColon = trimmed.endsWith(":")
145
-
146
- if (hasLeftColon && hasRightColon) return "center"
147
- if (hasRightColon) return "right"
148
- return "left"
149
- }
150
-
151
- function calculateDisplayWidth(text: string): number {
152
- if (widthCache.has(text)) {
153
- return widthCache.get(text)!
154
- }
155
-
156
- const width = getStringWidth(text)
157
- widthCache.set(text, width)
158
- return width
159
- }
160
-
161
- function getStringWidth(text: string): number {
162
- // Strip markdown symbols for concealment mode
163
- // Users with concealment ON don't see **, *, ~~, ` but DO see markdown inside `code`
164
-
165
- // CRITICAL: Content inside backticks should PRESERVE inner markdown symbols
166
- // because concealment treats them as literal text, not markdown
167
-
168
- // Step 1: Extract and protect inline code content
169
- const codeBlocks: string[] = []
170
- let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
171
- codeBlocks.push(content)
172
- return `\x00CODE${codeBlocks.length - 1}\x00`
173
- })
174
-
175
- // Step 2: Strip markdown from non-code parts
176
- let visualText = textWithPlaceholders
177
- let previousText = ""
178
-
179
- while (visualText !== previousText) {
180
- previousText = visualText
181
- visualText = visualText
182
- .replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***bold+italic*** -> text
183
- .replace(/\*\*(.+?)\*\*/g, "$1") // **bold** -> bold
184
- .replace(/\*(.+?)\*/g, "$1") // *italic* -> italic
185
- .replace(/~~(.+?)~~/g, "$1") // ~~strike~~ -> strike
186
- .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1") // ![alt](url) -> alt (OpenTUI shows only alt text)
187
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) -> text (url)
188
- }
189
-
190
- // Step 3: Restore code content (with its original markdown preserved)
191
- visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
192
- return codeBlocks[parseInt(index)]
193
- })
194
-
195
- return Bun.stringWidth(visualText)
196
- }
197
-
198
- function padCell(text: string, width: number, align: "left" | "center" | "right"): string {
199
- const displayWidth = calculateDisplayWidth(text)
200
- const totalPadding = Math.max(0, width - displayWidth)
201
-
202
- if (align === "center") {
203
- const leftPad = Math.floor(totalPadding / 2)
204
- const rightPad = totalPadding - leftPad
205
- return " ".repeat(leftPad) + text + " ".repeat(rightPad)
206
- } else if (align === "right") {
207
- return " ".repeat(totalPadding) + text
208
- } else {
209
- return text + " ".repeat(totalPadding)
210
- }
211
- }
212
-
213
- function formatSeparatorCell(width: number, align: "left" | "center" | "right"): string {
214
- if (align === "center") return ":" + "-".repeat(Math.max(1, width - 2)) + ":"
215
- if (align === "right") return "-".repeat(Math.max(1, width - 1)) + ":"
216
- return "-".repeat(width)
217
- }
218
-
219
- function incrementOperationCount() {
220
- cacheOperationCount++
221
-
222
- if (cacheOperationCount > 100 || widthCache.size > 1000) {
223
- cleanupCache()
224
- }
225
- }
226
-
227
- function cleanupCache() {
228
- widthCache.clear()
229
- cacheOperationCount = 0
230
- }
1
+ import type { Plugin, Hooks } from "@opencode-ai/plugin"
2
+
3
+ declare const Bun: any
4
+
5
+ // Width cache for performance optimization
6
+ const widthCache = new Map<string, number>()
7
+ let cacheOperationCount = 0
8
+
9
+ export const FormatTables: Plugin = async () => {
10
+ return {
11
+ "experimental.text.complete": async (
12
+ input: { sessionID: string; messageID: string; partID: string },
13
+ output: { text: string },
14
+ ) => {
15
+ try {
16
+ output.text = formatMarkdownTables(output.text)
17
+ } catch (error) {
18
+ // If formatting fails, keep original md text
19
+ output.text = output.text + "\n\n<!-- table formatting failed: " + (error as Error).message + " -->"
20
+ }
21
+ },
22
+ } as Hooks
23
+ }
24
+
25
+ export function formatMarkdownTables(text: string): string {
26
+ const lines = text.split("\n")
27
+ const result: string[] = []
28
+ let i = 0
29
+
30
+ while (i < lines.length) {
31
+ const line = lines[i]
32
+
33
+ if (isTableRow(line)) {
34
+ const tableLines: string[] = [line]
35
+ i++
36
+
37
+ while (i < lines.length && isTableRow(lines[i])) {
38
+ tableLines.push(lines[i])
39
+ i++
40
+ }
41
+
42
+ if (isValidTable(tableLines)) {
43
+ result.push(...formatTable(tableLines))
44
+ } else {
45
+ result.push(...tableLines)
46
+ result.push("<!-- table not formatted: invalid structure -->")
47
+ }
48
+ } else {
49
+ result.push(line)
50
+ i++
51
+ }
52
+ }
53
+
54
+ incrementOperationCount()
55
+ return result.join("\n")
56
+ }
57
+
58
+ function isTableRow(line: string): boolean {
59
+ const trimmed = line.trim()
60
+ return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.split("|").length > 2
61
+ }
62
+
63
+ function isSeparatorRow(line: string): boolean {
64
+ const trimmed = line.trim()
65
+ if (!trimmed.startsWith("|") || !trimmed.endsWith("|")) return false
66
+ const cells = trimmed.split("|").slice(1, -1)
67
+ return cells.length > 0 && cells.every((cell) => /^\s*:?-+:?\s*$/.test(cell))
68
+ }
69
+
70
+ function isValidTable(lines: string[]): boolean {
71
+ if (lines.length < 2) return false
72
+
73
+ const rows = lines.map((line) =>
74
+ line
75
+ .split("|")
76
+ .slice(1, -1)
77
+ .map((cell) => cell.trim()),
78
+ )
79
+
80
+ if (rows.length === 0 || rows[0].length === 0) return false
81
+
82
+ const firstRowCellCount = rows[0].length
83
+ const allSameColumnCount = rows.every((row) => row.length === firstRowCellCount)
84
+ if (!allSameColumnCount) return false
85
+
86
+ const hasSeparator = lines.some((line) => isSeparatorRow(line))
87
+ return hasSeparator
88
+ }
89
+
90
+ function formatTable(lines: string[]): string[] {
91
+ const separatorIndices = new Set<number>()
92
+ for (let i = 0; i < lines.length; i++) {
93
+ if (isSeparatorRow(lines[i])) separatorIndices.add(i)
94
+ }
95
+
96
+ const rows = lines.map((line) =>
97
+ line
98
+ .split("|")
99
+ .slice(1, -1)
100
+ .map((cell) => cell.trim()),
101
+ )
102
+
103
+ if (rows.length === 0) return lines
104
+
105
+ const colCount = Math.max(...rows.map((row) => row.length))
106
+
107
+ const colAlignments: Array<"left" | "center" | "right"> = Array(colCount).fill("left")
108
+ for (const rowIndex of separatorIndices) {
109
+ const row = rows[rowIndex]
110
+ for (let col = 0; col < row.length; col++) {
111
+ colAlignments[col] = getAlignment(row[col])
112
+ }
113
+ }
114
+
115
+ const colWidths: number[] = Array(colCount).fill(3)
116
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
117
+ if (separatorIndices.has(rowIndex)) continue
118
+ const row = rows[rowIndex]
119
+ for (let col = 0; col < row.length; col++) {
120
+ const displayWidth = calculateDisplayWidth(row[col])
121
+ colWidths[col] = Math.max(colWidths[col], displayWidth)
122
+ }
123
+ }
124
+
125
+ return rows.map((row, rowIndex) => {
126
+ const cells: string[] = []
127
+ for (let col = 0; col < colCount; col++) {
128
+ const cell = row[col] ?? ""
129
+ const align = colAlignments[col]
130
+
131
+ if (separatorIndices.has(rowIndex)) {
132
+ cells.push(formatSeparatorCell(colWidths[col], align))
133
+ } else {
134
+ cells.push(padCell(cell, colWidths[col], align))
135
+ }
136
+ }
137
+ return "| " + cells.join(" | ") + " |"
138
+ })
139
+ }
140
+
141
+ function getAlignment(delimiterCell: string): "left" | "center" | "right" {
142
+ const trimmed = delimiterCell.trim()
143
+ const hasLeftColon = trimmed.startsWith(":")
144
+ const hasRightColon = trimmed.endsWith(":")
145
+
146
+ if (hasLeftColon && hasRightColon) return "center"
147
+ if (hasRightColon) return "right"
148
+ return "left"
149
+ }
150
+
151
+ function calculateDisplayWidth(text: string): number {
152
+ if (widthCache.has(text)) {
153
+ return widthCache.get(text)!
154
+ }
155
+
156
+ const width = getStringWidth(text)
157
+ widthCache.set(text, width)
158
+ return width
159
+ }
160
+
161
+ function getStringWidth(text: string): number {
162
+ // Strip markdown symbols for concealment mode
163
+ // Users with concealment ON don't see **, *, ~~, ` but DO see markdown inside `code`
164
+
165
+ // CRITICAL: Content inside backticks should PRESERVE inner markdown symbols
166
+ // because concealment treats them as literal text, not markdown
167
+
168
+ // Step 1: Extract and protect inline code content
169
+ const codeBlocks: string[] = []
170
+ let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
171
+ codeBlocks.push(content)
172
+ return `\x00CODE${codeBlocks.length - 1}\x00`
173
+ })
174
+
175
+ // Step 2: Strip markdown from non-code parts
176
+ let visualText = textWithPlaceholders
177
+ let previousText = ""
178
+
179
+ while (visualText !== previousText) {
180
+ previousText = visualText
181
+ visualText = visualText
182
+ .replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***bold+italic*** -> text
183
+ .replace(/___(.+?)___/g, "$1") // ___bold+italic___ -> text
184
+ .replace(/\*\*(.+?)\*\*/g, "$1") // **bold** -> bold
185
+ .replace(/__(.+?)__/g, "$1") // __bold__ -> bold
186
+ .replace(/\*(.+?)\*/g, "$1") // *italic* -> italic
187
+ .replace(/_(.+?)_/g, "$1") // _italic_ -> italic
188
+ .replace(/~~(.+?)~~/g, "$1") // ~~strike~~ -> strike
189
+ .replace(/~(.+?)~/g, "$1") // ~subscript~ -> subscript
190
+ .replace(/\^(.+?)\^/g, "$1") // ^superscript^ -> superscript
191
+ .replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1") // ![alt](url) -> alt (OpenTUI shows only alt text)
192
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) -> text (url)
193
+ }
194
+
195
+ // Step 3: Restore code content (with its original markdown preserved)
196
+ visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
197
+ return codeBlocks[parseInt(index)]
198
+ })
199
+
200
+ return Bun.stringWidth(visualText)
201
+ }
202
+
203
+ function padCell(text: string, width: number, align: "left" | "center" | "right"): string {
204
+ const displayWidth = calculateDisplayWidth(text)
205
+ const totalPadding = Math.max(0, width - displayWidth)
206
+
207
+ if (align === "center") {
208
+ const leftPad = Math.floor(totalPadding / 2)
209
+ const rightPad = totalPadding - leftPad
210
+ return " ".repeat(leftPad) + text + " ".repeat(rightPad)
211
+ } else if (align === "right") {
212
+ return " ".repeat(totalPadding) + text
213
+ } else {
214
+ return text + " ".repeat(totalPadding)
215
+ }
216
+ }
217
+
218
+ function formatSeparatorCell(width: number, align: "left" | "center" | "right"): string {
219
+ if (align === "center") return ":" + "-".repeat(Math.max(1, width - 2)) + ":"
220
+ if (align === "right") return "-".repeat(Math.max(1, width - 1)) + ":"
221
+ return "-".repeat(width)
222
+ }
223
+
224
+ function incrementOperationCount() {
225
+ cacheOperationCount++
226
+
227
+ if (cacheOperationCount > 100 || widthCache.size > 1000) {
228
+ cleanupCache()
229
+ }
230
+ }
231
+
232
+ function cleanupCache() {
233
+ widthCache.clear()
234
+ cacheOperationCount = 0
235
+ }
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "@franlol/opencode-md-table-formatter",
3
- "version": "0.0.2",
4
- "description": "Markdown table formatter plugin for OpenCode with concealment mode support",
5
- "keywords": [
6
- "opencode",
7
- "plugin",
8
- "markdown",
9
- "table",
10
- "formatter",
11
- "concealment",
12
- "alignment",
13
- "text-formatting"
14
- ],
15
- "homepage": "https://github.com/franlol/opencode-md-table-formatter#readme",
16
- "bugs": {
17
- "url": "https://github.com/franlol/opencode-md-table-formatter/issues"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/franlol/opencode-md-table-formatter.git"
22
- },
23
- "license": "MIT",
24
- "author": "franlol",
25
- "type": "module",
26
- "main": "index.ts",
27
- "files": [
28
- "index.ts",
29
- "LICENSE",
30
- "README.md"
31
- ],
32
- "scripts": {
33
- "test": "echo \"Error: no test specified\" && exit 1"
34
- },
35
- "peerDependencies": {
36
- "@opencode-ai/plugin": ">=0.13.7"
37
- },
38
- "engines": {
39
- "node": ">=18.0.0"
40
- }
41
- }
1
+ {
2
+ "name": "@franlol/opencode-md-table-formatter",
3
+ "version": "0.0.4",
4
+ "description": "Markdown table formatter plugin for OpenCode with concealment mode support",
5
+ "keywords": [
6
+ "opencode",
7
+ "plugin",
8
+ "markdown",
9
+ "table",
10
+ "formatter",
11
+ "concealment",
12
+ "alignment",
13
+ "text-formatting"
14
+ ],
15
+ "homepage": "https://github.com/franlol/opencode-md-table-formatter#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/franlol/opencode-md-table-formatter/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/franlol/opencode-md-table-formatter.git"
22
+ },
23
+ "license": "MIT",
24
+ "author": "franlol",
25
+ "type": "module",
26
+ "main": "index.ts",
27
+ "files": [
28
+ "index.ts",
29
+ "LICENSE",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "test": "bun test"
34
+ },
35
+ "peerDependencies": {
36
+ "@opencode-ai/plugin": ">=0.13.7"
37
+ },
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ }
41
+ }