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:

PatternMatches
*.csvAll CSV files
*.csv,*.tsvCSV and TSV files
*.jsonAll JSON files
server.jsonOnly 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 }
    }
  }
}