T-DSVW-md-engine-html-details
State: closed/done · Impact: high · Complexity: medium
HTMLBlock nodes (including <details>) parse today but render as plain text in live mode, so a
toggle block is dead markup while editing. This task adds the html-blocks plugin: a
DetailsToggleWidget that folds the toggle body ephemerally, a sanitized rendered widget for
other HTML blocks, and the block.details command.
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/decorations.ts | The enter switch has no HTMLBlock case; raw HTML shows as plain text in live mode |
packages/ts/obsidian-md/src/sanitize.ts | The sanitize schema read mode applies; the rendered widget reuses it via renderToHtmlSync |
packages/ts/obsidian-editor/src/commands.ts | No block.details |
packages/ts/obsidian-editor/test/helpers.ts#makeState | Headless state builder for the plugin tests |
The plugin roster comes from T-4TE2-md-engine-plugin-contract; the catalogue from
T-EXQ7-md-engine-command-catalogue; the details.md corpus fixture from
T-0GV4-md-engine-line-endings-corpus.
Proposed
Section titled “Proposed”packages/ts/obsidian-editor/src/plugins/html-blocks.ts, a defineEditorPlugin with
extensions: codeFolding():
- A
<details>opener block (with its optional<summary>line) paired with its</details>closer →DetailsToggleWidgetover the opener: a disclosure triangle plus the summary text. The closer line is hidden; the body lines between them fold and unfold throughcodeFolding()fold effects driven by the widget’s click handler. Folding is ephemeral view state — theopenattribute is never written into the document. - Any other
HTMLBlock→ block widget ofrenderToHtmlSyncmarkup, sanitized by the same schema read mode uses, so nothing renders in live mode that read mode would strip. - Any line of the block intersecting the selection → widgets and hides suppressed, raw HTML visible, as with every construct.
- Catalogue addition
block.details(groupblock): wraps the selected lines in a<details><summary>…</summary>…</details>skeleton viachangeByRange; nothing outside the selection is rewritten.
The theme module gains rules for the toggle widget and the rendered HTML container.
Approach
Section titled “Approach”- Write
packages/ts/obsidian-editor/test/plugins/html-blocks.plugin.test.tsfirst, over fixtures with a summary toggle, a bare toggle, a nested toggle and a<script>block: widget over the opener; closer hidden; click toggles the fold effect over exactly the body lines; ten toggles leavestate.sliceDoc()byte-identical; the<script>block’s widget DOM contains no script element. - Add the
block.detailscase: applied to a two-line selection in a five-line fixture, the other three lines are byte-identical and the cursor lands inside the summary. - Implement the plugin: pairing walk over sibling
HTMLBlocknodes,DetailsToggleWidget, fold dispatch, sanitized rendered widget. - Register
html-blocksinBUILTIN_PLUGINS; implementblock.detailsin the catalogue; add the theme rules. bun test,tsc --noEmit,moon run obsidian-editor:bench; confirm the swap-writer corpus passes over the details fixture.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/plugins/html-blocks.ts | new | html-blocks plugin: DetailsToggleWidget, fold wiring, sanitized HTML widget |
packages/ts/obsidian-editor/src/plugins/index.ts | new | Created by T-4TE2-md-engine-plugin-contract; html-blocks joins BUILTIN_PLUGINS |
packages/ts/obsidian-editor/src/commands/catalogue.ts | new | Created by T-EXQ7-md-engine-command-catalogue; gains block.details |
packages/ts/obsidian-editor/src/theme.ts | modify | Toggle widget and rendered-HTML container rules |
packages/ts/obsidian-editor/test/plugins/html-blocks.plugin.test.ts | new | Toggle, fold, sanitize and command tests |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editorpasses, including the new suite. - AC-2: clicking the toggle folds the body lines (a fold range spanning exactly the lines
between opener and closer exists) and clicking again unfolds; after ten toggles
state.sliceDoc()is byte-identical — noopenattribute ever appears in the document. - AC-3: the widget DOM for a fixture
HTMLBlockcontaining a script tag has no script element and no inline event-handler attribute. - AC-4:
block.detailsapplied to a two-line selection in the five-line fixture leaves the other three lines byte-identical. - AC-5:
bun test packages/ts/obsidian-editor/test/swap-writer.test.tspasses withblock.detailsin the matrix overpackages/ts/obsidian-editor/test/fixtures/corpus/details.md. - AC-6:
moon run obsidian-editor:benchstays within 1.5× of the recorded baseline.
Out of scope
Section titled “Out of scope”- Persisting toggle state per note; folding is view-local by design.
- Read-mode toggle behavior (
obsidian-mdalready renders<details>natively). - Inline HTML spans; this task covers
HTMLBlocknodes only.
Dependencies
Section titled “Dependencies”- T-RCWI-md-obsidian-md-sync-helpers for
renderToHtmlSync. - T-4TE2-md-engine-plugin-contract for the plugin contract and the
extensionsseamcodeFolding()rides; T-EXQ7-md-engine-command-catalogue for the catalogue; T-0GV4-md-engine-line-endings-corpus for the corpus fixture. - D-7VMX: block boundaries come from
HTMLBlocktree nodes; only the opener/closer pairing inspects the block’s text.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E9 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-RCWI-md-obsidian-md-sync-helpers