# emkoma

Common Markdown — one rendering path for SSR, preview, and editor.

Pure TypeScript, zero dependencies. Published as `@emkodev/emkoma` on npm.

## Install

```bash
npm install @emkodev/emkoma
```

```ts
import { renderMarkdown } from "@emkodev/emkoma/render";
```

## Usage

### SSR / string rendering

```ts
import { renderMarkdown } from "@emkodev/emkoma/render";

const html = renderMarkdown("# Hello\n\nWorld");
// <h1 id="hello">Hello</h1>\n<p>World</p>
```

### Browser preview

```html
<script type="module">
  import "@emkodev/emkoma/element/register";
</script>

<emkoma-document>
  <pre>
# Hello

This is **bold** and *italic* text.
  </pre>
</emkoma-document>
```

### Browser editor

```html
<emkoma-document editable>
  <pre>
# Editable document

Click any block to edit it.
  </pre>
</emkoma-document>
```

## Design decisions

- **No raw inline HTML.** emkoma does not support HTML tags in markdown — no
  `<b>`, no `<div>`, no HTML entities. Markdown syntax is the only way to format
  content. Interactive elements (form controls, widgets) enter the document
  exclusively through fenced code block namespaces (`widget:`, `html:`).

## Architecture

Block handlers are composable pure functions — no custom elements per block
type. Only `<emkoma-document>` is a custom element (shadow DOM controller).
Child blocks are plain `<div>`s inside the shadow root.

```
markdown string
  → identifyBlocks(markdown)  → BlockChunk[]
  → handlers[type].render()   → HTML strings
  → join
```

### BlockHandler interface

```ts
interface BlockHandler {
  render(raw: string, attrs?: Record<string, string>): string;
  serialize(raw: string, attrs?: Record<string, string>): string;
  identify?: (lines: string[], index: number) => IdentifyResult | null;
}
```

`BlockHandler` is deliberately DOM-free so `./block` can be consumed by SSR and
other non-DOM runtimes. Editing UI is a separate concern:

```ts
import { registerEditor } from "@emkodev/emkoma/editor";

registerEditor("my-block", (container, raw, attrs, onCommit, onCancel) => {
  // build editing UI inside `container`
});
```

Built-in handlers: `paragraph`, `heading`, `code-block`, `hr`, `blockquote`,
`list`. Widget blocks (`widget:*`) are auto-resolved on demand.

### Custom blocks

````ts
import { registerHandler, unregisterHandler } from "@emkodev/emkoma/block";

registerHandler("my-block", {
  render(raw, attrs) {
    return `<div class="my-block">${raw}</div>`;
  },
  serialize(raw, attrs) {
    return "```my-block\n" + raw + "\n```";
  },
});

// Remove when no longer needed
unregisterHandler("my-block");
````

### Code block metadata

Fence info strings support key-value pairs after the language:

````md
```typescript filepath=src/greet.ts
export function greet() {}
```
````

### Source mode (power users)

When `editable`, the toolbar includes a Visual/Source toggle. Source mode shows
the raw markdown in a textarea for direct editing.

## Exports

| Entry point                        | What                                             | DOM-free |
| ---------------------------------- | ------------------------------------------------ | -------- |
| `@emkodev/emkoma`                  | Aggregate of `render` + `block` + `inline`       | Yes      |
| `@emkodev/emkoma/render`           | `renderMarkdown()` — top-level SSR entry         | Yes      |
| `@emkodev/emkoma/block`            | `identifyBlocks()`, handlers, registry, `TableData` | Yes   |
| `@emkodev/emkoma/inline`           | `renderInline()`, `escapeHtml()`, `safeUrl()`    | Yes      |
| `@emkodev/emkoma/editor`           | Block editors, editor registry, inline editing   | No       |
| `@emkodev/emkoma/element`          | `EmkomaDocumentElement` class (no side effects)  | No       |
| `@emkodev/emkoma/element/register` | `customElements.define()` side effect            | No       |

Every DOM-free entry point is verified as such by `bun run check:pure`, so the
table cannot drift from reality.

### Tables

Producers should build a `TableData` and let emkoma emit the fence, rather than
assembling JSON and delimiters by hand:

```ts
import { serializeTable, parseTable, type TableData } from "@emkodev/emkoma";

const data: TableData = { head: ["Name", "Qty"], body: [["Bolt", 4]] };
const fence = serializeTable(data); // ```table\n{ … }\n```
parseTable(fence); // → TableData
```

Head strings are literal label text — nothing is reserved, so a label may
contain colons anywhere. Column alignment is a parallel `align` array, omitted
entirely for an all-left table:

```ts
serializeTable({
  head: ["Name", "Qty"],
  align: ["left", "right"],
  body: [["Bolt", 4]],
});
```

`serializeTable` widens the fence past any backtick run in the content, so a
table containing a fenced sample cannot close its own block.

### Import maps

`dist/` is self-contained — every specifier is relative and carries a `.js`
extension, with no bare specifiers — so it can be vendored and served directly:

```json
{ "imports": { "@emkodev/emkoma/": "/vendor/emkoma/dist/" } }
```

## Build & Test

```bash
bun run build      # compile to dist/
bun test           # run all tests
bun run check      # type-check all entry points
bun run fmt        # format with Biome
bun run lint       # lint with Biome
```

## License

MIT
