docs

outl docs

ASCII Box

Capability: content-transformer:text · Source on GitHub

Wraps the body of a ```box fence in a drawn ASCII box. The width tracks the longest line, every line is padded to match, and a one-space gutter sits inside the border.

What it demonstrates

A text content-transformer. The plugin calls ctx.content.register("box", fn) during activate. When a client renders a ```box fence, the host runs fn with the fence body and draws the descriptor it returns.

The transformer is a pure function: fn(body) returns { kind: "text", content } (or null to decline). Because the descriptor is kind: "text", the result renders on every client — desktop, mobile, and the TUI/CLI (no webview required). Contrast with kind: "rich", which is HTML run in a GUI-only sandboxed iframe (see the bars example).

The host knows nothing about “boxes”; it only transports the string the plugin produced. Want rounded corners or a double border? Swap the glyphs in boxify.

Example

```box
hello
world!
```

renders into:

┌────────┐
│ hello  │
│ world! │
└────────┘

The code

/**
 * ASCII Box — a text content-transformer for outl.
 *
 * Registers the `box` code-fence language. When a client renders a ```box
 * fence, the host runs `boxify` with the fence body and draws the descriptor
 * we return. Because we return `kind: "text"`, the result renders on *every*
 * client — desktop, mobile, and the TUI/CLI (no webview required).
 *
 * The transformer is a pure function: given the body, it returns a box drawn
 * around the text. The host knows nothing about "boxes" — it only transports
 * the string we produced. Want rounded corners or a double border? Swap the
 * glyphs below; it's your transformer, not a fixed catalog.
 *
 * Needs the `content-transformer:text` capability. No permissions —
 * transformers are pure and never mutate the workspace.
 */

import { definePlugin, type PluginContext } from "@outl/plugin-sdk";

const TOP_LEFT = "┌";
const TOP_RIGHT = "┐";
const BOTTOM_LEFT = "└";
const BOTTOM_RIGHT = "┘";
const HORIZONTAL = "─";
const VERTICAL = "│";

/**
 * Draw an ASCII box around `body`.
 *
 * The box is as wide as the longest line, every line is right-padded to that
 * width, and a one-space gutter sits inside the border. An empty body still
 * draws a minimal box so the fence never renders as nothing.
 */
function boxify(body: string): string {
  // Drop a single trailing newline (fence bodies usually carry one) but keep
  // intentional blank lines in the middle.
  const lines = body.replace(/\n$/, "").split("\n");
  const width = lines.reduce((max, line) => Math.max(max, line.length), 0);

  const top = TOP_LEFT + HORIZONTAL.repeat(width + 2) + TOP_RIGHT;
  const bottom = BOTTOM_LEFT + HORIZONTAL.repeat(width + 2) + BOTTOM_RIGHT;
  const middle = lines.map(
    (line) => VERTICAL + " " + line.padEnd(width, " ") + " " + VERTICAL,
  );

  return [top, ...middle, bottom].join("\n");
}

export default definePlugin({
  activate(ctx: PluginContext) {
    ctx.content.register("box", (body) => ({
      kind: "text",
      content: boxify(body),
    }));
  },
});

Try it

outl -w <workspace> plugin install ./examples/box --yes
# put a ```box fence in a page and open it on any client (text renders everywhere, TUI included)