⚠️Early Alpha — Org-press is experimental. Perfect for hackers and tinkerers, not ready for production. Documentation may be incomplete or inaccurate.

Creating Plugins

This guide covers everything you need to build plugins for org-press.

Quick Start

The plugin system provides factory functions for common patterns. Here's a quick overview:

import {
  CreateBlock,           // Code block handlers (by language)
  CreateDrawer,          // Drawer element handlers
  CreateTransformer,     // :use pipeline stages
  CreateCommand,         // CLI commands
  CreateVitePlugin,      // Register Vite plugins
  CreateMiddlewarePlugin,// Dev server middleware
  CreatePlugin,          // Full control escape hatch
} from "org-press";

// Block: Handle #+begin_src mylang blocks
const myBlock = CreateBlock("mylang", {
  transform: (code, ctx) => ({ html: `<pre>${code}</pre>` }),
});

// Drawer: Handle :NOTE: and :TIP: drawers
const noteDrawer = CreateDrawer(["NOTE", "TIP"], (drawer) =>
  `<aside class="${drawer.name.toLowerCase()}">${drawer.html}</aside>`
);

// Transformer: Add :use mymode to pipeline
const myTransformer = CreateTransformer("mymode", {
  onBuild: (input, ctx) => ({ html: `<div>${input.html}</div>` }),
});

// Command: Add `orgp mycommand` to CLI
const myCommand = CreateCommand("mycommand", {
  description: "Do something",
  execute: async (args, ctx) => 0,
});

CreateBlock - Code Block Plugins

Use CreateBlock to handle #+begin_src language blocks.

Basic Usage

import { CreateBlock } from "org-press";

// Handle a single language
const htmlBlock = CreateBlock("html", {
  transform: (code, ctx) => ({
    html: code, // Raw HTML output
  }),
});

// Handle multiple languages
const jsBlock = CreateBlock(["javascript", "js"], {
  transform: (code, ctx) => ({
    html: `<pre class="language-js"><code>${escapeHtml(code)}</code></pre>`,
  }),
});

Block Context

The ctx parameter provides useful information about the block:

CreateBlock("mylang", {
  transform: (code, ctx) => {
    // ctx.blockId   - Unique ID for DOM targeting (e.g., "block-0")
    // ctx.language  - Block language ("mylang")
    // ctx.params    - Parsed block parameters
    // ctx.orgFilePath - Relative path to the org file
    // ctx.blockIndex  - 0-based index of this block
    // ctx.base      - Base URL path

    return {
      html: `<div id="${ctx.blockId}">...</div>`,
    };
  },
});

Transform Results

The transform function can return various outputs:

// HTML only
return { html: `<div>...</div>` };

// HTML + CSS
return {
  html: `<div class="chart">...</div>`,
  css: `.chart { width: 100%; }`,
};

// HTML + JavaScript
return {
  html: `<div id="${ctx.blockId}">...</div>`,
  script: `document.getElementById("${ctx.blockId}").innerHTML = "Hello";`,
};

// Transformed code (for executable blocks)
return {
  code: transpileCode(code),
  html: `<pre>${code}</pre>`,
};

Complete Example: Chart Plugin

import { CreateBlock } from "org-press";

export const chartPlugin = CreateBlock("chart", {
  transform: (code, ctx) => {
    const config = JSON.parse(code);
    const chartId = ctx.blockId;

    return {
      html: `<canvas id="${chartId}" style="max-width: 600px;"></canvas>`,
      script: `
        import Chart from 'chart.js/auto';
        new Chart(document.getElementById('${chartId}'), ${JSON.stringify(config)});
      `,
      css: `#${chartId} { margin: 1rem 0; }`,
    };
  },
});

Usage:

#+begin_src chart
{
  "type": "bar",
  "data": {
    "labels": ["A", "B", "C"],
    "datasets": [{ "data": [10, 20, 30] }]
  }
}
#+end_src

CreateDrawer - Drawer Plugins

Use CreateDrawer to handle org-mode drawers (content between :DRAWER_NAME: and :END:).

Basic Usage

import { CreateDrawer } from "org-press";

// Single drawer type
const aiDrawer = CreateDrawer("AI", (drawer) =>
  `<details class="ai-content">
    <summary>AI Generated</summary>
    <div>${drawer.html}</div>
  </details>`
);

// Multiple drawer types
const calloutDrawer = CreateDrawer(
  ["NOTE", "INFO", "TIP", "WARNING"],
  (drawer) => {
    const type = drawer.name.toLowerCase();
    return `<aside class="callout callout--${type}" role="note">
      <strong>${drawer.name}</strong>
      ${drawer.html}
    </aside>`;
  }
);

// Remove a drawer from output
const hideProperties = CreateDrawer("PROPERTIES", () => null);

Drawer Node Properties

CreateDrawer("CUSTOM", (drawer, ctx) => {
  // drawer.name     - Drawer name (e.g., "CUSTOM")
  // drawer.children - Raw AST children
  // drawer.html     - Pre-rendered HTML content

  // ctx.orgFilePath - Path to the org file
  // ctx.config      - Org-press configuration
  // ctx.base        - Base URL path

  return `<div class="custom">${drawer.html}</div>`;
});

Complete Example: Collapsible Quote

import { CreateDrawer } from "org-press";

export const quoteDrawer = CreateDrawer("QUOTE", (drawer) => {
  return `<blockquote class="org-quote">
    ${drawer.html}
  </blockquote>`;
});

// With CSS
export const styledQuoteDrawer = CreateDrawer("QUOTE", (drawer) => {
  return `<style>
    .org-quote {
      border-left: 4px solid #3b82f6;
      padding-left: 1rem;
      font-style: italic;
      color: #6b7280;
    }
  </style>
  <blockquote class="org-quote">${drawer.html}</blockquote>`;
});

CreateTransformer - Pipeline Stages

Use CreateTransformer to add stages to the :use parameter pipeline.

Understanding the Pipeline

When you write :use dom | withSourceCode, each name is a transformer stage:

#+begin_src javascript :use dom | withSourceCode
console.log("Hello");
#+end_src




Basic Transformer

import { CreateTransformer } from "org-press";

// Simple HTML wrapper
const wrapperTransformer = CreateTransformer("myWrapper", {
  onBuild: (input, ctx) => ({
    html: `<div class="wrapper">${input.html}</div>`,
  }),
});

// Usage: :use dom | myWrapper

Transformer Input/Output

CreateTransformer("myTransformer", {
  onBuild: (input, ctx) => {
    // Input from previous stage:
    // input.code     - Original source code
    // input.language - Block language
    // input.params   - Block parameters
    // input.html     - HTML from previous transformer
    // input.result   - Result from onServer (if any)

    // Context:
    // ctx.id         - Unique block ID
    // ctx.code       - Original source code
    // ctx.orgFilePath - Path to org file
    // ctx.blockIndex - Block index
    // ctx.params     - Block parameters

    return {
      html: `<div id="${ctx.id}">${input.html}</div>`,
      script: `console.log("Block ${ctx.id} loaded");`,
      css: `.block-${ctx.id} { padding: 1rem; }`,
    };
  },
});

Server-Side Execution

For transformers that need Node.js access (file system, database, etc.):

CreateTransformer("serverData", {
  // Runs in Node.js at build time
  onServer: async (code, ctx) => {
    const fs = await import("fs/promises");
    const data = await fs.readFile(code.trim(), "utf-8");
    return JSON.parse(data);
  },

  // Receives result from onServer
  onBuild: (input, ctx) => ({
    html: `<pre>${JSON.stringify(input.result, null, 2)}</pre>`,
  }),
});

Client-Side Interactivity

For browser-side code, use the client option:

// Inline client script
CreateTransformer("interactive", {
  onBuild: (input, ctx) => ({
    html: `<button id="${ctx.id}">Click me</button>`,
    clientData: { clicks: 0 },
  }),
  client: `
    export function onMount(element, ctx) {
      let clicks = ctx.data.clicks;
      element.addEventListener('click', () => {
        clicks++;
        element.textContent = \`Clicked \${clicks} times\`;
      });
    }
  `,
});

// Dynamic import for complex clients (better code splitting)
CreateTransformer("complexClient", {
  onBuild: (input, ctx) => ({
    html: `<div id="${ctx.id}"></div>`,
  }),
  client: () => import('./my-client-module'),
});

Client Module Interface

// my-client-module.ts
export function onMount(element: HTMLElement, ctx: ClientContext) {
  // Called when element is mounted in DOM
  // Return cleanup function if needed
  return () => {
    // Cleanup on unmount
  };
}

export function onUnmount(element: HTMLElement) {
  // Called when element is removed
}

export function onUpdate(element: HTMLElement, ctx: ClientContext) {
  // Called during HMR updates
}

CreateVitePlugin - Vite Integration

Use CreateVitePlugin to register Vite plugins from org-press plugins. This allows you to add custom transforms, virtual modules, or other Vite functionality.

Basic Usage

import { CreateVitePlugin } from "org-press";

// Add a custom file transform
const myTransform = CreateVitePlugin("my-transform", {
  enforce: "pre",
  transform(code, id) {
    if (id.endsWith(".special")) {
      return transformSpecialFile(code);
    }
  },
});

Virtual Modules

A common use case is creating virtual modules:

import { CreateVitePlugin } from "org-press";

const virtualConfig = CreateVitePlugin("virtual-config", {
  resolveId(id) {
    if (id === "virtual:my-config") {
      return "\0virtual:my-config";
    }
  },
  load(id) {
    if (id === "\0virtual:my-config") {
      const config = { apiUrl: "/api", version: "1.0" };
      return `export default ${JSON.stringify(config)}`;
    }
  },
});

// Usage in your code:
// import config from "virtual:my-config";

Vite Plugin Configuration

The config accepts all standard Vite plugin hooks:

CreateVitePlugin("full-example", {
  // When to run: 'pre' (before transforms) | 'post' (after)
  enforce: "pre",

  // When to apply: 'build' | 'serve' | undefined (both)
  apply: "serve",

  // Standard Vite hooks
  configResolved(config) {
    // Access resolved Vite config
  },

  transform(code, id) {
    // Transform files
  },

  resolveId(id) {
    // Resolve virtual modules
  },

  load(id) {
    // Load virtual modules
  },

  // ... any other Vite plugin hook
});

CreateMiddlewarePlugin - Dev Server Middleware

Use CreateMiddlewarePlugin to add custom API routes or middleware to the Vite dev server.

Basic Usage

import { CreateMiddlewarePlugin } from "org-press";

// Simple handler shorthand
const healthCheck = CreateMiddlewarePlugin("/api/health", (req, res) => {
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
});

Method Filtering

Restrict which HTTP methods the middleware handles:

import { CreateMiddlewarePlugin } from "org-press";

// Only handle POST requests
const dataApi = CreateMiddlewarePlugin("/api/data", {
  methods: ["POST"],
  handler: async (req, res, next) => {
    // Collect request body
    let body = "";
    req.on("data", (chunk) => (body += chunk));
    req.on("end", () => {
      const data = JSON.parse(body);
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify({ received: data }));
    });
  },
});

Complete Example: Search API

import { CreateMiddlewarePlugin } from "org-press";

export const searchApi = CreateMiddlewarePlugin("/api/search", {
  methods: ["GET"],
  handler: async (req, res, next) => {
    const url = new URL(req.url!, `http://${req.headers.host}`);
    const query = url.searchParams.get("q") || "";

    // Perform search (using your search logic)
    const results = await searchContent(query);

    res.setHeader("Content-Type", "application/json");
    res.setHeader("Cache-Control", "public, max-age=60");
    res.end(JSON.stringify(results));
  },
});

Middleware Options

interface MiddlewareOptions {
  /** HTTP methods to handle. Default: all methods */
  methods?: Array<"GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "OPTIONS">;

  /** Handler function */
  handler: (
    req: IncomingMessage,
    res: ServerResponse,
    next: () => void
  ) => void | Promise<void>;
}

// If you don't need method filtering, use the shorthand:
CreateMiddlewarePlugin("/api/ping", (req, res) => res.end("pong"));

CreateCommand - CLI Commands

Use CreateCommand to add commands to the orgp CLI.

Basic Command

import { CreateCommand } from "org-press";

const fmtCommand = CreateCommand("fmt", {
  description: "Format code blocks with Prettier",
  execute: async (args, ctx) => {
    console.log("Formatting files in:", ctx.contentDir);
    // Do formatting...
    return 0; // Exit code: 0 = success, non-zero = error
  },
});

// Usage: orgp fmt

Command with Arguments

const lintCommand = CreateCommand("lint", {
  description: "Lint org files for issues",
  args: [
    { name: "fix", type: "boolean", alias: "f", description: "Auto-fix issues" },
    { name: "pattern", type: "string", default: "**/*.org", description: "Glob pattern" },
    { name: "verbose", type: "boolean", alias: "v", description: "Verbose output" },
  ],
  execute: async (args, ctx) => {
    const fix = args.fix as boolean;
    const pattern = args.pattern as string;

    console.log(`Linting ${pattern}${fix ? " (with auto-fix)" : ""}`);

    // Positional arguments available in args._
    const [target] = args._;

    // ctx provides:
    // ctx.config     - Org-press configuration
    // ctx.projectRoot - Project root directory
    // ctx.contentDir - Content directory path

    return 0;
  },
});

// Usage:
// orgp lint
// orgp lint --fix
// orgp lint -f --pattern "docs/**/*.org"
// orgp lint -v somefile.org

CreatePlugin - Full Control Escape Hatch

For complex plugins that need access to multiple features, use CreatePlugin:

import { CreatePlugin } from "org-press";

const aiPlugin = CreatePlugin("ai-assistant", (ctx) => {
  // Register drawer handler
  ctx.onDrawer("AI", (drawer) =>
    `<details class="ai">${drawer.html}</details>`
  );

  // Register block handler
  ctx.onBlock("ai-prompt", async (block, blockCtx) => ({
    html: `<div class="ai-prompt">${block.value}</div>`,
  }));

  // Add CLI command
  ctx.addCommand("ai", {
    description: "AI assistant for code blocks",
    execute: async (args) => {
      console.log("AI command running...");
      return 0;
    },
  });

  // Add transformer for :use ai | ...
  ctx.addTransformer("ai", {
    onBuild: (input, ctx) => ({
      html: `<div class="ai-enhanced">${input.html}</div>`,
    }),
  });

  // Hook into pipeline stages
  ctx.hook("build:start", (payload) => {
    console.log("Build started!");
  });

  ctx.hook("render:after", (payload) => {
    // Modify final HTML
    payload.metadata = { ...payload.metadata, aiProcessed: true };
  });

  // Share state between plugins
  ctx.provide("ai:config", { model: "gpt-4" });

  // Access shared state from other plugins
  const sharedConfig = ctx.inject("other-plugin:config");

  // Access project config
  console.log("Content dir:", ctx.config.contentDir);
});

PluginContext API Reference

interface PluginContext {
  // Element handlers
  onElement(type: string, handler: ElementHandler): void;
  onDrawer(name: string | string[], handler: DrawerHandler): void;
  onBlock(language: string | string[], handler: BlockHandler): void;

  // Pipeline hooks
  hook(stage: PipelineStage, handler: StageHandler): void;

  // Unified ecosystem
  useRehype(plugin: RehypePlugin, options?: unknown): void;
  useUniorg(plugin: UniorgPlugin, options?: unknown): void;

  // Extensions
  addCommand(name: string, options: CommandOptions): void;
  addTransformer(name: string, options: TransformerOptions): void;

  // Shared state
  provide<T>(key: string, value: T): void;
  inject<T>(key: string): T | undefined;

  // Utilities
  config: OrgPressConfig;
  projectRoot: string;
  logger: Logger;
}

// Pipeline stages
type PipelineStage =
  | "parse:before"    // Before uniorg parsing
  | "parse:after"     // After uniorg, before transforms
  | "transform:before"// Before element transforms
  | "transform:after" // After element transforms
  | "render:before"   // Before rehype processing
  | "render:after"    // After HTML generation
  | "build:start"     // Build started
  | "build:end";      // Build finished

Plugin Presets

Org-press provides preset arrays of plugins for common configurations:

import {
  defaultPlugins,  // Standard plugins for content rendering
  minimalPlugins,  // Empty array for full control
  cliPlugins,      // CLI command plugins (fmt, lint, type-check)
} from "org-press";

// Default configuration
export default {
  plugins: [...defaultPlugins],
};

// Add your own plugins
export default {
  plugins: [
    ...defaultPlugins,
    myCustomPlugin,
    CreateDrawer("NOTE", (d) => `<aside>${d.html}</aside>`),
  ],
};

// Minimal - only what you need
export default {
  plugins: [
    ...minimalPlugins,
    javascriptPlugin,
    myPlugin,
  ],
};

What's in defaultPlugins

PluginDescription
domPluginDOM rendering for :use dom blocks
javascriptPluginJavaScript/JS block handling
typescriptPluginTypeScript/TS block handling
cssPluginCSS block handling
serverPluginServer-side execution for :use server

What's in cliPlugins

PluginDescription
fmtPluginorgp fmt - Format code blocks
lintPluginorgp lint - Lint org files
typeCheckPluginorgp type-check - TypeScript checking

Testing Plugins

Test your plugins using :use test blocks:

#+begin_src typescript :use test
import { describe, it, expect } from "vitest";
import { myPlugin } from "./my-plugin";

describe("myPlugin", () => {
  it("transforms code correctly", async () => {
    const result = await myPlugin._config.transform("input", {
      blockId: "test-1",
      language: "mylang",
      params: {},
      orgFilePath: "test.org",
      blockIndex: 0,
      base: "/",
    });
    expect(result.html).toContain("input");
  });
});
#+end_src

Run tests with orgp test or directly with Vitest.

Publishing Plugins

Package Structure

my-org-press-plugin/
├── src/
│   └── index.ts        # Plugin exports
├── 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": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "peerDependencies": {
    "org-press": "^0.3.0"
  }
}

Export Your Plugin

// src/index.ts
export { myPlugin } from "./plugin";
export { createMyPlugin } from "./plugin"; // Factory for options

// Re-export types for consumers
export type { MyPluginOptions } from "./types";

Best Practices

  1. Use the simplest factory - Don't use CreatePlugin when CreateDrawer or CreateBlock suffices
  2. Unique names - Prefix plugin names with your package name to avoid conflicts
  3. Handle errors gracefully - Return helpful error HTML instead of throwing
  4. Support rendering modes - Respect :use sourceOnly, :use silent, etc.
  5. Document your syntax - Show example org blocks in your README
  6. Test your plugin - Use :use test blocks to verify behavior
  7. Minimize dependencies - Keep bundle size small
  8. Support SSR - Ensure client code handles server-side rendering

See Also