A block markdown editor built for practical wisdom: Lezer-parsed, ProseMirror-rendered, structurally correct. It infers tasks, callouts, tables, and refs from what you type — and gets out of the way the moment you're focused on writing.
|
|
пре 20 часа | |
|---|---|---|
| .forgejo | пре 1 дан | |
| apps | пре 20 часа | |
| packages | пре 20 часа | |
| .editorconfig | пре 1 месец | |
| .envrc | пре 1 месец | |
| .gitignore | пре 1 дан | |
| AGENTS.md | пре 1 дан | |
| README.md | пре 1 дан | |
| TESTING.md | пре 2 дана | |
| biome.json | пре 1 недеља | |
| devenv.lock | пре 1 дан | |
| devenv.nix | пре 1 дан | |
| devenv.yaml | пре 1 дан | |
| package.json | пре 1 дан | |
| pnpm-lock.yaml | пре 20 часа | |
| pnpm-workspace.yaml | пре 20 часа | |
| rust-toolchain.toml | пре 1 дан | |
| skills-lock.json | пре 20 часа |
A high-performance block markdown editor built with Vue 3, TypeScript, ProseMirror, and the Lezer parsing framework.
Documents are structured as distinct, interactive Blocks rather than a single flat string. Each block is a self-contained ProseMirror editor instance that stores raw markdown as plain text — no AST conversion, no mark storage. A Lezer-based parsing pipeline renders syntax as visual decorations in real time, enabling live-preview editing of Obsidian-style references, task states, callouts, headings, and inline formatting.
Enesis is being built as a complete block-editing substrate where plain text stays plain text — formatting is purely decorative via Lezer-driven decorations, never structural. This makes it ideal for note-taking apps, knowledge bases, and any tool that needs to edit rich markdown with programmatic access to the raw source.
**bold** to <strong>. What you type is what's stored.@lezer/markdown AST walk with custom extensions for Obsidian-style elements.split, merge-previous, arrow-up-from-start, etc.) — no shared state.TipTap's extension ecosystem assumes formatting is structural — Bold becomes a <strong> mark, Italic becomes <em>, headings become heading nodes. This editor's core premise is the opposite: markdown syntax stays as raw text and formatting is purely decorative via Lezer-driven ProseMirror decorations. Adopting TipTap would mean fighting its content model rather than using it, essentially reimplementing the decoration layer on top of an abstraction that actively resists it. TipTap also manages the ProseMirror schema for you, which would conflict with the deliberately minimal schema here (doc, paragraph, code_block, math_block, text — no marks at all).
The multi-block architecture is the other major friction point. Each EditorBlock is its own independent EditorView instance, coordinating via events. TipTap assumes a single editor wrapping the whole document, so the block-per-instance model would require either abandoning TipTap's lifecycle entirely (at which point you're just using raw ProseMirror anyway) or shoehorning the pattern into something TipTap wasn't designed for. The custom Lezer parsing pipeline, pattern plugin, and node view registry all interact directly with ProseMirror's internals in ways that TipTap's abstraction layer would make unnecessarily awkward to access.
├── apps/
│ └── dev/ Docs-style development site (Vue 3 + Vite)
│ ├── src/
│ │ ├── components/
│ │ │ ├── LiveExample.vue Editable demo wrapper with source view
│ │ │ └── AppLogo.vue Enesis brand mark
│ │ ├── pages/
│ │ │ ├── index.vue Overview
│ │ │ ├── basic-editing.vue Headings, paragraphs, rules
│ │ │ ├── inline-marks.vue Bold, italic, code, strike, highlight
│ │ │ ├── tasks.vue Task states & priorities
│ │ │ ├── blockquotes.vue Blockquotes & callouts
│ │ │ ├── links.vue Markdown links
│ │ │ ├── refs-tags.vue Page refs, block refs, tags
│ │ │ ├── code-blocks.vue Fenced code blocks
│ │ │ ├── properties.vue Key::value properties & dates
│ │ │ └── math.vue Inline & display LaTeX
│ │ ├── App.vue Shell layout (Nuxt UI) with sidebar
│ │ ├── main.ts App bootstrap (10 routes)
│ │ └── style.css Tailwind v4 + Nuxt UI theme
│ ├── public/ Static assets
│ └── vite.config.ts Vite with Nuxt UI plugin
│
├── packages/
│ └── editor/ Core headless engine
│ ├── src/
│ │ ├── index.ts Public API (Block + Editor + EditorToolbar components)
│ │ ├── components/
│ │ │ ├── EditorBlock.vue Self-contained ProseMirror block editor with draggable bullet
│ │ │ ├── Editor.vue Multi-block shell with insertion zones, drag-to-reorder, undo/redo
│ │ │ ├── EditorInsertionZone.vue 32px hit target between blocks, hover-reveal, drag target
│ │ │ └── EditorToolbar.vue Shared toolbar bound to active block
│ │ ├── composables/
│ │ │ ├── useMarkdownDecorations.ts ProseMirror decoration plugin
│ │ │ ├── useBlockKeyboardHandlers.ts Keyboard handler (Enter, Backspace, arrows)
│ │ │ ├── useCodeBlockView.ts CM6 NodeView for fenced code blocks
│ │ │ ├── useMathBlockView.ts KaTeX NodeView for display math
│ │ │ ├── usePatternPlugin.ts Pattern detection ([[, ((, /, #)
│ │ │ ├── usePasteHandler.ts Multi-line paste split handler
│ │ │ └── useFocusRegistry.ts Cross-block focus management
│ │ └── lib/
│ │ ├── block-parser.ts splitMarkdownIntoBlocks / serializeBlocks
│ │ ├── content-model.ts Markdown ↔ ProseMirror conversion
│ │ ├── schema.ts Minimal ProseMirror schema
│ │ ├── auto-close-plugin.ts Auto-close for ```, **, _
│ │ ├── formatting.ts Formatting handlers (toggleBold, insertLink, etc.)
│ │ ├── operation-history.ts EditorOperation types + undo/redo stack
│ │ ├── theme.ts CSS-variable theme system with presets
│ │ ├── katex.ts Dynamic KaTeX import + render helpers
│ │ ├── logger.ts Namespace-based debug logger
│ │ ├── markdown-parser.ts Lezer parser configuration
│ │ ├── markdown-extensions.ts Custom Lezer extensions
│ │ └── markdown-rules/
│ │ ├── engine.ts MarkdownRuleEngine (2-stage pipeline)
│ │ ├── types.ts Shared type interfaces
│ │ ├── inline-rules.ts Inline syntax rules
│ │ ├── block-rules.ts Block syntax rules
│ │ └── block-classifier.ts First-line regex classifier (paste only)
│ ├── vite.config.ts Library build (ESM, tailwind, vue)
│ └── vitest.config.ts Test runner configuration
│
├── .forgejo/
│ └── workflows/
│ └── deploy.yml CI: build + deploy to Codeberg Pages
│
├── package.json Workspace root
├── pnpm-workspace.yaml pnpm workspace definition
├── AGENTS.md Context optimization file — strict rules and system boundaries to guide AI coding assistants safely through ProseMirror's state architecture
└── biome.json Linting & formatting
pnpm install
pnpm dev # Start the dev sandbox at localhost:5173
pnpm test # Run editor unit tests (Vitest)
pnpm check # Lint & format check (Biome)
pnpm install @enesis/editor
<script setup lang="ts">
import { ref } from "vue"
import { Editor, EditorToolbar } from "@enesis/editor"
import "@enesis/editor/dist/index.css"
const content = ref(`* # Hello World
* A paragraph with **bold** and *italic* text.
* A TODO task with ::TODO:: status
`)
function onFocus(payload: { view: EditorView; handlers: FormattingHandlers }) {
// payload.handlers provides toggleBold, insertLink, setHeading, etc.
}
</script>
<template>
<EditorToolbar />
<Editor
v-model:content="content"
@focus="onFocus"
@blur="() => console.log('blurred')"
@error="(e: any) => console.error(e)"
/>
</template>
| Script | Description |
|---|---|
pnpm dev |
Start Vite dev server for @enesis/dev sandbox |
pnpm build |
Build @enesis/editor library (ESM + type declarations) |
pnpm test |
Run unit tests for @enesis/editor |
pnpm check |
Run Biome lint & format check across the workspace |
On push to master, a Forgejo Actions workflow builds the dev app and deploys it to Codeberg Pages at https://enesismd.codeberg.page/editor/.
| Layer | Technology |
|---|---|
| Framework | Vue 3 (Composition API, <script setup>) |
| Editor Engine | ProseMirror (view, state, model, transform, keymap, gapcursor) |
| Parsing | Lezer (@lezer/markdown + GFM + custom extensions) |
| Styling | Tailwind CSS v4 + Nuxt UI v4 |
| Type Safety | TypeScript (strict: true) |
| Testing | Vitest with jsdom |
| Formatting | Biome |
| Package Manager | pnpm workspaces |
| CI/CD | Forgejo Actions → Codeberg Pages |
MIT