Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Dependencies
node_modules/
package-lock.json
.pnp
.pnp.js

Expand Down
94 changes: 78 additions & 16 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@ 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<string, number>()
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<!-- table formatting failed: " + (error as Error).message + " -->"
Expand All @@ -22,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
Expand All @@ -40,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("<!-- table not formatted: invalid structure -->")
Expand Down Expand Up @@ -87,7 +108,17 @@ function isValidTable(lines: string[]): boolean {
return hasSeparator
}

function formatTable(lines: string[]): string[] {
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[], style: "markdown" | "box"): string[] {
const separatorIndices = new Set<number>()
for (let i = 0; i < lines.length; i++) {
if (isSeparatorRow(lines[i])) separatorIndices.add(i)
Expand Down Expand Up @@ -122,20 +153,50 @@ 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]
if (style === "box") {
// Build the box-drawing table
const result: string[] = []

// 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)) {
cells.push(formatSeparatorCell(colWidths[col], align))
// Replace markdown separator with box-drawing middle border
result.push(buildHorizontalLine(colWidths, BOX.leftTee, BOX.cross, BOX.rightTee))
} else {
cells.push(padCell(cell, colWidths[col], align))
// 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
} else {
return rows.map((row, rowIndex) => {
const cells: string[] = []
for (let col = 0; col < colCount; col++) {
const cell = row[col] ?? ""
const align = colAlignments[col]

if (separatorIndices.has(rowIndex)) {
cells.push(formatSeparatorCell(colWidths[col], align))
} else {
cells.push(padCell(cell, colWidths[col], align))
}
}
return "| " + cells.join(" | ") + " |"
})
}
}

function getAlignment(delimiterCell: string): "left" | "center" | "right" {
Expand Down Expand Up @@ -169,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
Expand All @@ -188,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)]
})

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down