@owomark/core
v0.1.4
Published
Framework-agnostic core engine for the OwoMark editor.
Readme
@owomark/core
Framework-agnostic Markdown editor engine. Provides document model, incremental rendering, input handling, commands, and selection management for a single-layer contenteditable editing experience.
Install the official package: @owomark/core.
Install
npm install @owomark/coreQuick Start
Pure Logic Core (no DOM)
import { createOwoMarkCore } from '@owomark/core';
const core = createOwoMarkCore({ initialMarkdown: '# Hello' });
// Intent-driven input API
const result = core.applyBeforeInput(
{ inputType: 'insertText', data: 'X' },
{ anchor: 7, focus: 7 },
);
// result.action: 'handled' | 'allow-native' | 'composition-sync'
console.log(core.getMarkdown()); // '# HelloX'
core.destroy();With DOM (use @owomark/view)
import { createOwoMarkCore } from '@owomark/core';
import { createOwoMarkView } from '@owomark/view';
const core = createOwoMarkCore({ initialMarkdown: '# Hello\n\nStart typing...' });
const view = createOwoMarkView(core, document.getElementById('editor')!);
core.onChange((markdown) => {
console.log('Content changed:', markdown);
});
// Cleanup
view.destroy();
core.destroy();The standalone DOM editor API is also available through @owomark/view:
import { createOwoMarkVanillaEditor } from '@owomark/view';
const editor = createOwoMarkVanillaEditor();
editor.setMarkdown('# Hello');
editor.mount(document.getElementById('editor')!);
editor.destroy();Core API
State
| Method | Description |
|--------|-------------|
| getMarkdown(): string | Get current Markdown content |
| getDocument(): OwoMarkDocument | Get immutable document model |
| getSelection(): OwoMarkSelection \| null | Current selection as linear offsets |
| getVirtualSelection() | Current selection as { blockId, offset } pairs |
| getComposition() | Current IME composition state |
| getSnapshot() | Full state snapshot |
Input Handling
| Method | Description |
|--------|-------------|
| applyBeforeInput(intent, selection) | Handle beforeinput events |
| applyKeyDown(intent, selection) | Handle keydown events |
| applyPaste(intent, selection) | Handle paste events |
| handleCompositionStart(selection) | Enter IME composition mode |
| handleCompositionEnd(selection) | Exit IME composition mode |
| syncText(text, selection) | Sync DOM text back to model (IME fallback) |
Content Mutation
| Method | Description |
|--------|-------------|
| setMarkdown(md: string) | Replace content (resets history) |
| replaceMarkdown(md, selection) | Replace content with explicit selection (supports undo) |
| dispatch(command, payload?) | Dispatch a named command |
Events
| Method | Description |
|--------|-------------|
| onChange(cb) | Subscribe to content changes |
| onSelectionChange(cb) | Subscribe to selection changes |
| onCompositionStateChange(cb) | Subscribe to IME composition state |
| onDocumentChange(cb) | Subscribe to document model changes (sovereign mode) |
| onStateChange(cb) | Subscribe to any state change |
Pure Function Exports
The package also exports pure functions that can be used independently:
Commands
import {
handleMarkdownEnter,
applyMarkdownIndent,
handleCharInput,
handleSmartBackspace,
toggleBold,
toggleItalic,
insertLink,
insertCodeFence,
} from '@owomark/core';Parser
import { tokenizeBlock, tokenizeInline } from '@owomark/core';Model
import {
parseMarkdownToDocument,
serializeDocument,
getBlockAtOffset,
} from '@owomark/core';Clipboard
import { normalizeMarkdownPaste } from '@owomark/core';Supported Markdown
- Headings (
#through######) - Bold (
**text**) and italic (*text*) - Inline code (
`code`) - Links (
[text](url)) - Unordered and ordered lists
- Blockquotes (
>) - Fenced code blocks (
```) - Thematic breaks (
---)
Preview Projection
Convert a parsed document into preview-ready blocks for incremental rendering:
import {
createSharedStateStore,
projectToPreviewBlocks,
computePreviewDirtyRange,
deriveBlockId,
} from '@owomark/core';Shared State Store
const store = createSharedStateStore({ initialMarkdown: '# Hello' });
// Connect an editor instance
const disconnect = store.connectEditor(editorInstance);
// Subscribe to state changes (consumed by preview engines)
const unsubscribe = store.subscribe((state) => {
console.log('Version:', state.version);
console.log('Preview blocks:', state.previewBlocks.length);
});
// Cleanup
unsubscribe();
disconnect();Block Projection
const blocks = projectToPreviewBlocks(document, 'vitesse-light');
const dirtyRange = computePreviewDirtyRange(previousBlocks, blocks);- Adjacent same-type container blocks (lists, blockquotes) are grouped into a single
PreviewBlock - Each block has a content-based
blockId({renderKey}#{occurrenceIndex}) for stable DOM reconciliation renderKey(djb2 hash ofkind:theme:raw) drives cache invalidation
Preview Types
PreviewBlock— block identity, kind, raw content, line range, render keyPreviewBlockKind—'paragraph' | 'heading' | 'code-fence' | ...PreviewDirtyReason—'text-edit' | 'structure-edit' | 'theme-change' | 'renderer-change' | 'custom-block-change' | 'full-rebuild'OwoMarkSharedState— full shared state snapshot (document, preview blocks, selection, version)OwoMarkSharedStateStore— subscribe/getState store interfaceOwoMarkSharedStateController— store +setMarkdown(),connectEditor(),updateVisibleBlockIds(),setThemeKey(),notifyRendererChange(),notifyCustomBlockChange()
Design Principles
- Input-first: IME composition is never interrupted by rendering. Patches only run after
compositionend. - Incremental: Only dirty blocks are re-parsed and re-rendered. No full DOM rebuilds on each keystroke.
- Framework-free: Zero runtime dependencies. Works with any UI framework or vanilla JS.
