@franlol/opencode-md-table-formatter 0.0.2 → 0.0.3
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 +21 -21
- package/README.md +94 -97
- package/index.ts +230 -230
- 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,94 @@
|
|
|
1
|
-
# @franlol/opencode-
|
|
2
|
-
|
|
3
|
-
Markdown table formatter plugin for
|
|
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-
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Restart
|
|
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
|
|
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
|
|
71
|
-
- Check that tables have a separator row (`|---|---|`)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
## 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-md-table-formatter@0.0.2"],
|
|
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 `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).
|
|
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
|
+
|
|
74
|
+
**Invalid table structure comment?**
|
|
75
|
+
|
|
76
|
+
- The plugin validates tables before formatting
|
|
77
|
+
- All rows must have the same number of columns
|
|
78
|
+
- Tables must have at least 2 rows including the separator
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- Opencode CLI
|
|
83
|
+
- Node.js >= 18.0.0 or Bun runtime
|
|
84
|
+
- `@opencode-ai/plugin` >= 0.13.7
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT © franlol
|
|
89
|
+
|
|
90
|
+
## Links
|
|
91
|
+
|
|
92
|
+
- [GitHub Repository](https://github.com/franlol/opencode-md-table-formatter)
|
|
93
|
+
- [npm Package](https://www.npmjs.com/package/@franlol/opencode-md-table-formatter)
|
|
94
|
+
- [Report Issues](https://github.com/franlol/opencode-md-table-formatter/issues)
|
package/index.ts
CHANGED
|
@@ -1,230 +1,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
|
-
"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 (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
|
+
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 (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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@franlol/opencode-md-table-formatter",
|
|
3
|
-
"version": "0.0.
|
|
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.3",
|
|
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
|
+
}
|