Creating Plugins
This guide covers everything you need to build custom block plugins for org-press.
What is a Block Plugin?
A block plugin transforms org-mode source blocks into rendered output. When org-press encounters a code block, it checks each plugin to see if it matches, then calls the plugin's transform function to generate HTML, JavaScript, or CSS.
Plugin Interface
Every plugin implements the BlockPlugin interface:
interface BlockPlugin {
/** Unique plugin identifier */
name: string;
/** Languages this plugin handles (e.g., ["javascript", "typescript"]) */
languages?: string[];
/** Custom matching logic (alternative to languages) */
matches?(block: CodeBlock): boolean;
/** Transform the block into renderable output */
transform(block: CodeBlock, ctx: TransformContext): Promise<TransformResult>;
/** Optional: Server-side execution at build time */
onServer?(block: CodeBlock, ctx: TransformContext): Promise<ServerResult>;
/** Optional: CLI command to add to orgp */
cli?: CliCommand;
}
Your First Plugin
Here's a minimal plugin that renders blocks with :use greeting:
// greeting-plugin.ts
import type { BlockPlugin } from "org-press";
export const greetingPlugin: BlockPlugin = {
name: "greeting",
// Match blocks with :use greeting parameter
matches(block) {
return block.parameters.use === "greeting";
},
// Transform the block content
async transform(block, ctx) {
const name = block.value.trim() || "World";
return {
html: `<div class="greeting">Hello, ${name}!</div>`,
css: `.greeting { font-size: 2rem; color: blue; }`,
};
},
};
Usage in org file:
#+begin_src text :use greeting
Alice
#+end_src
Matching Blocks
Plugins can match blocks in two ways:
By Language
export const myPlugin: BlockPlugin = {
name: "my-plugin",
languages: ["python", "python3"],
// ...
};
Matches: #+begin_src python and #+begin_src python3
By Parameter
export const myPlugin: BlockPlugin = {
name: "my-plugin",
matches(block) {
// Match any block with :use myplugin
return block.parameters.use === "myplugin";
},
// ...
};
Matches: #+begin_src javascript :use myplugin
Combined Matching
export const myPlugin: BlockPlugin = {
name: "my-plugin",
languages: ["json"],
matches(block) {
// Only match JSON blocks with :use diagram
return block.language === "json" &&
block.parameters.use === "diagram";
},
// ...
};
Transform Context
The ctx parameter provides context about the block and project:
interface TransformContext {
/** Block parameters from org (e.g., { exports: "both", use: "server" }) */
parameters: Record<string, string>;
/** Full path to the org file */
filePath: string;
/** Project configuration */
config: OrgPressConfig;
/** Cache directory for generated files */
cacheDir: string;
/** Block name if provided via #+NAME: */
blockName?: string;
}
Transform Results
The transform function returns what to render:
HTML Only
async transform(block, ctx) {
return {
html: `<div class="my-component">${block.value}</div>`,
};
}
HTML + CSS
async transform(block, ctx) {
return {
html: `<div class="chart"></div>`,
css: `.chart { width: 100%; height: 300px; }`,
};
}
JavaScript Execution
async transform(block, ctx) {
return {
// Code runs in browser when page loads
code: `
const data = ${JSON.stringify(block.value)};
console.log("Loaded:", data);
`,
};
}
With React Wrapper
For interactive components, provide a wrapper:
async transform(block, ctx) {
return {
// Data passed to wrapper component
code: JSON.stringify({ value: block.value }),
// React component to render
wrapper: {
path: "@my-plugin/components",
exportName: "MyInteractiveComponent",
},
};
}
Creating Wrapper Components
Wrappers are React components that receive the block data:
// components/MyInteractiveComponent.tsx
import React, { useState, useEffect } from "react";
interface Props {
data: string; // The code string from transform
container: HTMLElement; // DOM element to render into
}
export function MyInteractiveComponent({ data, container }: Props) {
const [state, setState] = useState(JSON.parse(data));
return (
<div className="interactive-wrapper">
<pre>{JSON.stringify(state, null, 2)}</pre>
<button onClick={() => setState({ ...state, clicked: true })}>
Click me
</button>
</div>
);
}
Server-Side Execution
For build-time processing, implement onServer:
export const dataPlugin: BlockPlugin = {
name: "data-loader",
matches(block) {
return block.parameters.use === "data";
},
// Runs at build time with Node.js access
async onServer(block, ctx) {
const fs = await import("fs/promises");
const filePath = block.value.trim();
const content = await fs.readFile(filePath, "utf-8");
return {
result: JSON.parse(content),
};
},
// Transform receives server result
async transform(block, ctx) {
// ctx.serverResult contains what onServer returned
return {
html: `<pre>${JSON.stringify(ctx.serverResult, null, 2)}</pre>`,
};
},
};
Adding CLI Commands
Plugins can extend the orgp CLI:
export const testPlugin: BlockPlugin = {
name: "test",
cli: {
command: "test",
description: "Run tests in :use test blocks",
options: [
{ flag: "-w, --watch", description: "Watch mode" },
{ flag: "--coverage", description: "Collect coverage" },
{ flag: "-t, --filter <pattern>", description: "Filter tests" },
],
async action(args, config, plugins) {
console.log("Running tests...");
console.log("Options:", args);
// Find all test blocks
// Run vitest or your test runner
// Report results
},
},
};
Usage:
orgp test --watch
orgp test --coverage
orgp test -t "user validation"
Complete Example: Chart Plugin
Here's a full plugin that renders charts using Chart.js:
// chart-plugin.ts
import type { BlockPlugin } from "org-press";
export const chartPlugin: BlockPlugin = {
name: "chart",
languages: ["json"],
matches(block) {
return block.parameters.use === "chart";
},
async transform(block, ctx) {
const chartId = `chart-${Math.random().toString(36).slice(2)}`;
const config = JSON.parse(block.value);
return {
html: `<canvas id="${chartId}" style="max-width: 600px;"></canvas>`,
code: `
import Chart from 'chart.js/auto';
const ctx = document.getElementById('${chartId}');
new Chart(ctx, ${JSON.stringify(config)});
`,
css: `
#${chartId} {
margin: 1rem 0;
}
`,
};
},
};
Usage:
#+begin_src json :use chart
{
"type": "bar",
"data": {
"labels": ["Red", "Blue", "Yellow"],
"datasets": [{
"label": "Votes",
"data": [12, 19, 3]
}]
}
}
#+end_src
Plugin Configuration
Plugins can accept options:
// Plugin factory function
export function createChartPlugin(options: ChartPluginOptions = {}) {
const { defaultType = "bar", theme = "light" } = options;
return {
name: "chart",
async transform(block, ctx) {
const config = JSON.parse(block.value);
config.type = config.type || defaultType;
// Apply theme...
return { /* ... */ };
},
} satisfies BlockPlugin;
}
// Usage in config
export default {
plugins: [
createChartPlugin({ defaultType: "line", theme: "dark" }),
],
};
Writing Plugins as Org Files (Literate Plugins)
Plugins can be written as literate programming files that combine documentation, examples, and code. The code is extracted and compiled using orgp build --block.
Literate Plugin Structure
my-literate-plugin/
├── index.org # Literate source (plugin + docs + examples)
├── package.json
└── dist/ # Built output
├── index.js
└── index.d.ts
Example index.org
#+beginexport html <pre><code class="language-org">#+TITLE: My Plugin #+DESCRIPTION: A literate plugin example
Documentation
This plugin does amazing things...
Plugin Implementation
#+NAME: plugin #+beginsrc typescript import type { BlockPlugin } from "org-press";
export const myPlugin: BlockPlugin = { name: "my-plugin", async transform(block, ctx) { return { html: "<div>" + block.value + "</div>" }; }, };
export default myPlugin; #+endsrc
Examples
...usage examples here... </code></pre> #+endexport
Building the Plugin
# Extract and compile the "plugin" block to dist/
orgp build index.org --block plugin --out dist/
# The generated files:
# dist/plugin.js - Compiled JavaScript
# dist/plugin.d.ts - TypeScript declarations
# dist/index.js - Re-exports all blocks
# dist/index.d.ts
package.json for Literate Plugins
{
"name": "my-org-press-plugin",
"type": "module",
"exports": {
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }
},
"scripts": {
"build": "orgp build index.org --block plugin --out dist/"
},
"peerDependencies": {
"org-press": "^0.2.0"
}
}
See the ECharts Plugin for a complete literate plugin example.
Publishing Your Plugin
Traditional Package Structure
my-org-press-plugin/
├── src/
│ ├── index.ts # Plugin export
│ └── components/ # Wrapper components (if any)
├── package.json
├── tsconfig.json
└── README.md
package.json
{
"name": "my-org-press-plugin",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./components": "./dist/components/index.js"
},
"peerDependencies": {
"org-press": "^0.2.0"
}
}
Export Your Plugin
// src/index.ts
export { myPlugin } from "./plugin";
export { createMyPlugin } from "./plugin";
// Re-export types if useful
export type { MyPluginOptions } from "./types";
Best Practices
- Use unique names - Prefix with your package name to avoid conflicts
- Handle errors gracefully - Return helpful error HTML instead of throwing
- Support rendering modes - Respect
dom,sourceOnly,silent,raw - Document your syntax - Show example org blocks in README
- Test your plugin - Use
:use testblocks to verify behavior - Minimize dependencies - Keep bundle size small
- Support SSR - Ensure wrappers work with server-side rendering
See Also
- Plugin API Reference - Complete API documentation
- CLI Plugins - Adding CLI commands
- Excalidraw Plugin - Example: diagram plugin
- JSCAD Plugin - Example: 3D modeling plugin
- Test Plugin - Example: CLI integration