From 7553f671826f44d1d7575ecd9cb315636c8c7e53 Mon Sep 17 00:00:00 2001 From: Bertrand Date: Fri, 30 Jan 2026 23:20:49 -0800 Subject: [PATCH 1/3] feat: use box-drawing characters for table formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace standard markdown pipe/dash table borders with Unicode box-drawing characters (┌┐└┘─│┬┴├┤┼) for improved visual appearance. Add buildHorizontalLine helper and remove formatSeparatorCell which is no longer needed. --- index.ts | 64 +++++++++++++++++++++++++++++++++++------------ package-lock.json | 47 ++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 package-lock.json diff --git a/index.ts b/index.ts index 4062098..1e739d5 100644 --- a/index.ts +++ b/index.ts @@ -2,6 +2,21 @@ import type { Plugin, Hooks } from "@opencode-ai/plugin" declare const Bun: any +// Box-drawing characters +const BOX = { + topLeft: "┌", + topRight: "┐", + bottomLeft: "└", + bottomRight: "┘", + horizontal: "─", + vertical: "│", + topTee: "┬", + bottomTee: "┴", + leftTee: "├", + rightTee: "┤", + cross: "┼", +} + // Width cache for performance optimization const widthCache = new Map() let cacheOperationCount = 0 @@ -87,6 +102,16 @@ function isValidTable(lines: string[]): boolean { return hasSeparator } +function buildHorizontalLine( + colWidths: number[], + left: string, + mid: string, + right: string, +): string { + const segments = colWidths.map((w) => BOX.horizontal.repeat(w + 2)) + return left + segments.join(mid) + right +} + function formatTable(lines: string[]): string[] { const separatorIndices = new Set() for (let i = 0; i < lines.length; i++) { @@ -122,20 +147,33 @@ function formatTable(lines: string[]): string[] { } } - return rows.map((row, rowIndex) => { - const cells: string[] = [] - for (let col = 0; col < colCount; col++) { - const cell = row[col] ?? "" - const align = colAlignments[col] + // Build the box-drawing table + const result: string[] = [] - if (separatorIndices.has(rowIndex)) { - cells.push(formatSeparatorCell(colWidths[col], align)) - } else { + // Top border + result.push(buildHorizontalLine(colWidths, BOX.topLeft, BOX.topTee, BOX.topRight)) + + // Data rows (skip separator rows, replace them with box-drawing middle borders) + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + if (separatorIndices.has(rowIndex)) { + // Replace markdown separator with box-drawing middle border + result.push(buildHorizontalLine(colWidths, BOX.leftTee, BOX.cross, BOX.rightTee)) + } else { + // Data row with box-drawing vertical borders + const cells: string[] = [] + for (let col = 0; col < colCount; col++) { + const cell = rows[rowIndex][col] ?? "" + const align = colAlignments[col] cells.push(padCell(cell, colWidths[col], align)) } + result.push(BOX.vertical + " " + cells.join(" " + BOX.vertical + " ") + " " + BOX.vertical) } - return "| " + cells.join(" | ") + " |" - }) + } + + // Bottom border + result.push(buildHorizontalLine(colWidths, BOX.bottomLeft, BOX.bottomTee, BOX.bottomRight)) + + return result } function getAlignment(delimiterCell: string): "left" | "center" | "right" { @@ -210,12 +248,6 @@ function padCell(text: string, width: number, align: "left" | "center" | "right" } } -function formatSeparatorCell(width: number, align: "left" | "center" | "right"): string { - if (align === "center") return ":" + "-".repeat(Math.max(1, width - 2)) + ":" - if (align === "right") return "-".repeat(Math.max(1, width - 1)) + ":" - return "-".repeat(width) -} - function incrementOperationCount() { cacheOperationCount++ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..456c069 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,47 @@ +{ + "name": "@franlol/opencode-md-table-formatter", + "version": "0.0.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@franlol/opencode-md-table-formatter", + "version": "0.0.3", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=0.13.7" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.1.44", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.1.44.tgz", + "integrity": "sha512-5w66Dq2Fugwgr2yrd8obvnlIEjBOuya82UgfR/3z3EzlyNDi2sitQSYbz7CcOtwd89eZ0n/tH/JX2KDGVuzxTQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@opencode-ai/sdk": "1.1.44", + "zod": "4.1.8" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.1.44", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.44.tgz", + "integrity": "sha512-coQgtSSCbY46/GY+M5zG0rChiLSJWSjPERRt5L1hbjvDWvErelVV0ILPbd1+3CwJLFTedBYgotby2TcO8U0IfQ==", + "license": "MIT", + "peer": true + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} From 07155eed758596e3973b249448c8ba13e7024c91 Mon Sep 17 00:00:00 2001 From: Bertrand Date: Sun, 8 Feb 2026 12:54:49 -0800 Subject: [PATCH 2/3] fix: address PR #8 review feedback Remove accidentally committed package-lock.json (Bun project), add it to .gitignore, and bump version to 0.1.0 for breaking change. --- .gitignore | 1 + package-lock.json | 47 ----------------------------------------------- package.json | 2 +- 3 files changed, 2 insertions(+), 48 deletions(-) delete mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 3ee7f1c..5c08c02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Dependencies node_modules/ +package-lock.json .pnp .pnp.js diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 456c069..0000000 --- a/package-lock.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "@franlol/opencode-md-table-formatter", - "version": "0.0.3", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@franlol/opencode-md-table-formatter", - "version": "0.0.3", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@opencode-ai/plugin": ">=0.13.7" - } - }, - "node_modules/@opencode-ai/plugin": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.1.44.tgz", - "integrity": "sha512-5w66Dq2Fugwgr2yrd8obvnlIEjBOuya82UgfR/3z3EzlyNDi2sitQSYbz7CcOtwd89eZ0n/tH/JX2KDGVuzxTQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "@opencode-ai/sdk": "1.1.44", - "zod": "4.1.8" - } - }, - "node_modules/@opencode-ai/sdk": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.1.44.tgz", - "integrity": "sha512-coQgtSSCbY46/GY+M5zG0rChiLSJWSjPERRt5L1hbjvDWvErelVV0ILPbd1+3CwJLFTedBYgotby2TcO8U0IfQ==", - "license": "MIT", - "peer": true - }, - "node_modules/zod": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", - "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/package.json b/package.json index 7666616..5e82433 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@franlol/opencode-md-table-formatter", - "version": "0.0.3", + "version": "0.1.0", "description": "Markdown table formatter plugin for OpenCode with concealment mode support", "keywords": [ "opencode", From 3c7a1b630e6b565d1b22981ec037827e75a9fc86 Mon Sep 17 00:00:00 2001 From: manascb1344 Date: Tue, 10 Feb 2026 12:37:38 +0530 Subject: [PATCH 3/3] fix: make box-drawing tables opt-in and restore markdown compatibility --- index.ts | 84 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/index.ts b/index.ts index 1e739d5..2d3c922 100644 --- a/index.ts +++ b/index.ts @@ -21,14 +21,20 @@ const BOX = { const widthCache = new Map() let cacheOperationCount = 0 -export const FormatTables: Plugin = async () => { +interface TableFormatterOptions { + style?: "markdown" | "box" +} + +export const FormatTables: Plugin = async (options?: TableFormatterOptions) => { + const style = options?.style ?? "markdown" + return { "experimental.text.complete": async ( input: { sessionID: string; messageID: string; partID: string }, output: { text: string }, ) => { try { - output.text = formatMarkdownTables(output.text) + output.text = formatMarkdownTables(output.text, style) } catch (error) { // If formatting fails, keep original md text output.text = output.text + "\n\n" @@ -37,7 +43,7 @@ export const FormatTables: Plugin = async () => { } as Hooks } -function formatMarkdownTables(text: string): string { +function formatMarkdownTables(text: string, style: "markdown" | "box"): string { const lines = text.split("\n") const result: string[] = [] let i = 0 @@ -55,7 +61,7 @@ function formatMarkdownTables(text: string): string { } if (isValidTable(tableLines)) { - result.push(...formatTable(tableLines)) + result.push(...formatTable(tableLines, style)) } else { result.push(...tableLines) result.push("") @@ -112,7 +118,7 @@ function buildHorizontalLine( return left + segments.join(mid) + right } -function formatTable(lines: string[]): string[] { +function formatTable(lines: string[], style: "markdown" | "box"): string[] { const separatorIndices = new Set() for (let i = 0; i < lines.length; i++) { if (isSeparatorRow(lines[i])) separatorIndices.add(i) @@ -147,33 +153,50 @@ function formatTable(lines: string[]): string[] { } } - // Build the box-drawing table - const result: string[] = [] + if (style === "box") { + // Build the box-drawing table + const result: string[] = [] - // Top border - result.push(buildHorizontalLine(colWidths, BOX.topLeft, BOX.topTee, BOX.topRight)) + // Top border + result.push(buildHorizontalLine(colWidths, BOX.topLeft, BOX.topTee, BOX.topRight)) - // Data rows (skip separator rows, replace them with box-drawing middle borders) - for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { - if (separatorIndices.has(rowIndex)) { - // Replace markdown separator with box-drawing middle border - result.push(buildHorizontalLine(colWidths, BOX.leftTee, BOX.cross, BOX.rightTee)) - } else { - // Data row with box-drawing vertical borders + // Data rows (skip separator rows, replace them with box-drawing middle borders) + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + if (separatorIndices.has(rowIndex)) { + // Replace markdown separator with box-drawing middle border + result.push(buildHorizontalLine(colWidths, BOX.leftTee, BOX.cross, BOX.rightTee)) + } else { + // Data row with box-drawing vertical borders + const cells: string[] = [] + for (let col = 0; col < colCount; col++) { + const cell = rows[rowIndex][col] ?? "" + const align = colAlignments[col] + cells.push(padCell(cell, colWidths[col], align)) + } + result.push(BOX.vertical + " " + cells.join(" " + BOX.vertical + " ") + " " + BOX.vertical) + } + } + + // Bottom border + result.push(buildHorizontalLine(colWidths, BOX.bottomLeft, BOX.bottomTee, BOX.bottomRight)) + + return result + } else { + return rows.map((row, rowIndex) => { const cells: string[] = [] for (let col = 0; col < colCount; col++) { - const cell = rows[rowIndex][col] ?? "" + const cell = row[col] ?? "" const align = colAlignments[col] - cells.push(padCell(cell, colWidths[col], align)) + + if (separatorIndices.has(rowIndex)) { + cells.push(formatSeparatorCell(colWidths[col], align)) + } else { + cells.push(padCell(cell, colWidths[col], align)) + } } - result.push(BOX.vertical + " " + cells.join(" " + BOX.vertical + " ") + " " + BOX.vertical) - } + return "| " + cells.join(" | ") + " |" + }) } - - // Bottom border - result.push(buildHorizontalLine(colWidths, BOX.bottomLeft, BOX.bottomTee, BOX.bottomRight)) - - return result } function getAlignment(delimiterCell: string): "left" | "center" | "right" { @@ -207,7 +230,7 @@ function getStringWidth(text: string): number { const codeBlocks: string[] = [] let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => { codeBlocks.push(content) - return `\x00CODE${codeBlocks.length - 1}\x00` + return `\u0000CODE${codeBlocks.length - 1}\u0000` }) // Step 2: Strip markdown from non-code parts @@ -226,7 +249,8 @@ function getStringWidth(text: string): number { } // Step 3: Restore code content (with its original markdown preserved) - visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => { + const restoreRegex = new RegExp("\\u0000CODE(\\d+)\\u0000", "g") + visualText = visualText.replace(restoreRegex, (match, index) => { return codeBlocks[parseInt(index)] }) @@ -248,6 +272,12 @@ function padCell(text: string, width: number, align: "left" | "center" | "right" } } +function formatSeparatorCell(width: number, align: "left" | "center" | "right"): string { + if (align === "center") return ":" + "-".repeat(Math.max(1, width - 2)) + ":" + if (align === "right") return "-".repeat(Math.max(1, width - 1)) + ":" + return "-".repeat(width) +} + function incrementOperationCount() { cacheOperationCount++