Skip to content

Wait for excalidraw to finish rendering before copying the html - #750

Open
rettier-claudi wants to merge 1 commit into
KosmosisDire:masterfrom
rettier-claudi:fix/excalidraw-export-race
Open

Wait for excalidraw to finish rendering before copying the html#750
rettier-claudi wants to merge 1 commit into
KosmosisDire:masterfrom
rettier-claudi:fix/excalidraw-export-race

Conversation

@rettier-claudi

Copy link
Copy Markdown

Disclosure

This patch was written by an AI agent (Claude Code), driven and reviewed by me. I am disclosing that up front so you can weigh the diff accordingly. The bug itself is real and hit me in my own vault, and the fix is verified against a reproduction rather than reasoned about in the abstract — details below. Happy to rework anything you'd rather see solved differently.

The bug

Drawings embedded into a note (![[Drawing.excalidraw]]) are rendered by an async markdown post processor of the excalidraw plugin, which replaces the .internal-embed element obsidian created with the drawing once it is done. That work is additionally queued in the excalidraw plugin's global task queue with a concurrency limit (3 by default).

Obsidian does not wait for post processors before it reports a page as rendered, and renderMarkdownView only waits for sections, transclusions and block-language-* blocks — nothing that covers excalidraw. So newSizerEl.innerHTML = sizerEl.innerHTML can snapshot the page while the placeholder embeds are still in it. The drawing is then missing from the exported page and a bare <span class="internal-embed ..."> is left in its place. With one or two small drawings the post processor usually wins the race, with several or large ones it does not, which is why this looks random.

renderExcalidraw had the same problem when a drawing is exported as its own page: it slept a fixed 500ms and then read view.excalidrawData.scene, which throws a TypeError or yields an empty drawing when the view has not finished loading yet.

The change

  • renderMarkdownView waits for pending excalidraw embeds before copying the preview element. An embed counts as pending while it is still an .internal-embed whose src resolves to a file the excalidraw plugin claims via isExcalidrawFile, and which contains none of excalidraw's own markers. Block and section references are excluded because excalidraw embeds those as text, so they would never resolve. The wait is bounded (3s without progress, 30s hard cap) and logs a warning naming the embeds it gave up on.
  • Inserting the drawings grows the document a lot, which can make obsidian unload the sections that are now outside of the viewport — the page would then be exported with those sections missing. The existing all-sections check is therefore repeated after the wait, and the existing fallback renderer is used if sections went missing.
  • renderMarkdownViewFallback waits per section.
  • renderExcalidraw waits for the view to report its scene as loaded instead of sleeping a fixed amount, and returns a proper failRender message instead of throwing. The view flags are only checked when the installed excalidraw version actually exposes them, so older versions still work.

Everything is a no-op when the excalidraw plugin is not enabled, and the wait returns immediately when a page has no pending excalidraw embeds.

Verification

Using this repo's docker export harness (real obsidian under Xvfb) on a vault with 8 large drawings embedded into one note, three runs per variant:

note page
master (1.9.2) 0–2 of 8 drawings exported, 6–8 leftover <span class="internal-embed">
this branch 8 of 8, no leftover embeds, page otherwise unchanged (same paragraph count)

The standalone drawing pages are unchanged in both variants, and lighter vaults (4 and 2 embedded drawings) export identically before and after, so the fast path is not affected.

Known limitation, not addressed here

This only helps in excalidraw's Native SVG embed mode. In PNG / SVG-image mode excalidraw produces blob: URLs which inlineMedia cannot resolve — that is the separate limitation validateSettings already warns about, and I left it alone.

Drawings embedded into a note (![[Drawing.excalidraw]]) are rendered by an
async markdown post processor of the excalidraw plugin, which replaces the
`.internal-embed` element obsidian created with the drawing once it is done.
Obsidian does not wait for post processors before it reports a page as
rendered, and the excalidraw plugin additionally queues that work in a global
task queue with a concurrency limit, so on pages with several or large drawings
the export copied the html while the placeholder embeds were still in it. The
drawings were then missing from the exported page and only the bare embed
element was left behind, seemingly at random.

renderMarkdownView now waits for those embeds to be resolved before it copies
the preview element. Inserting the drawings grows the document, which can make
obsidian unload the sections that are now outside of the viewport, so the
existing section check is repeated afterwards and the fallback renderer is used
if sections went missing. renderMarkdownViewFallback waits per section.

renderExcalidraw had the same race when a drawing is exported as its own page:
it slept a fixed 500ms and then read `view.excalidrawData.scene`, which throws
or yields an empty drawing when the view has not finished loading. It now waits
for the view to report the scene as loaded and fails with a proper message
instead of a TypeError.

Verified with the docker export harness on a vault of 8 embedded drawings:
before the change 0-2 of 8 drawings were exported over three runs, after it
8 of 8 in each run, with the page otherwise unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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