Skip to content

feat: render embedded asset files inline - #204

Merged
avelino merged 3 commits into
mainfrom
avelino/issue-203
Aug 2, 2026
Merged

feat: render embedded asset files inline#204
avelino merged 3 commits into
mainfrom
avelino/issue-203

Conversation

@avelino

@avelino avelino commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Uploading or importing a file already copied it into assets/, but nothing rendered it. Every asset, images included, landed as a plain [name](assets/…) link, so an imported graph showed clickable text where you expected to see the picture.

![alt](url) is now a real inline token in outl-md, parsed once and consumed by every client, so there's no parallel tokenizer to keep in sync. Desktop and mobile render an inline . A local asset loads its bytes through a new read_asset_data_url command as a size-capped data URL, a remote image loads straight from its URL. The TUI paints a text placeholder since a terminal can't show pixels. The importers now emit the embed form for images, and every non-image file stays a click-to-open chip.

Fixes #203

Uploading or importing a file already copied it into assets/, but nothing rendered it. Every asset, images included, landed as a plain [name](assets/…) link, so an imported graph showed clickable text where you expected to see the picture.

![alt](url) is now a real inline token in outl-md, parsed once and consumed by every client, so there's no parallel tokenizer to keep in sync. Desktop and mobile render an inline <img>. A local asset loads its bytes through a new read_asset_data_url command as a size-capped data URL, a remote image loads straight from its URL. The TUI paints a text placeholder since a terminal can't show pixels. The importers now emit the embed form for images, and every non-image file stays a click-to-open chip.

Fixes #203

Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 31, 2026 12:05
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added embedded Markdown image support for local and remote assets.
    • Local images render inline across desktop and mobile.
    • Non-image attachments appear as clickable file chips.
    • TUI displays image or file placeholders with accessible labels.
    • Roam, Logseq, and Obsidian imports preserve image embeds.
    • Added fallback handling for unavailable or unsupported image assets.
  • Documentation

    • Documented supported image formats, asset handling, fallbacks, and importer behavior.

Walkthrough

The change adds Markdown image tokens, image-aware asset imports, secure local asset data-URL loading, shared desktop/mobile rendering, file-chip fallbacks, and TUI placeholders.

Changes

Embedded asset rendering

Layer / File(s) Summary
Markdown image tokenization
crates/outl-md/src/inline.rs, crates/outl-md/tests/*
The tokenizer recognizes ![alt](url), preserves empty alt text, rejects invalid URLs, and keeps regular links and wiki embeds distinct.
Image-aware asset imports
crates/outl-actions/src/asset.rs, crates/outl-import/tests/*_e2e.rs
Image files now import as Markdown image embeds. Other files remain regular links.
Asset data URL backend
crates/outl-tauri-shared/..., crates/outl-desktop/..., crates/outl-mobile/...
The shared command resolves assets, applies a 25 MiB limit, detects MIME types, returns Base64 data URLs, and is registered on desktop and mobile.
Shared frontend asset rendering
crates/outl-frontend-shared/src/api/*, crates/outl-frontend-shared/src/links/*, crates/outl-frontend-shared/src/markdown/*
Remote images render directly. Local images load through the data-URL command. Failed or non-image assets render as clickable file chips.
TUI rendering and documentation
crates/outl-tui/src/view/inline.rs, docs/markdown-format.md, CHANGELOG.md
The TUI renders image and file placeholders. Documentation describes syntax, supported formats, resolution, limits, and fallback behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MarkdownTokenizer
  participant MarkdownInline
  participant readAssetDataUrl
  participant read_asset_data_url
  participant WorkspaceAssets
  MarkdownTokenizer->>MarkdownInline: provide Image token
  MarkdownInline->>readAssetDataUrl: request local asset
  readAssetDataUrl->>read_asset_data_url: invoke Tauri command
  read_asset_data_url->>WorkspaceAssets: resolve and read bytes
  WorkspaceAssets-->>read_asset_data_url: asset bytes
  read_asset_data_url-->>MarkdownInline: data URL
  MarkdownInline-->>MarkdownInline: render image or file chip
Loading

Possibly related PRs

  • avelino/outl#198 — Both changes modify shared MarkdownInline rendering and inline token handling.
  • avelino/outl#201 — This change extends the asset-link and import infrastructure with image embeds and data-URL loading.

Poem

A rabbit sees image tokens glow,
While file chips line up below.
Local bytes hop through a safe gate,
Remote images render straight.
The TUI keeps a gentle sign:
“Your asset is doing fine!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: inline rendering for embedded asset files.
Description check ✅ Passed The description directly explains inline asset rendering, tokenizer support, client behavior, importer changes, and fallback handling.
Linked Issues check ✅ Passed The changes implement the linked issue objectives for image tokens, client rendering, safe local loading, file chips, TUI placeholders, importers, and documentation.
Out of Scope Changes check ✅ Passed The changes remain within the linked issue scope and include related implementation, tests, documentation, and platform wiring.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements inline rendering for embedded assets using standard Markdown image syntax (![alt](url)), by adding a first-class image token in outl-md and rendering it consistently across TUI + desktop + mobile + importers (Fixes #203).

Changes:

  • Add InlineTok::Image / owned InlineToken::Image { alt, href } to outl-md, with tests and documentation updates.
  • Render image tokens in GUI clients via shared <MarkdownInline /> (local assets/... via a new read_asset_data_url command; remote http(s) directly), and render placeholders in the TUI.
  • Update asset import/attach paths and importer E2E tests so images emit the embed form (![…](assets/…)) while non-images remain plain links.

Reviewed changes

Copilot reviewed 23 out of 24 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/markdown-format.md Documents ![alt](url) as an inline image/asset token and clarifies asset-link behavior.
crates/outl-tui/src/view/inline.rs Adds TUI placeholder rendering for InlineTok::Image plus cursor-aligned raw rendering and tests.
crates/outl-tauri-shared/src/commands/asset.rs Adds read_asset_data_url command to load local assets as size-capped data: URLs.
crates/outl-tauri-shared/CLAUDE.md Updates crate docs to include the new asset inline-render command.
crates/outl-tauri-shared/Cargo.toml Adds base64 dependency for data URL encoding.
crates/outl-mobile/src-tauri/src/lib.rs Exposes read_asset_data_url command to the mobile webview.
crates/outl-mobile/src-tauri/src/commands/asset.rs Adds the read_asset_data_url Tauri command wrapper for mobile.
crates/outl-md/tests/tokenize_owned.rs Adds owned-token wire-form tests including the new image variant.
crates/outl-md/tests/inline.rs Adds borrowed-token tokenizer tests for ![alt](url) parsing behavior.
crates/outl-md/src/inline.rs Introduces InlineTok::Image + owned form support, parsing, and source roundtrip.
crates/outl-md/CLAUDE.md Documents the new image token and matching order guarantees.
crates/outl-import/tests/obsidian_e2e.rs Updates import expectations so imported images become ![…](assets/…).
crates/outl-import/tests/logseq_e2e.rs Updates import expectations so imported images become ![…](assets/…).
crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx Renders image tokens as inline <img> (local via data URL) or file chips for non-images.
crates/outl-frontend-shared/src/markdown/MarkdownInline.test.tsx Adds unit tests for the new image/asset rendering behavior (mocking backend call).
crates/outl-frontend-shared/src/links/index.ts Adds shared helpers for classifying image-vs-non-image and extracting labels.
crates/outl-frontend-shared/src/api/types.ts Extends the InlineToken TS union with { kind: "image"; alt; href }.
crates/outl-frontend-shared/src/api/commands.ts Adds the readAssetDataUrl command wrapper.
crates/outl-frontend-shared/CLAUDE.md Documents the shared frontend behavior for ![alt](href) rendering.
crates/outl-desktop/src-tauri/src/lib.rs Exposes read_asset_data_url command to the desktop webview.
crates/outl-desktop/src-tauri/src/commands/asset.rs Adds the read_asset_data_url Tauri command wrapper for desktop.
crates/outl-actions/src/asset.rs Changes imported image assets to emit ![…](assets/…) instead of [...](...); updates tests.
CHANGELOG.md Adds release notes for embedded asset rendering and related behavior changes.
Cargo.lock Locks the new base64 dependency for outl-tauri-shared.

Comment thread crates/outl-tui/src/view/inline.rs
Comment thread crates/outl-frontend-shared/src/links/index.ts
Comment thread crates/outl-tauri-shared/src/commands/asset.rs Outdated
Comment thread CHANGELOG.md
Comment thread crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/outl-frontend-shared/src/markdown/MarkdownInline.test.tsx`:
- Around line 11-16: Wrap the readAssetDataUrlMock creation in vi.hoisted() so
it is initialized before the hoisted vi.mock("../api/commands") factory
executes. Keep the mock’s existing resolved data URL behavior and preserve the
readAssetDataUrlMock reference for later mockClear() calls.

In `@crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx`:
- Around line 252-264: The remote-image rendering branch in MarkdownInline must
not load unreviewed HTTP(S) URLs automatically. Add the existing per-image or
per-page opt-in before rendering the <img> for non-asset links, while preserving
backend loading for isAssetLink paths; if remote loading is intentionally
retained as MVP behavior, document that privacy decision at the relevant
implementation point.

In `@crates/outl-md/src/inline.rs`:
- Around line 749-773: The try_image function currently uses find(']') to locate
the alt-text terminator without accounting for escaped closing brackets emitted
by escape_link_label. Replace the simple bracket search with a scan through the
alt text that identifies an unescaped closing bracket (one not preceded by a
backslash). Update the logic around bracket_close to skip escaped brackets until
finding a truly unescaped terminator, so syntax like
![pic\]final.png](assets/hash.png) parses correctly. Add a regression test case
for this escaped-bracket scenario.

In `@crates/outl-tauri-shared/src/commands/asset.rs`:
- Around line 117-128: Update the asset-reading flow around the metadata check
and std::fs::read to open one file handle and read at most MAX_DATA_URL_BYTES +
1 bytes, rejecting the result when the extra byte is present before Base64
encoding. Preserve the existing oversized-asset error behavior, and add a
regression test covering the bounded reader or a file that changes between size
observation and reading.

In `@crates/outl-tui/src/view/inline.rs`:
- Around line 214-224: Update the image-link rendering logic around
is_image_target to strip query and fragment suffixes before image classification
and filename fallback, while preserving the existing alt-text behavior. In
crates/outl-tui/src/view/inline.rs lines 520-550, add a regression test covering
empty alt text with a remote image URL containing a query string and assert the
expected image placeholder and fallback label.
- Around line 207-225: Extend the cursor link resolution used by decide_gx so
InlineTok::Image entries with asset targets are treated like links and passed to
open_external_url, including images where the alt text and URL are identical.
Preserve existing InlineTok::Link behavior and add a regression test covering gx
on a markdown image.
- Around line 520-550: Extend the image_pretty_shows_glyph_raw_keeps_source test
function to cover additional fallback paths. Add test cases using
render_pretty_block_text for an image reference with empty alt text (e.g.,
![](assets/x.png)) and a remote image URL with a query string (e.g.,
![cover](https://example.com/image.png?size=large)). For each case, assert that
the image glyph appears in the pretty render and that the normalized filename
fallback is correctly handled, matching the pattern of the existing assertions
for .png and .pdf cases.

In `@docs/markdown-format.md`:
- Around line 285-287: Update docs/markdown-format.md lines 285-287 to state the
100 MiB [assets] max_bytes ingestion limit separately from the 25 MiB inline
data-URL rendering limit. Update CHANGELOG.md line 13 to use “25 MiB” instead of
“25 MB” for the inline limit.
- Around line 280-281: Update the image-extension documentation to include tif,
and modify is_image_target to remove query strings before checking the file
extension, matching isImagePath behavior so URLs such as photo.png?size=large
are classified as images consistently across renderers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ad2eee74-e1bb-4976-a521-80ae70ecb8de

📥 Commits

Reviewing files that changed from the base of the PR and between e92de50 and 27f82a7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • CHANGELOG.md
  • crates/outl-actions/src/asset.rs
  • crates/outl-desktop/src-tauri/src/commands/asset.rs
  • crates/outl-desktop/src-tauri/src/lib.rs
  • crates/outl-frontend-shared/CLAUDE.md
  • crates/outl-frontend-shared/src/api/commands.ts
  • crates/outl-frontend-shared/src/api/types.ts
  • crates/outl-frontend-shared/src/links/index.ts
  • crates/outl-frontend-shared/src/markdown/MarkdownInline.test.tsx
  • crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx
  • crates/outl-import/tests/logseq_e2e.rs
  • crates/outl-import/tests/obsidian_e2e.rs
  • crates/outl-md/CLAUDE.md
  • crates/outl-md/src/inline.rs
  • crates/outl-md/tests/inline.rs
  • crates/outl-md/tests/tokenize_owned.rs
  • crates/outl-mobile/src-tauri/src/commands/asset.rs
  • crates/outl-mobile/src-tauri/src/lib.rs
  • crates/outl-tauri-shared/CLAUDE.md
  • crates/outl-tauri-shared/Cargo.toml
  • crates/outl-tauri-shared/src/commands/asset.rs
  • crates/outl-tui/src/view/inline.rs
  • docs/markdown-format.md

Comment thread crates/outl-frontend-shared/src/markdown/MarkdownInline.test.tsx
Comment thread crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx Outdated
Comment thread crates/outl-md/src/inline.rs
Comment thread crates/outl-tauri-shared/src/commands/asset.rs Outdated
Comment thread crates/outl-tui/src/view/inline.rs
Comment thread crates/outl-tui/src/view/inline.rs
Comment thread crates/outl-tui/src/view/inline.rs
Comment thread docs/markdown-format.md Outdated
Comment thread docs/markdown-format.md
Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docs/markdown-format.md (1)

217-217: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Document only supported remote schemes.

The table says “a remote URL,” but remote image rendering accepts only http and https.
Change this to “a remote HTTP(S) URL” to match the renderer and Line 288.

Based on coding guidelines: isSafeHttpUrl must allow only http and https.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/markdown-format.md` at line 217, Update the image-link description in
the markdown format table to specify “a remote HTTP(S) URL” instead of a generic
remote URL, matching the renderer’s supported schemes and the wording near the
existing asset-link guidance.

Source: Coding guidelines

crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx (1)

298-334: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache readAssetDataUrl results to avoid repeated disk reads and re-encoding.

Each image token creates its own createResource(() => tok.href, readAssetDataUrl). The same asset referenced twice on a page, or a MarkdownInline remount (e.g., during list virtualization or navigation back), re-triggers a full IPC round-trip, disk read, and base64 re-encoding of up to 25 MiB on the backend. Add a small shared cache (e.g., a module-level Map<string, Promise<string>>, with a cap/eviction so it doesn't grow unbounded for large workspaces) keyed by tok.href, and reuse the in-flight or resolved promise across resources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx` around lines 298
- 334, Update the image handling in MarkdownInline to reuse readAssetDataUrl
results through a module-level, bounded cache keyed by tok.href. Store and
return the same Promise for both in-flight and resolved requests so duplicate
image tokens and MarkdownInline remounts avoid repeated IPC, disk reads, and
encoding; evict older entries when the configured cap is reached.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx`:
- Around line 298-334: Update the image handling in MarkdownInline to reuse
readAssetDataUrl results through a module-level, bounded cache keyed by
tok.href. Store and return the same Promise for both in-flight and resolved
requests so duplicate image tokens and MarkdownInline remounts avoid repeated
IPC, disk reads, and encoding; evict older entries when the configured cap is
reached.

In `@docs/markdown-format.md`:
- Line 217: Update the image-link description in the markdown format table to
specify “a remote HTTP(S) URL” instead of a generic remote URL, matching the
renderer’s supported schemes and the wording near the existing asset-link
guidance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef01b6bc-e03f-4b9e-8064-c30d2f4892f1

📥 Commits

Reviewing files that changed from the base of the PR and between 27f82a7 and a43727c.

📒 Files selected for processing (9)
  • crates/outl-frontend-shared/CLAUDE.md
  • crates/outl-frontend-shared/src/links/index.ts
  • crates/outl-frontend-shared/src/markdown/MarkdownInline.test.tsx
  • crates/outl-frontend-shared/src/markdown/MarkdownInline.tsx
  • crates/outl-md/tests/inline.rs
  • crates/outl-md/tests/tokenize_owned.rs
  • crates/outl-tauri-shared/CLAUDE.md
  • crates/outl-tauri-shared/src/commands/asset.rs
  • docs/markdown-format.md

Signed-off-by: Avelino <31996+avelino@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/outl-frontend-shared/src/links/index.test.ts`:
- Around line 3-5: Add boundary coverage for isAssetLink in the existing test
suite: import isAssetLink and assert it accepts assets/…, ./assets/…, and
/assets/… paths while rejecting scheme-based URLs, including https://…/assets/….
Ensure the tests enforce that any value containing :// is not classified as a
workspace asset.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4cc01613-1930-4a70-9539-b94b62d4da16

📥 Commits

Reviewing files that changed from the base of the PR and between a43727c and 9e9d274.

📒 Files selected for processing (1)
  • crates/outl-frontend-shared/src/links/index.test.ts

Comment thread crates/outl-frontend-shared/src/links/index.test.ts
@avelino
avelino merged commit ed650d0 into main Aug 2, 2026
19 checks passed
@avelino
avelino deleted the avelino/issue-203 branch August 2, 2026 13:52
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.

feat(md): render embedded asset files — inline images, PDF, generic file chips

2 participants