|
|
5 дней назад | |
|---|---|---|
| .. | ||
| src | 5 дней назад | |
| README.md | 5 дней назад | |
| package.json | 1 неделя назад | |
| tsconfig.app.json | 1 неделя назад | |
| tsconfig.json | 1 месяц назад | |
| tsconfig.node.json | 1 месяц назад | |
| vite.config.ts | 1 неделя назад | |
| vitest.config.ts | 1 неделя назад | |
| vitest.setup.ts | 1 месяц назад | |
A headless block markdown editor component for Vue 3. Each Block is a self-contained ProseMirror editor instance that renders markdown syntax as visual decorations via a Lezer-based parsing pipeline.
pnpm add @enesis/editor
Requires peer dependencies: vue, @nuxt/ui, @vueuse/core, pinia.
<script setup lang="ts">
import { Block } from "@enesis/editor"
import { ref } from "vue"
const content = ref("# Hello\n\nThis is **bold** text.\n\n> [!NOTE] A callout")
</script>
<template>
<Block v-model:content="content" focused cursor-position="end" />
</template>
import EnesisEditor from "@enesis/editor"
app.use(EnesisEditor)
// → <Block /> registered globally
<Block> API| Prop | Type | Default | Description |
|---|---|---|---|
content (v-model) |
string |
required | Markdown content of the block |
focused |
boolean |
false |
Whether the block receives focus on mount |
cursorPosition |
"start" \| "end" \| number |
— | Cursor placement on focus |
markerMode |
"live-preview" \| "always-visible" |
"live-preview" |
When to show markdown delimiters |
debug |
string |
— | Namespace filter for debug logs (e.g. "CodeBlockView", "*") |
onBoundaryExit |
(dir: "up" \| "down") => void |
— | Called when code-block cursor exits at a doc boundary. |
registry |
FocusRegistry |
— | Cross-block focus registry (see useFocusRegistry). |
depth |
number |
— | List-item indent depth (0–10). |
| Event | Payload | Trigger |
|---|---|---|
change |
content: string |
Content edited |
split |
before: string, after: string |
Enter pressed |
merge-previous |
— | Backspace at document start |
delete-if-empty |
— | Backspace on empty block |
indent / outdent |
— | Tab / Shift+Tab |
arrow-up-from-start |
— | Cursor at first line start |
arrow-down-from-end |
— | Cursor at last line end |
pattern-open |
PatternOpenPayload |
Trigger pattern detected |
pattern-update |
PatternUpdatePayload |
Pattern query changed |
pattern-close |
PatternClosePayload |
Pattern session ended |
focus |
{ view, handlers } |
Block received focus |
blur |
— | Block lost focus |
selection-change |
{ from, to, empty } |
Selection changed |
const blockRef = ref<BlockExposed>()
blockRef.value?.view // ProseMirror EditorView
blockRef.value?.focus(pos?) // Programmatic focus
blockRef.value?.getContent() // Current markdown content
blockRef.value?.setContent(md)// Set content externally
<Editor> APIThe Editor component manages a list of blocks with insertion zones between them. Each block is a * list item in the unified markdown string. The editor owns parsing, serialization, split/merge, indent/outdent, and undo/redo.
| Prop | Type | Default | Description |
|---|---|---|---|
content (v-model) |
string |
required | Full document markdown (all blocks serialized). |
focused |
boolean |
false |
Whether the first block receives focus on mount. |
markerMode |
"live-preview" \| "always-visible" |
"live-preview" |
When to show markdown delimiters. |
debug |
string |
— | Namespace filter for debug logs. |
theme |
ThemeInput |
— | CSS-variable theme preset or overrides (see theme.ts). |
#toolbarThe editor forwards a #toolbar slot with bindings from the focused block:
<Editor v-model:content="md">
<template #toolbar="{ handlers, selectionVersion, canUndo, canRedo, undo, redo }">
<EditorToolbar :handlers="handlers" :selection-version="selectionVersion" />
</template>
</Editor>
| Binding | Type | Description |
|---|---|---|
handlers |
FormattingHandlers \| null |
Handler object from the focused block, or null if blurred. |
selectionVersion |
number |
Incremented on each selection change — use as watch key. |
canUndo |
boolean |
Whether undo history has entries. |
canRedo |
boolean |
Whether redo history has entries. |
undo |
() => void |
Pop and apply the inverse of the last operation. |
redo |
() => void |
Re-apply the last undone operation. |
| Event | Payload | Trigger |
|---|---|---|
focus |
{ view, handlers } |
Any block received focus. |
blur |
— | All blocks lost focus. |
selection-change |
{ from, to, empty } |
Selection changed in the active block. |
const editorRef = ref<{ undo: () => void; redo: () => void; canUndo: Ref<boolean>; canRedo: Ref<boolean> }>()
editorRef.value?.undo()
editorRef.value?.redo()
[Raw block content string]
│
├── Phase 1: Lezer AST Walk
│ ├─ enter → block rules (headings, blockquotes, callouts, tasks)
│ └─ leave → inline rules (bold, italic, code, links, refs, tags, highlights)
│
├── Phase 2: Property Extraction (key:: value)
│
▼
[ParsedDecorations — unified output struct]
The pipeline runs inside a ProseMirror decoration plugin (useMarkdownDecorations.ts). Results are cached per-paragraph and rebuilt only on content change. Target: <16ms per parse cycle.
Formatting is purely decorative — markdown syntax is stored as plain text in ProseMirror and rendered via decorations. Delimiters (**, [[, ^^, etc.) are hidden ranges that disappear when the cursor is inside the content (live-preview mode) or are always visible.
| Element | Example | Decoration Class |
|---|---|---|
| Bold | **text** |
md-strong |
| Italic | *text* |
md-em |
| Inline Code | `code` |
md-code |
| Strikethrough | ~~text~~ |
md-strike |
| Link | [text](url) |
md-link |
| Page Reference | [[Page Name\|Alias]] |
md-page-ref |
| Block Reference | ((block-id)) |
md-block-ref |
| Tag | #tag |
md-tag |
| Highlight | ^^text^^ |
md-highlight |
| Heading | # H1 through ###### H6 |
md-heading-content |
| Blockquote | > text |
md-blockquote |
| Callout | > [!NOTE] text |
md-callout |
| Task | TODO task text |
md-task-marker / md-task-content |
| Property | key:: value |
md-property-* |
| Inline Math | $...$ |
md-math-inline (+ KaTeX preview on blur) |
| Display Math | $$...$$ |
MathBlock node view (+ KaTeX preview) |
| Fenced Code Block | python/~~~js` |
CM6 node view |
| Highlight | ^^text^^ |
md-highlight |
Priority flags (
[#A],[#B],[#C]) and date emoji (📅 YYYY-MM-DD) were removed from the parser — they render as plain text. Styling classes remain for backward compatibility.
As the user types, a ProseMirror ViewPlugin (usePatternPlugin.ts) detects trigger characters and emits lifecycle events:
| Trigger | Pattern Kind | Terminator |
|---|---|---|
[[ |
reference |
]] |
(( |
embed |
)) |
/ |
command |
whitespace |
# |
tag |
whitespace |
import { getPatternSession } from "@enesis/editor"
// Inside a pattern-open handler:
const session = getPatternSession(view.state)
session?.respond("query") // filter results
session?.close() // end the session
Add inline syntax by creating a MarkdownPattern:
import type { MarkdownPattern } from "@enesis/editor"
const myPattern: MarkdownPattern = {
name: "custom",
parseNode: { name: "MyNode", type: "InlineCode" },
delimiter: { open: "~~", close: "~~" },
}
pnpm build # Build library
pnpm test # Run tests (Vitest)
pnpm dev # Watch mode rebuild
| File | Purpose |
|---|---|
src/index.ts |
Public API surface |
src/components/Block.vue |
ProseMirror block editor component |
src/components/Editor.vue |
Multi-block shell with insertion zones, split/merge, undo/redo |
src/components/InsertionZone.vue |
Invisible click target between blocks |
src/components/EditorToolbar.vue |
Shared toolbar bound to active block's handlers |
src/composables/useMarkdownDecorations.ts |
Decoration plugin (live-preview, caching) |
src/composables/useBlockKeyboardHandlers.ts |
Keyboard event handler |
src/composables/useCodeBlockView.ts |
CM6 NodeView for fenced code blocks |
src/composables/useMathBlockView.ts |
KaTeX NodeView for display math blocks |
src/composables/usePatternPlugin.ts |
Pattern detection plugin |
src/composables/usePasteHandler.ts |
Multi-line paste split handler |
src/composables/useFocusRegistry.ts |
Cross-block focus management |
src/lib/block-parser.ts |
splitMarkdownIntoBlocks / serializeBlocks |
src/lib/content-model.ts |
contentToDoc(md) / docToContent(doc) — Markdown ↔ ProseMirror |
src/lib/formatting.ts |
Formatting handlers — toggle/wrap/insert for bold, link, math, page refs, tags, heading, task |
src/lib/operation-history.ts |
EditorOperation types + undo/redo stack |
src/lib/theme.ts |
CSS-variable theme system with presets |
src/lib/katex.ts |
Dynamic KaTeX import + render helpers |
src/lib/auto-close-plugin.ts |
Auto-close for ```, **, _ and bracket pairs |
src/lib/logger.ts |
Namespace-based debug logger |
src/lib/schema.ts |
ProseMirror schema |
src/lib/markdown-parser.ts |
Lezer parser configuration |
src/lib/markdown-extensions.ts |
Custom Lezer node definitions (PageRef, BlockRef, Tag, Highlight, InlineMath, Task) |
src/lib/markdown-rules/engine.ts |
MarkdownRuleEngine — Lezer AST walk |
src/lib/markdown-rules/inline-rules.ts |
Inline decoration rules |
src/lib/markdown-rules/block-rules.ts |
Block decoration rules |
src/lib/markdown-rules/types.ts |
Shared type definitions |
src/lib/markdown-rules/block-classifier.ts |
Regex first-line classifier (paste only) |