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/README.md b/README.md index e955571..71f3aa9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,47 @@ Add the plugin to your `.opencode/opencode.jsonc`: +## Configuration + +Set the `OPENCODE_TABLE_STYLE` environment variable to choose a table style. If unset or invalid, defaults to `markdown`. + +| Value | Description | +|---------------|--------------------------------------------| +| `markdown` | Standard markdown tables (default) | +| `boxDrawing` | Single-line box-drawing characters | +| `doublePipe` | Double-line box-drawing characters | + +**markdown** (default): + +``` +| Name | Age | City | +| ------ | --- | -------- | +| Alice | 30 | New York | +| Bob | 25 | London | +``` + +**boxDrawing**: + +``` +┌────────┬─────┬──────────┐ +│ Name │ Age │ City │ +├────────┼─────┼──────────┤ +│ Alice │ 30 │ New York │ +│ Bob │ 25 │ London │ +└────────┴─────┴──────────┘ +``` + +**doublePipe**: + +``` +╔════════╦═════╦══════════╗ +║ Name ║ Age ║ City ║ +╠════════╬═════╬══════════╣ +║ Alice ║ 30 ║ New York ║ +║ Bob ║ 25 ║ London ║ +╚════════╩═════╩══════════╝ +``` + ## Features - **Automatic table formatting** - Formats markdown tables after AI text completion diff --git a/index.ts b/index.ts index 4062098..8f684eb 100644 --- a/index.ts +++ b/index.ts @@ -2,18 +2,120 @@ import type { Plugin, Hooks } from "@opencode-ai/plugin" declare const Bun: any +type Alignment = "left" | "center" | "right" + +type ParsedTable = { + rows: string[][] + colWidths: number[] + colAlignments: Alignment[] + separatorIndices: Set +} + +type TableStyle = { + renderTable: (table: ParsedTable) => string[] +} + +type TableStyleName = "markdown" | "boxDrawing" | "doublePipe" + +type BorderChars = { + topLeft: string; topRight: string; bottomLeft: string; bottomRight: string + horizontal: string; vertical: string + topTee: string; bottomTee: string; leftTee: string; rightTee: string; cross: string +} + // Width cache for performance optimization const widthCache = new Map() let cacheOperationCount = 0 +function createBorderedStyle(chars: BorderChars): TableStyle { + function buildHorizontalLine(colWidths: number[], left: string, mid: string, right: string): string { + const segments = colWidths.map((w) => chars.horizontal.repeat(w + 2)) + return left + segments.join(mid) + right + } + + return { + renderTable(table: ParsedTable): string[] { + const { rows, colWidths, colAlignments, separatorIndices } = table + const colCount = colWidths.length + const result: string[] = [] + + result.push(buildHorizontalLine(colWidths, chars.topLeft, chars.topTee, chars.topRight)) + + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + if (separatorIndices.has(rowIndex)) { + result.push(buildHorizontalLine(colWidths, chars.leftTee, chars.cross, chars.rightTee)) + } else { + const cells: string[] = [] + for (let col = 0; col < colCount; col++) { + const cell = rows[rowIndex][col] ?? "" + cells.push(padCell(cell, colWidths[col], colAlignments[col])) + } + result.push(chars.vertical + " " + cells.join(" " + chars.vertical + " ") + " " + chars.vertical) + } + } + + result.push(buildHorizontalLine(colWidths, chars.bottomLeft, chars.bottomTee, chars.bottomRight)) + return result + }, + } +} + +function formatSeparatorCell(width: number, align: Alignment): string { + if (align === "center") return ":" + "-".repeat(Math.max(1, width - 2)) + ":" + if (align === "right") return "-".repeat(Math.max(1, width - 1)) + ":" + return "-".repeat(width) +} + +const markdownStyle: TableStyle = { + renderTable(table: ParsedTable): string[] { + const { rows, colWidths, colAlignments, separatorIndices } = table + const colCount = colWidths.length + + return rows.map((row, rowIndex) => { + const cells: string[] = [] + for (let col = 0; col < colCount; col++) { + const cell = row[col] ?? "" + if (separatorIndices.has(rowIndex)) { + cells.push(formatSeparatorCell(colWidths[col], colAlignments[col])) + } else { + cells.push(padCell(cell, colWidths[col], colAlignments[col])) + } + } + return "| " + cells.join(" | ") + " |" + }) + }, +} + +const TABLE_STYLES: Record = { + markdown: markdownStyle, + boxDrawing: createBorderedStyle({ + topLeft: "┌", topRight: "┐", bottomLeft: "└", bottomRight: "┘", + horizontal: "─", vertical: "│", + topTee: "┬", bottomTee: "┴", leftTee: "├", rightTee: "┤", cross: "┼", + }), + doublePipe: createBorderedStyle({ + topLeft: "╔", topRight: "╗", bottomLeft: "╚", bottomRight: "╝", + horizontal: "═", vertical: "║", + topTee: "╦", bottomTee: "╩", leftTee: "╠", rightTee: "╣", cross: "╬", + }), +} + +function resolveStyleName(): TableStyleName { + const env = (typeof process !== "undefined" && process.env?.OPENCODE_TABLE_STYLE) || "" + if (env in TABLE_STYLES) return env as TableStyleName + return "markdown" +} + export const FormatTables: Plugin = async () => { + const style = TABLE_STYLES[resolveStyleName()] + 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" @@ -22,7 +124,7 @@ export const FormatTables: Plugin = async () => { } as Hooks } -function formatMarkdownTables(text: string): string { +function formatMarkdownTables(text: string, style: TableStyle): string { const lines = text.split("\n") const result: string[] = [] let i = 0 @@ -40,7 +142,7 @@ function formatMarkdownTables(text: string): string { } if (isValidTable(tableLines)) { - result.push(...formatTable(tableLines)) + result.push(...formatTable(tableLines, style)) } else { result.push(...tableLines) result.push("") @@ -87,7 +189,7 @@ function isValidTable(lines: string[]): boolean { return hasSeparator } -function formatTable(lines: string[]): string[] { +function parseTable(lines: string[]): ParsedTable { const separatorIndices = new Set() for (let i = 0; i < lines.length; i++) { if (isSeparatorRow(lines[i])) separatorIndices.add(i) @@ -100,11 +202,9 @@ function formatTable(lines: string[]): string[] { .map((cell) => cell.trim()), ) - if (rows.length === 0) return lines - const colCount = Math.max(...rows.map((row) => row.length)) - const colAlignments: Array<"left" | "center" | "right"> = Array(colCount).fill("left") + const colAlignments: Alignment[] = Array(colCount).fill("left") for (const rowIndex of separatorIndices) { const row = rows[rowIndex] for (let col = 0; col < row.length; col++) { @@ -122,23 +222,16 @@ 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] + return { rows, colWidths, colAlignments, separatorIndices } +} - if (separatorIndices.has(rowIndex)) { - cells.push(formatSeparatorCell(colWidths[col], align)) - } else { - cells.push(padCell(cell, colWidths[col], align)) - } - } - return "| " + cells.join(" | ") + " |" - }) +function formatTable(lines: string[], style: TableStyle): string[] { + const table = parseTable(lines) + if (table.rows.length === 0) return lines + return style.renderTable(table) } -function getAlignment(delimiterCell: string): "left" | "center" | "right" { +function getAlignment(delimiterCell: string): Alignment { const trimmed = delimiterCell.trim() const hasLeftColon = trimmed.startsWith(":") const hasRightColon = trimmed.endsWith(":") @@ -195,7 +288,7 @@ function getStringWidth(text: string): number { return Bun.stringWidth(visualText) } -function padCell(text: string, width: number, align: "left" | "center" | "right"): string { +function padCell(text: string, width: number, align: Alignment): string { const displayWidth = calculateDisplayWidth(text) const totalPadding = Math.max(0, width - displayWidth) @@ -210,12 +303,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.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",