From 8bca7a372d4d61d39e0a26204a69bfbc40fd69ef Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:14:09 -0400 Subject: [PATCH 1/7] fix(rendering): nest markdown inside raw-HTML blocks separated by blank lines CommonMark "type 6" HTML blocks (e.g. `
`) end at the next blank line, so
**bold** content
reaches the renderer as three blocks: a `RawBlock` for `
\n`, a `Para` for the markdown, another `RawBlock` for `
\n`. The existing `` wrapper trapped the open and close inside their own elements, leaving the markdown paragraph a sibling of the (now empty) details element instead of its child. Fixed upstream in srid/heist-extra by a new `groupRawHtmlBlocks` AST pass that walks every block list and rewrites orphan opener/closer triplets into a `B.Div` with the tag in its `tag` attribute. The existing Div renderer turns that into the named element, so the markdown content lands as a real DOM child. Pointing flake.lock at the heist-extra branch carrying the fix. Closes #433. --- emanote/CHANGELOG.md | 1 + flake.lock | 7 ++++--- flake.nix | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/emanote/CHANGELOG.md b/emanote/CHANGELOG.md index 10ebed31c..dab860ced 100644 --- a/emanote/CHANGELOG.md +++ b/emanote/CHANGELOG.md @@ -26,6 +26,7 @@ - Markdown links to a static `.xml` asset (e.g. `[Test](./test.xml)`) now resolve to the file. Previously a `.xml` URL was always interpreted as the Atom feed of a same-named note, leaving asset links broken when no such feed-enabled note existed. The missing-link page now also tailors its "you may create…" hint to the URL extension instead of always suggesting `.md` / `.org` (closes [#547](https://github.com/srid/emanote/issues/547)) - Resolve relative URLs inside `/index.md` against `/` instead of its parent ([#651](https://github.com/srid/emanote/pull/651), closes [#608](https://github.com/srid/emanote/issues/608)) - Raw HTML blocks containing a literal `` no longer crash the renderer with `div cannot contain text looking like its end tag` (closes [#119](https://github.com/srid/emanote/issues/119)). Fixed upstream in [srid/heist-extra#13](https://github.com/srid/heist-extra/pull/13) by switching the raw-HTML wrapper to a unique `` element with `display: contents`. +- Raw HTML blocks separated by blank lines (CommonMark "type 6", e.g. `
` … markdown … `
`) now nest the markdown content as a real DOM child of the surrounding element instead of leaving the open and close tags trapped inside their own `` wrappers. Fixed upstream by the new `groupRawHtmlBlocks` AST pass in heist-extra (closes [#433](https://github.com/srid/emanote/issues/433)). - A malformed `*.yaml` file (e.g. a non-string mapping key like `[]: foo`) no longer takes the live server down with `BadInput "NonStringKey []"`. The parse error is folded into `SData` itself and surfaced as a banner on the notes whose meta cascade actually depends on the bad file — a broken `subfolder/index.yaml` shows up under `/subfolder/*`, not on every page (closes [#285](https://github.com/srid/emanote/issues/285)). - TOC sidebar: tightened entry padding and styled the overflow scrollbar (Firefox `scrollbar-width: thin` + WebKit pseudo-element) so long tables of contents no longer surface the chunky OS-default bar (closes [#668](https://github.com/srid/emanote/issues/668)). - Markdown tables now honour Pandoc's column alignment, column widths, cell `rowspan` / `colspan`, row & cell attributes, and table footers — previously every field beyond "rows of cells" was discarded (closes [#27](https://github.com/srid/emanote/issues/27); fixed upstream in [srid/heist-extra#15](https://github.com/srid/heist-extra/pull/15)). diff --git a/flake.lock b/flake.lock index 6c46e49b5..936ea80cd 100644 --- a/flake.lock +++ b/flake.lock @@ -169,15 +169,16 @@ "heist-extra": { "flake": false, "locked": { - "lastModified": 1777251633, - "narHash": "sha256-r1KTm9OIvwVeU6K6rZrGL/IrhGeCZ/9EqmK0e7FqDFk=", + "lastModified": 1777425229, + "narHash": "sha256-t3es4sF9f6QBLA6qu089MBtmPBGc18X0XoKMXTBUfYA=", "owner": "srid", "repo": "heist-extra", - "rev": "4857e8b265968f6c8f8c0d4d0075455bf99eeddb", + "rev": "970c9a373b0c7eb10c76a20f44a44fb8547d7066", "type": "github" }, "original": { "owner": "srid", + "ref": "group-orphan-rawhtml-blocks", "repo": "heist-extra", "type": "github" } diff --git a/flake.nix b/flake.nix index 3cadc5e85..93a87846f 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,7 @@ ema.flake = false; lvar.url = "github:srid/lvar/0.2.0.0"; lvar.flake = false; - heist-extra.url = "github:srid/heist-extra"; + heist-extra.url = "github:srid/heist-extra/group-orphan-rawhtml-blocks"; heist-extra.flake = false; unionmount.url = "github:srid/unionmount/0.3.0.0"; unionmount.flake = false; From f2be6335235e710248c67e04a6f06b4217671d1d Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:23:48 -0400 Subject: [PATCH 2/7] chore(deps): bump heist-extra to e5a71f2 (hickey+lowy follow-ups) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 936ea80cd..2add6d093 100644 --- a/flake.lock +++ b/flake.lock @@ -169,11 +169,11 @@ "heist-extra": { "flake": false, "locked": { - "lastModified": 1777425229, - "narHash": "sha256-t3es4sF9f6QBLA6qu089MBtmPBGc18X0XoKMXTBUfYA=", + "lastModified": 1777425819, + "narHash": "sha256-95Fabu2iFqurcys/0X8pMXWmpvAsTY8T5aQ6GMxOjc8=", "owner": "srid", "repo": "heist-extra", - "rev": "970c9a373b0c7eb10c76a20f44a44fb8547d7066", + "rev": "e5a71f2870c28280cb84fe9277973998394b8bfc", "type": "github" }, "original": { From 9f412bae78c6e363fb028be487bb30269f57d75e Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:33:32 -0400 Subject: [PATCH 3/7] chore(deps): bump heist-extra to 3bd7aba (police follow-ups) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 2add6d093..a6af1c7e8 100644 --- a/flake.lock +++ b/flake.lock @@ -169,11 +169,11 @@ "heist-extra": { "flake": false, "locked": { - "lastModified": 1777425819, - "narHash": "sha256-95Fabu2iFqurcys/0X8pMXWmpvAsTY8T5aQ6GMxOjc8=", + "lastModified": 1777426406, + "narHash": "sha256-4N50ZxRC0kMKxfylHjdjwGhC+Od7WdAdVPo6wp07NSU=", "owner": "srid", "repo": "heist-extra", - "rev": "e5a71f2870c28280cb84fe9277973998394b8bfc", + "rev": "3bd7abae96f51f488d3a1108ab1c1dbc47b42ff8", "type": "github" }, "original": { From 67ae6800ee0207f9a7d5cd1c8111bc05212db4e6 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:42:08 -0400 Subject: [PATCH 4/7] test: e2e fixture + smoke scenario for #433 raw-HTML grouping Mirrors the #119 pattern: a fixture (tests/fixtures/notebook/rawhtml-details.md) exercises the issue's exact shape (a
tag with markdown content between blank lines) and a smoke scenario asserts the marker inside has a
ancestor in the rendered HTML. Without the heist-extra groupRawHtmlBlocks pass the marker would be a sibling of an empty
. --- tests/features/smoke.feature | 4 ++++ tests/fixtures/notebook/rawhtml-details.md | 19 +++++++++++++++++++ tests/step_definitions/smoke_steps.ts | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 tests/fixtures/notebook/rawhtml-details.md diff --git a/tests/features/smoke.feature b/tests/features/smoke.feature index 9d4882010..76066fbb1 100644 --- a/tests/features/smoke.feature +++ b/tests/features/smoke.feature @@ -44,6 +44,10 @@ Feature: Smoke When I open "/rawhtml.html" Then the page contains an element with data-marker "RAWHTML_DIV_OK" + Scenario: Markdown between blank-line-separated raw-HTML tags nests inside (#433) + When I open "/rawhtml-details.html" + Then the element with data-marker "RAWHTML_DETAILS_INNER" has a
ancestor + Scenario: A feed-enabled note whose query matches no notes does not crash the build (regression: #490) When I fetch "/empty-feed.xml" Then the response is a valid Atom feed diff --git a/tests/fixtures/notebook/rawhtml-details.md b/tests/fixtures/notebook/rawhtml-details.md new file mode 100644 index 000000000..ee56782be --- /dev/null +++ b/tests/fixtures/notebook/rawhtml-details.md @@ -0,0 +1,19 @@ +--- +slug: rawhtml-details +--- + +# Raw HTML group (issue #433) + +CommonMark "type 6" HTML blocks (e.g. `
`) end at the next blank +line, so Pandoc emits the open tag, the markdown content, and the close +tag as three separate AST blocks. Without grouping, the markdown +paragraph escapes the surrounding element and ends up as its sibling. +The marker on the inner paragraph is what the e2e step asserts on: +the marker must have a `
` ancestor. + +
+ +This paragraph **must** render as a child of the `
` element. +marker + +
diff --git a/tests/step_definitions/smoke_steps.ts b/tests/step_definitions/smoke_steps.ts index ddd8602a1..c5dd6c000 100644 --- a/tests/step_definitions/smoke_steps.ts +++ b/tests/step_definitions/smoke_steps.ts @@ -28,6 +28,24 @@ Then( }, ); +// #433: orphan opener/closer raw-HTML tags around markdown content used to +// produce two stranded `` wrappers with the markdown paragraph as +// a sibling of (not a child of) the surrounding element. Asserting the +// marker has a `
` ancestor proves the grouping pass landed it +// inside. +Then( + "the element with data-marker {string} has a
ancestor", + async function (this: EmanoteWorld, marker: string) { + const ancestorCount = await this.page + .locator(`details:has([data-marker="${marker}"])`) + .count(); + assert.ok( + ancestorCount > 0, + `Expected the element with data-marker="${marker}" to have a
ancestor; got ${ancestorCount} matching
. The orphan-RawHtml grouping pass (heist-extra: groupRawHtmlBlocks) likely regressed — the markdown paragraph is rendering as a sibling of
instead of a child.`, + ); + }, +); + // #285 — two complementary checks. The "no Ema exception" assertion // guards the *crash* (the original bug surface); the banner assertion // guards the *visibility* of the error so a parse failure can't fail From 4a4b4203c421765a1a7ba99e2ec1870f4d02c404 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Tue, 28 Apr 2026 21:50:08 -0400 Subject: [PATCH 5/7] test(police): emitted-HTML check for #433 (DOM-recovery would mask the bug) The first cut of this scenario asserted closest('details') on the parsed DOM. Browser HTML5 parsers recover the broken master output into a DOM that *does* nest the marker under
, so a DOM-level check passes either way. The bug is in the emitted HTML, not in the recovered DOM. Switch to fetching the raw HTTP response and verifying that the opener and closer
tags have no immediately-adjacent wrapper. Confirmed: regex matches the broken pattern on master (`
` + `
`), fails to match on the fix. --- tests/features/smoke.feature | 2 +- tests/step_definitions/smoke_steps.ts | 27 ++++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/features/smoke.feature b/tests/features/smoke.feature index 76066fbb1..4e6f26559 100644 --- a/tests/features/smoke.feature +++ b/tests/features/smoke.feature @@ -46,7 +46,7 @@ Feature: Smoke Scenario: Markdown between blank-line-separated raw-HTML tags nests inside (#433) When I open "/rawhtml-details.html" - Then the element with data-marker "RAWHTML_DETAILS_INNER" has a
ancestor + Then the emitted HTML for "/rawhtml-details.html" wraps no around its
tags Scenario: A feed-enabled note whose query matches no notes does not crash the build (regression: #490) When I fetch "/empty-feed.xml" diff --git a/tests/step_definitions/smoke_steps.ts b/tests/step_definitions/smoke_steps.ts index c5dd6c000..26620d21e 100644 --- a/tests/step_definitions/smoke_steps.ts +++ b/tests/step_definitions/smoke_steps.ts @@ -29,19 +29,24 @@ Then( ); // #433: orphan opener/closer raw-HTML tags around markdown content used to -// produce two stranded `` wrappers with the markdown paragraph as -// a sibling of (not a child of) the surrounding element. Asserting the -// marker has a `
` ancestor proves the grouping pass landed it -// inside. +// produce two stranded `` wrappers immediately adjacent to the +// `
` opener and closer. Browsers' lenient HTML5 parser recovers +// the broken stream into a DOM that nests the marker under
, so a +// DOM-level `closest("details")` check passes on master too. Catching the +// regression requires inspecting the *emitted* HTML directly. The +// load-bearing structural difference is the `
` +// adjacency on master vs. a bare `
` on the fix. Then( - "the element with data-marker {string} has a
ancestor", - async function (this: EmanoteWorld, marker: string) { - const ancestorCount = await this.page - .locator(`details:has([data-marker="${marker}"])`) - .count(); + "the emitted HTML for {string} wraps no around its
tags", + async function (this: EmanoteWorld, route: string) { + const response = await this.page.request.get(route); + assert.ok(response.ok(), `Failed to fetch ${route}: ${response.status()}`); + const html = await response.text(); + const wrappedOpener = /]*>\s*)/.test(html); + const wrappedCloser = /]*>\s*<\/details>/.test(html); assert.ok( - ancestorCount > 0, - `Expected the element with data-marker="${marker}" to have a
ancestor; got ${ancestorCount} matching
. The orphan-RawHtml grouping pass (heist-extra: groupRawHtmlBlocks) likely regressed — the markdown paragraph is rendering as a sibling of
instead of a child.`, + !wrappedOpener && !wrappedCloser, + `Expected ${route} to emit a bare
...
with no surrounding wrappers. Got wrappedOpener=${wrappedOpener}, wrappedCloser=${wrappedCloser}. The orphan-RawHtml grouping pass (heist-extra: groupRawHtmlBlocks) likely regressed.`, ); }, ); From 7d4933f1f561166a812c4ad0077d1a4101100950 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 29 Apr 2026 14:50:05 -0400 Subject: [PATCH 6/7] docs: demonstrate
markdown nesting + note the convention in .agency/do.md The fix in this PR makes
blocks separated by blank lines work as a disclosure widget with parsed markdown inside. Add a Raw HTML section to docs/guide/markdown.md showing the live syntax (and pointing back to callouts for the Emanote-native foldable variant), then update .agency/do.md so future Markdown-syntax features land their demo in the same file as a matter of convention. --- .agency/do.md | 2 ++ docs/guide/markdown.md | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/.agency/do.md b/.agency/do.md index bec84165e..dc83375c6 100644 --- a/.agency/do.md +++ b/.agency/do.md @@ -19,6 +19,8 @@ Ignore Github Actions (slow) unless user asks for it. ## Documentation Keep `README.md`, `docs/` (user documentation), and `CHANGELOG.md` (under the `Unreleased` section) in sync with user-facing changes. +New or fixed **Markdown-syntax features** should be demonstrated in [`docs/guide/markdown.md`](../docs/guide/markdown.md) so the live example serves as both reference and regression check. A working `
` block, a new callout type, a new wiki-link form — each goes there as a real rendered sample, not just a CHANGELOG note. + ## PR evidence When the change has visible UI impact (theme, layout, rendering, navigation), post a `## Evidence` PR comment with screenshots. Use judgment — backend-only diffs (parser, link resolver, model) sometimes ripple into rendering and warrant a shot anyway. diff --git a/docs/guide/markdown.md b/docs/guide/markdown.md index b6632abd7..95ff43c17 100644 --- a/docs/guide/markdown.md +++ b/docs/guide/markdown.md @@ -160,6 +160,19 @@ See [[callout]] for details. > Lorem **ipsum** dolor sit *amet*, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +## Raw HTML + +Block-level HTML is passed through verbatim. When an opening tag and its matching closing tag each sit on their own line with blank lines on either side, the markdown between them is parsed as the element's children. This is how `
` renders as a working disclosure widget with rich content inside: + +
+ +This paragraph is a child of the `
` element. Regular **markdown** still works inside — _emphasis_, [example links](https://example.com), `code spans`, and Emanote extensions like [[neuron|wiki links]] all parse normally. + +
+ +Without the blank lines, the `
...
` is treated as one opaque HTML block — markdown inside is not parsed. _For an Emanote-native foldable variant that doesn't require raw HTML at all, see the `+` / `-` callout suffix in [[callout]]._ + + {#hanchor} ## Heading anchors From 733c07c00bb3ffbfd8cb37c50e9c9aeb761541ff Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar Date: Wed, 29 Apr 2026 14:53:08 -0400 Subject: [PATCH 7/7] chore(deps): point heist-extra at master (merged via heist-extra#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that the upstream PR has landed, drop the branch ref so flake.nix tracks heist-extra master again. The locked rev advances from 3bd7aba to f496d0c (same tree — the merge was a fast-forward). --- flake.lock | 5 ++--- flake.nix | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/flake.lock b/flake.lock index a6af1c7e8..746d5d9d1 100644 --- a/flake.lock +++ b/flake.lock @@ -169,16 +169,15 @@ "heist-extra": { "flake": false, "locked": { - "lastModified": 1777426406, + "lastModified": 1777488739, "narHash": "sha256-4N50ZxRC0kMKxfylHjdjwGhC+Od7WdAdVPo6wp07NSU=", "owner": "srid", "repo": "heist-extra", - "rev": "3bd7abae96f51f488d3a1108ab1c1dbc47b42ff8", + "rev": "f496d0c8210d4b17c97d85dde589363823d45aea", "type": "github" }, "original": { "owner": "srid", - "ref": "group-orphan-rawhtml-blocks", "repo": "heist-extra", "type": "github" } diff --git a/flake.nix b/flake.nix index 93a87846f..3cadc5e85 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,7 @@ ema.flake = false; lvar.url = "github:srid/lvar/0.2.0.0"; lvar.flake = false; - heist-extra.url = "github:srid/heist-extra/group-orphan-rawhtml-blocks"; + heist-extra.url = "github:srid/heist-extra"; heist-extra.flake = false; unionmount.url = "github:srid/unionmount/0.3.0.0"; unionmount.flake = false;