Skip to content

feat(editor): render Obsidian-style callouts from blockquotes#912

Open
praxstack wants to merge 61 commits into
refactoringhq:mainfrom
praxstack:feat/obsidian-callouts
Open

feat(editor): render Obsidian-style callouts from blockquotes#912
praxstack wants to merge 61 commits into
refactoringhq:mainfrom
praxstack:feat/obsidian-callouts

Conversation

@praxstack

Copy link
Copy Markdown

What & why

Obsidian-style callouts authored as blockquotes — > [!tip] Title, > [!abstract], and collapsible > [!example]+ / > [!failure]- — currently render as plain quote blocks in the BlockNote editor, showing the literal [!type] marker text instead of a styled box. Since Tolaria is designed to operate the same markdown vault as Obsidian, every callout in a migrated/shared vault looks broken. (Refs #910.)

This renders them as styled callout boxes while keeping the markdown round-trip lossless.

How

A new calloutBlock custom block (mirrors the existing createReactBlockSpec pattern used by Math/Mermaid/Tldraw):

  • Detect: post-parse, a quote block whose first line matches [!type] is converted into a callout block (calloutType, fold, title, body) via a small block-graph transform wired into the existing inject pipeline in editorBlockResolution.ts.
  • Serialise: a callout block serialises back to exact > [!type]<fold> title + > body markdown, wired into the existing durable-serialisation chain in editorDurableMarkdown.ts. The callout type token is normalised to lowercase (Obsidian treats it case-insensitively); title, body, and fold are preserved verbatim.
  • Render: emoji + label header and a colored left border. Type families map onto the app's existing semantic palette (--feedback-{info,success,warning,error}, --color-accent), so it stays theme-aware. Unknown types fall back to the default "note" style (never crash).

The detection regex is deliberately simple (no lookbehind) so it stays safe under WKWebView.

Tests

  • src/utils/calloutMarkdown.test.ts — unit coverage of marker parse/format, the type map + unknown-type fallback, and round-trip identity for every type with/without title and with +/- folds, plus adversarial cases: a body line that itself looks like a marker stays in the body, blank body lines round-trip as bare >, whitespace-only titles collapse to marker-only.
  • src/components/editorSchema.callout.test.ts — editor integration: markdown → blocks → callout block → markdown through the real BlockNoteEditor + schema, proving notes don't corrupt on a parse/serialise cycle.

Scope / notes for maintainers

  • Kept focused to callout rendering only — no unrelated refactors.
  • Localisation: the callout type labels are kept in code for this change. They likely warrant extraction into the Lara pipeline (src/lib/locales/en.json) — I left a code comment to that effect but did not run pnpm l10n:translate (see below).
  • Gates I could not run as an external contributor (full transparency): the repo's pre-push gate (CodeScene code-health, Codacy, Lara l10n:translate, and the Chunk-sidecar Playwright lane) requires maintainer infrastructure/credentials I don't have, so I could not run those and the push used --no-verify on my fork. Everything runnable locally is green:
    • npx tsc --noEmit — 0 errors
    • pnpm lint (eslint) — clean on all touched files
    • pnpm test (vitest) — callout unit + integration tests pass; no existing tests broken
    • pnpm build — succeeds
  • No PostHog event added (rendering-only change with no discrete user action to instrument); happy to add one if you'd like to track callout authoring.
  • I wrote the code to the standard the CodeScene gate implies — small pure functions, no as any, no eslint-disable — but I can't produce the 10.0 score myself. Please run it your side; happy to refactor anything it flags.

I understand per CONTRIBUTING that features usually go through Canny first — I've also raised this there. Submitting the implementation in case a working, tested, focused PR is useful to land it faster. Totally fine to treat as a starting point.

Obsidian callouts authored as blockquotes — `> [!tip] Title`, `> [!abstract]`,
collapsible `> [!example]+` / `> [!failure]-` — previously rendered as plain
quote blocks in the BlockNote editor, showing the literal `[!type]` marker text
instead of a styled box.

This adds a `calloutBlock` custom block:
- Post-parse, a quote block whose first line matches `[!type]` is converted into
  a callout block (type, optional fold modifier, title, body) via a small
  block-graph transform wired into the existing inject pipeline.
- Serialisation emits the callout back to exact `> [!type]<fold> title` /`> body`
  markdown, so notes round-trip losslessly (the callout TYPE token is normalised
  to lowercase, since Obsidian treats it case-insensitively).
- Rendering shows an emoji + label header and a colored left border, mapping
  callout type families onto the app's existing semantic feedback palette
  (`--feedback-{info,success,warning,error}`), theme-aware.

Round-trip safety is covered by unit + editor-integration tests, including
adversarial cases (a body line that itself looks like a marker, blank body
lines, whitespace-only titles, unknown types falling back to the default style).

Notes for maintainers:
- Type labels are kept in code for this change; they may warrant extraction into
  the Lara localisation pipeline (`src/lib/locales/en.json`).
- I could not run the project's CodeScene/Codacy/Lara/Playwright-sidecar gates as
  an external contributor; locally `tsc --noEmit`, `eslint`, `vitest`, and
  `pnpm build` all pass clean.
github-actions Bot added 28 commits June 17, 2026 10:50
github-actions Bot added 30 commits June 22, 2026 08:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant