AGENTS.md 6.9 KB

AGENTS.md

System Intent & Context

This repository is a high-performance, rule-based block markdown editor built with Vue 3, TypeScript, and the Lezer parsing framework. Documents are structured as distinct, interactive Blocks rather than a single flat string. A 3-stage parsing pipeline bridges UI state down to ASTs, enabling real-time extraction of pages, references, task state, callouts, and inline decorations.


Monorepo Structure

apps/dev/          Development and testing sandbox (Vue 3 + Vite + TypeScript)
packages/editor/   Core headless engine — parsers, rule registries, composables, tests

Component flow:

apps/dev/src/pages/EditorView.vue
  └── apps/dev/src/components/Editor.vue
        └── packages/editor/src/components/Block.vue
              └── packages/editor/src/composables/useMarkdownDecorations.ts
                    └── packages/editor/src/lib/markdown-rules.ts
                          └── Lezer Parser Engine

Parsing Pipeline

All syntax features flow through a 3-stage pipeline in packages/editor/src/lib/markdown-rules.ts:

[Raw String Fragment]
       │
       ├── Pipeline 1: Lezer AST Visitor  →  Inline Rules (Bold, Links, PageRef, Tags, Highlights)
       ├── Pipeline 2: Pattern Matcher    →  Block/Line Rules (Headings, Tasks, Callouts, Dates)
       └── Pipeline 3: Suffix Matcher     →  Property Extraction (Key::Value)
       │
       ▼
[ParsedDecorations — unified output struct]

Unified Output Contract

export interface DecorationRange {
  type: "hidden" | "content"
  from: number
  to: number
  className?: string
}

export interface ParsedDecorations {
  content: (TokenDecoration | PageRefDecoration | BlockRefDecoration | TagDecoration | HighlightDecoration)[]
  properties: PropertyInfo[]
  blocks: BlockDecoration[]
}

Do not change this interface without updating both unit tests and block render cycles.


Supported Syntax

Task States and Priorities

TODO Fix the critical focus bug [#A]
└──┘                            └──┘
status                          priority
  • Valid states: TODO, DOING, DONE, LATER, NOW, WAITING, CANCELLED
  • State progression: TODO → DOING → DONE (linear). LATER, NOW, WAITING, CANCELLED are orthogonal.
  • Valid priorities: [#A], [#B], [#C] — independent of task state.

Obsidian-Style Elements

Syntax Example
Page reference [[Page Name]] or [[Real Page\|Display Name]]
Block reference ((block-id-1234))
Callout > [!NOTE] Description text
Property author:: John Doe

Valid callout types: NOTE, WARNING, TIP, DANGER, INFO


Technology Stack

Layer Technology Rule
Framework Vue 3 (Composition API, <script setup>) Use composables for local state slices (e.g. useBlockKeyboardHandlers).
Type Safety TypeScript (strict: true) No implicit any. Use strict sum-types for internal configs like TaskState.
AST Parsing @lezer/common / Lezer Prefer specific SyntaxNode targeting over brute-force regex on blocks.
Formatting Biome Run biome check before committing.
Testing Vitest Run vitest run to verify. Every new rule must have a mirror test.

How to Add a New Rule

Follow these steps exactly. Do not deviate from this pattern.

1. Identify the pipeline stage.

  • Inline syntax (spans within a line, e.g. bold, links) → InlineRule
  • Line/block syntax (whole-line patterns, e.g. headings, tasks) → LineRule
  • Key-value extraction → SuffixRule

2. Implement a self-contained factory function in packages/editor/src/lib/markdown-rules.ts. Do not modify MarkdownRuleEngine.parse().

// Example: a minimal InlineRule factory
function createMyInlineRule(): InlineRule {
  return {
    // Lezer node type(s) this rule targets
    nodeTypes: ["MyNode"],
    extract(node, content, offset) {
      return [{
        type: "content",
        from: node.from + offset,
        to: node.to + offset,
        className: "my-decoration",
      }]
    },
  }
}

3. Register it in the MarkdownRuleEngine constructor.

constructor() {
  this.inlineRules = [
    createBoldRule(),
    createLinkRule(),
    createMyInlineRule(), // ← add here
  ]
}

4. Write a mirror test in packages/editor/src/lib/__tests__/markdown-parser.test.ts. Follow the existing pattern:

describe("MyInlineRule", () => {
  it("decorates correctly", () => {
    const result = engine.parse("some ~example~ text")
    expect(result.content).toContainEqual({
      type: "content",
      from: 5,
      to: 12,
      className: "my-decoration",
    })
  })
})

5. Verify cursor behavior. For any rule with hidden ranges (e.g. delimiters that hide when cursor is inside): confirm that from/to offsets are computed relative to the block's document offset, not the raw fragment string. Check the existing createBoldRule implementation as the reference.

6. Run checks.

biome check packages/editor/src/lib/markdown-rules.ts
vitest run

Both must pass before the change is complete.


Cursor Behavior — Hidden vs. Content Ranges

Decorations alternate between hidden (delimiters) and content (payload). For example, in **bold**:

  • Positions 0–1 (**) → hidden when cursor is inside content (positions 2–5)
  • Positions 2–5 (bold) → content
  • Positions 6–7 (**) → hidden when cursor is inside content

Rules:

  • hidden ranges disappear from view when the cursor is inside the enclosing content range.
  • content ranges are always visible.
  • Never update extraction slices without verifying that cursor offset tracking remains accurate for both the hidden and content segments.

Performance Constraints

  • Target < 16ms per parse cycle (60fps budget). The parser runs on every keystroke in focused Block.vue instances.
  • visitTree processes each Lezer node exactly once. Do not introduce nested tree walks.
  • Avoid allocations inside hot paths — reuse range objects where possible.

Critical Constraints

Read-only packed files. Aggregated repo views from tools like Repomix or workspace-pack are transport-only. Never write output targeting packed container files. Always emit changes to original source paths under apps/ or packages/.

Rule isolation. Never modify MarkdownRuleEngine.parse() to add syntax support. New syntax belongs in a new InlineRule or LineRule factory, registered in the constructor.

Test coverage is required. A rule without a mirror test in __tests__/markdown-parser.test.ts is not complete.

Strict types. Do not introduce any. If a type is genuinely unknown, model it explicitly with a discriminated union or unknown with a type guard.