Skip to content

fix(export): image headings name the resolved file; diff comments sort after the document - #1700

Merged
backnotprop merged 2 commits into
mainfrom
fix/export-image-source-and-diff-order
Oct 5, 2026
Merged

backnotprop merged 2 commits into
mainfrom
fix/export-image-source-and-diff-order

Conversation

@backnotprop

Copy link
Copy Markdown
Owner

Two low-severity export bugs from the 0.28.1 smoke tests, targeting 0.28.2.

1. Image heading names the wrong file, or none

The composer quote for a text-less media pin resolves the file through src, a lazy data-src over a data: placeholder, srcset, a video's <source>, and poster (descRawSource in the bridge). The export read only the src attribute.

Fix. The bridge now records the file it names as elementContext.sourceName, and the export heading reads that field. The value is the bridge's own descSourceName, after the same ctxScrubUrl scrub. Core's parseHtmlElementContext validates it and caps it at 80 characters. The field is optional and additive. Contexts saved without it fall back to the scrubbed src attribute, which is the old behavior. The resolution happens only in the bridge, so the heading and the quote cannot name different files.

I chose this over adding srcset / data-src / poster to the attribute allowlist because the export never sees child elements: a video's <source> and <picture><source> cannot be rebuilt from the element's own attributes. The allowlist is unchanged.

The bridge also resolves <picture><source srcset><img> now (the first candidate). Before, neither the quote nor the export named a file for it.

Pinned element Before (origin/main) After
<img src="data:image/gif…" data-src="img/real-photo.png?v=3"> Feedback on the <img> element (data:image/gif) Feedback on the <img> element (real-photo.png)
<img srcset="img/hero-480.jpg 480w, …"> Feedback on the <img> element Feedback on the <img> element (hero-480.jpg)
<picture><source srcset="img/pic.avif"><img></picture> Feedback on the <img> element (the quote had no file either) Feedback on the <img> element (pic.avif)

2. Version-diff comments printed first

A diff-view comment has blockId diff-block-N. That id is not in blocks, so sortAnnotationsInDocumentOrder ranked it -1, ahead of everything else. N indexes the diff against the selected base. Nothing on the annotation maps it back to a document block, and the export has no base version to recompute the diff from.

Fix. Diff comments now print together after the document's other comments, ordered by diff index. The diff walks the document, so diff order is also document order among the diff comments. The comparator is unchanged for every non-diff pair, so a document with no diff comments exports byte-identically. A test fuzzes this against the pre-fix comparator over 200 random annotation sets that include blockless ids. [In diff content] is kept.

BEFORE                                            AFTER
## 1. [In diff content] Feedback on: "- old…      ## 1. (line 3) Feedback on: "First paragraph"
## 2. (line 3) Feedback on: "First paragraph"     ## 2. (line 9) Feedback on: "Fourth paragraph"
## 3. (line 9) Feedback on: "Fourth paragraph"    ## 3. [In diff content] Feedback on: "- old…

Tests

  • packages/ui/utils/parser.diffOrder.test.ts (new, pure): root and linked-document order, with and without blocks; a global comment keeps its place; fuzzed byte-identity with no diff comments.
  • packages/ui/utils/parser.test.ts: the heading uses sourceName for srcset-only and lazy images; a name equal to the file is not repeated.
  • packages/ui/components/html-viewer/srcdoc.test.ts (already in DOM_TESTS): runs the real bridge on srcset-only, lazy data-src, <picture>, video poster, video <source> and src with a query. For each case it pins the element, passes the posted context through parseHtmlElementContext, exports it, and asserts that the heading names the same file as the composer quote and that no query secret survives.

Verification

  • bun run typecheck passes.
  • bun test packages/ui packages/editor packages/core: 1694 pass, 0 fail.
  • Every packages/ui and packages/editor DOM test in test.yml (DOM_TESTS=1 --isolate): 1152 pass, 0 fail.
  • scripts/dom-test-allowlist.test.ts passes.
  • apps/guides-show check:manifest is in sync; the guide viewer build is unchanged.
  • Headless Chromium against a real plannotator annotate session, using the built hook HTML:
    • A raw-HTML page with a srcset-only image, a lazy data-src image and a <picture>. Each pin's composer quote and the submitted /api/feedback heading both name hero-480.jpg, real-photo.png and pic.avif.
    • A markdown file with a saved previous version. Comments on paragraph 1 and paragraph 4, plus one on the changed paragraph in the Classic diff view, submit in this order: line 3, line 9, [In diff content].

Notes

  • Core changed: HtmlElementContext.sourceName? and its validation in packages/core/html-anchor.ts. UI code reads the field untyped and does not need a new core to compile. A host on an older published core's validator drops the field, and its export falls back to src as today. Publish core before ui as usual.
  • Files the other agent is editing are untouched: packages/editor/App.tsx, annotateSubmission.ts and apps/hook/server/host-result.ts.
  • Bridge protocol is unchanged (no BRIDGE_PROTOCOL_VERSION bump): the context field is additive in both directions.

…t after the document

Two export bugs found by the 0.28.1 smoke tests.

Image source in the heading: the composer quote for a text-less media pin
resolves the file through src, a lazy data-src over a data: placeholder,
srcset, a video's <source> and poster (descRawSource in the bridge), but the
export read only the src attribute, so a srcset-only image exported as a bare
"Feedback on the <img> element" and a lazy one as "(data:image/gif)". The
bridge now records the file it names as elementContext.sourceName (validated
in core's parseHtmlElementContext, capped, additive), and the export heading
reads it, falling back to the src attribute for contexts saved before. One
resolution, in the bridge: the heading and the quote cannot disagree. The
bridge also resolves <picture><source srcset><img>, which named nothing before.

Diff comment order: version-diff comments (blockId diff-block-N) are not in
the document's blocks, so they ranked -1 and printed FIRST. N indexes the diff
against the selected base and nothing maps it back to a block, so they now
print together after the document's other comments, in diff order. A list
with no diff comments sorts exactly as before (fuzzed against the old
comparator). The [In diff content] label is unchanged.
…ole context

sourceName was in neither shed order, so an image pin whose context only fit
without it (a long live-app route) passed the bridge but was dropped whole by
core's validator. Append it to both orders, last: the export heading falls
back to the src attribute without it. Core's order is now the exported
ELEMENT_CONTEXT_SHED_ORDER and a test keeps the bridge's CTX_SHED_ORDER equal
to it.
@backnotprop
backnotprop merged commit 2971016 into main Oct 5, 2026
28 checks passed
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