@emkodev/emkoma
v0.4.2
Published
Common Markdown — one rendering path for SSR, preview, and editor
Readme
emkoma
Common Markdown — one rendering path for SSR, preview, and editor.
Pure TypeScript, zero dependencies. Published as @emkodev/emkoma on npm.
Install
npm install @emkodev/emkomaimport { renderMarkdown } from "@emkodev/emkoma/render";Usage
SSR / string rendering
import { renderMarkdown } from "@emkodev/emkoma/render";
const html = renderMarkdown("# Hello\n\nWorld");
// <h1 id="hello">Hello</h1>\n<p>World</p>Browser preview
<script type="module">
import "@emkodev/emkoma/element/register";
</script>
<emkoma-document>
<pre>
# Hello
This is **bold** and *italic* text.
</pre>
</emkoma-document>Browser editor
<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
→ joinBlockHandler interface
interface BlockHandler {
render(raw: string, attrs?: Record<string, string>): string;
serialize(raw: string, attrs?: Record<string, string>): string;
createEditor?: (container, raw, attrs, onCommit, onCancel) => void;
}Built-in handlers: paragraph, heading, code-block, hr, blockquote,
list. Widget blocks (widget:*) are auto-resolved on demand.
Custom blocks
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:
```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/render | renderMarkdown() — top-level SSR entry | Yes |
| @emkodev/emkoma/block | identifyBlocks(), block handlers, registry | Yes |
| @emkodev/emkoma/inline | renderInline(), escapeHtml(), editing utilities | Mostly* |
| @emkodev/emkoma/element | EmkomaDocumentElement class (no side effects) | No |
| @emkodev/emkoma/element/register | customElements.define() side effect | No |
* Inline editing utilities (applyHighlights, walkTextNodes, etc.) require
DOM.
Build & Test
bun run build # compile to dist/
bun test # run all tests
bun run check # type-check all entry pointsLicense
MIT
