Renderers
Override how specific file types display in the Stoa editor with custom rendering plugins.
Renderers override how specific file types display in the editor. When a user opens a file matching the @filetype pattern, the plugin's output replaces the default code editor.
Basic renderer
// @stoa-plugin
// @name CSV Table View
// @type renderer
// @icon Table
// @filetype *.csv,*.tsv
module.exports = {
render(content, context) {
const delimiter = context.filePath.endsWith('.tsv') ? '\t' : ','
const lines = content.split('\n').filter(l => l.trim())
if (lines.length === 0) {
return { type: 'empty', message: 'Empty file' }
}
const headers = lines[0].split(delimiter)
const rows = lines.slice(1).map(l => l.split(delimiter))
return {
type: 'table',
columns: headers,
rows: rows
}
}
}A banner appears above rendered files showing the plugin name, with a toggle to switch between the plugin view and the raw source.
Function signature
Renderers receive two arguments:
render(content, context) {
content // string — the raw file content
context.filePath // string — path of the file being rendered
context.fileContent // string — same as content (for convenience)
}Filetype patterns
The @filetype header accepts comma-separated glob patterns:
| Pattern | Matches |
|---|---|
*.csv | All CSV files |
*.csv,*.tsv | CSV and TSV files |
*.json | All JSON files |
server.json | Only files named server.json |
*.config.* | Files like app.config.js, db.config.yaml |
Editable tables
Use editableTable to let users edit cells, add rows, and delete rows. Your onEdit() handler receives the edit and returns updated file content:
// @stoa-plugin
// @name CSV Editor
// @type renderer
// @icon Table
// @filetype *.csv,*.tsv
module.exports = {
render(content, context) {
const delimiter = context.filePath.endsWith('.tsv') ? '\t' : ','
const lines = content.split('\n').filter(l => l.trim())
if (lines.length === 0) {
return { type: 'empty', message: 'Empty file' }
}
const headers = lines[0].split(delimiter)
const rows = lines.slice(1).map(l => l.split(delimiter))
return {
type: 'editableTable',
columns: headers,
rows,
editable: Object.fromEntries(headers.map(h => [h, { type: 'text' }])),
rowOps: { add: true, delete: true },
}
},
onEdit(edit, content, context) {
const delimiter = context.filePath.endsWith('.tsv') ? '\t' : ','
const lines = content.split('\n').filter(l => l.trim())
const headers = lines[0].split(delimiter)
const rows = lines.slice(1).map(l => l.split(delimiter))
if (edit.kind === 'cell') {
rows[edit.rowIndex][edit.columnIndex] = edit.newValue
} else if (edit.kind === 'addRow') {
rows.push(headers.map(() => ''))
} else if (edit.kind === 'deleteRow') {
rows.splice(edit.rowIndex, 1)
}
return {
content: [headers.join(delimiter), ...rows.map(r => r.join(delimiter))].join('\n')
}
}
}See Editable Tables for the full onEdit protocol.
Editable HTML
For renderers that need custom UI beyond tables — forms, kanban boards, drag-and-drop surfaces — use editableHtml. The iframe stays mounted across edits, preserving focus and scroll position.
See Editable HTML for the full contract.
JSON viewer
A simple renderer for structured data:
// @stoa-plugin
// @name JSON Viewer
// @type renderer
// @icon FileText
// @filetype *.json
module.exports = {
render(content) {
try {
const data = JSON.parse(content)
return { type: 'json', data: data, collapsed: false }
} catch (e) {
return { type: 'error', message: 'Invalid JSON', details: e.message }
}
}
}