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.

Zander Hawke 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
.forgejo 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
apps 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
packages 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
.editorconfig 9a21611c2f feat: implement core block editor with markdown decorations 1 ヶ月 前
.envrc 9a21611c2f feat: implement core block editor with markdown decorations 1 ヶ月 前
.gitignore 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
AGENTS.md 8d945dde60 docs(editor): rewrite AGENTS.md rule guide with three registration points and MarkdownPattern example 3 日 前
README.md b80121dc71 chore: address code review items across history, caching, and event handling 1 日 前
TESTING.md 10ef13a829 feat(editor): replace drag grip with draggable bullet and zone-based drop 2 日 前
biome.json fff9911f7c refactor(editor): fix TypeScript errors, path alias setup, and CI deploy 1 週間 前
devenv.lock ac8ba1d93e feat: Tauri desktop app shell and editor cleanup 1 日 前
devenv.nix ac8ba1d93e feat: Tauri desktop app shell and editor cleanup 1 日 前
devenv.yaml ac8ba1d93e feat: Tauri desktop app shell and editor cleanup 1 日 前
package.json 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
pnpm-lock.yaml 7b66329bf7 test(editor): add Playwright E2E harness, data attributes, shared test utils 1 日 前
pnpm-workspace.yaml ac8ba1d93e feat: Tauri desktop app shell and editor cleanup 1 日 前
rust-toolchain.toml ac8ba1d93e feat: Tauri desktop app shell and editor cleanup 1 日 前
skills-lock.json 9a21611c2f feat: implement core block editor with markdown decorations 1 ヶ月 前

README.md

Enesis Editor

Enesis Editor

Block Markdown Editor

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 3-stage 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.

The editor is designed for the philosophy that plain text should stay plain text — formatting is purely decorative, 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.

Vision

Enesis is being built as a complete block-editing substrate:

| Phase | Feature | Status | |---|---|---|---| | 1 | Clean content model — raw markdown in/out, ProseMirror never parses formatting | Done | | 2 | Keyboard & boundary behavior — Enter, Backspace, Tab, arrows, pattern triggers | Done | | 3 | Focus & cursor control — programmatic focus, expose API | Done | | 4 | Core decoration system — Lezer-based inline tokens, cursor-reveal, caching | Done | | 5 | Block-level decorations — headings, blockquotes, tasks, callouts, properties | Done | | 6 | Code & math node views — CM6 fenced code blocks, KaTeX LaTeX rendering | Done | | 6.5 | Formatting handlers — toggle bold/italic/code/strike/highlight, insertLink, setHeading, toggleTask | Done | | 7 | Reference & tag insertion handlers — insertPageRef, insertBlockRef, insertTag | Done | | 8 | Reference interactivity — clickable [[page]], ((block)), #tag chips with preview | Planned | | 9 | Asset handling — image drop, upload protocol, inline preview | Planned | | 10 | Multi-block editing — shared toolbar, insertion zones, suggestion/mention menus, undo/redo | Done (shell + history) | | 11 | WCAG 2.1 AA compliance, screen reader support, RTL, high contrast | Planned | | 12 | History orchestration API across blocks | Planned | | Post-v1 | Third-party decoration plugin system | Planned |

Design Principles

  • Raw markdown in, raw markdown out. ProseMirror never converts **bold** to <strong>. What you type is what's stored.
  • Syntax through decorations, not marks. Bold, italic, links, references, tags — all rendered via ProseMirror decorations, never via schema marks. This keeps the document model clean and pluggable.
  • Lezer-based parsing. All syntax features flow through a single @lezer/markdown AST walk with custom extensions for Obsidian-style elements.
  • Block-level isolation. Each block is an independent ProseMirror instance. Blocks coordinate via emitted events (split, merge-previous, arrow-up-from-start, etc.) — no shared state.

Why Not TipTap?

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.

Monorepo Structure

├── 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                      Project conventions for AI coding agents
└── biome.json                     Linting & formatting

Quick Start

pnpm install
pnpm dev            # Start the dev sandbox at localhost:5173
pnpm test           # Run editor unit tests (Vitest)
pnpm check          # Lint & format check (Biome)

Scripts

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

Deployment

On push to master, a Forgejo Actions workflow builds the dev app and deploys it to Codeberg Pages at https://enesismd.codeberg.page/editor/.

Technology

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

License

MIT