Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ let document = anydoc::to_document(&bytes, None)?;

- **One output for every format.** Each format parses into a shared document model and renders through a single Markdown serializer, so escaping, tables, heading anchors, and footnotes behave identically whether the input was a `.doc` from 2003 or a `.pptx` from yesterday.
- **Full document structure.** Headings with anchors, bold/italic/strikethrough, inline code and code blocks, links and internal cross-references, bulleted/numbered/nested/task lists with the source's own numbering, tables with merged cells and header rows, block quotes, footnotes and endnotes, and speaker notes.
- **Embedded assets.** Images and embedded objects render as their alt text in the Markdown, and the raw bytes stay available on the document model, tagged with their media type. Images with an external URL become ordinary Markdown images.
- **Embedded assets.** Images and embedded objects become Markdown image refs of the form `![alt](asset:N)`, where `N` indexes `Document.assets` (raw bytes + media type). Rewrite those hrefs after writing the files out, or consume the document model directly. Images with an external URL become ordinary Markdown images.
- **Content-based format detection.** The format is read from the bytes themselves (PDF header, RTF open group, OLE stream names, ZIP package mimetype), so mislabeled files still convert correctly.
- **Fast.** Pure Rust, no ML models, no external services. Median conversion time is under 5ms per document.
- **Bindings that stay out of the way.** Node.js conversion runs on the libuv thread pool and never blocks the event loop; Python releases the GIL so other threads keep running. TypeScript types and Python stubs ship with the packages.
Expand Down
14 changes: 10 additions & 4 deletions src/render/markdown/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,16 @@ fn render_image(
escape_text(alt.trim(), ctx, EscapeOpts { in_label: true, ..Default::default() });
let _ = write!(out, "![{}]({})", alt, format_url(url));
}
// Embedded assets render as their alt text: Markdown cannot embed
// bytes, and the bytes stay available in `Document::assets`. A
// source-less image has only its alt text to offer.
ImageSource::Asset(_) | ImageSource::Unavailable => {
// Embedded assets cannot carry bytes in Markdown, but they keep a
// stable positional marker (`asset:N`) that indexes `Document::assets`
// so callers can rewrite the href after writing files out.
ImageSource::Asset(id) => {
let alt =
escape_text(alt.trim(), ctx, EscapeOpts { in_label: true, ..Default::default() });
let _ = write!(out, "![{}](asset:{})", alt, id.0);
}
// A source-less image has only its alt text to offer.
ImageSource::Unavailable => {
if !alt.trim().is_empty() {
out.push_str(&escape_text(
alt.trim(),
Expand Down
20 changes: 20 additions & 0 deletions src/render/markdown/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ fn sourceless_image_renders_alt_text() {
assert_eq!(md, "chart\n");
}

#[test]
fn embedded_asset_image_emits_asset_href() {
use crate::model::AssetId;
let md = doc(vec![Block::Paragraph(vec![Inline::Image {
alt: "photo".into(),
source: ImageSource::Asset(AssetId(0)),
}])]);
assert_eq!(md, "![photo](asset:0)\n");
}

#[test]
fn embedded_asset_empty_alt_keeps_positional_marker() {
use crate::model::AssetId;
let md = doc(vec![Block::Paragraph(vec![Inline::Image {
alt: "".into(),
source: ImageSource::Asset(AssetId(3)),
}])]);
assert_eq!(md, "![](asset:3)\n");
}

#[test]
fn composite_marker_labels_are_escaped() {
// M15: source-derived labels must not alter Markdown structure.
Expand Down
4 changes: 2 additions & 2 deletions tests/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,8 @@ fn doc_inline_picture_is_retained() {
);
}

/// The RTF `\pict` payload is retained as an asset (the Markdown output
/// shows only the alt text, which is empty for pictures without one).
/// The RTF `\pict` payload is retained as an asset (Markdown references it
/// as `asset:N` when the picture appears as an image inline).
#[test]
fn rtf_inline_picture_is_retained() {
let bytes = std::fs::read(fixture_root().join("rtf").join("text.rtf")).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__doc__text.doc.snap
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Jump to the bookmarked paragraph.

## Objects

Inline image: done.
Inline image: ![](asset:0) done.

Text box: after the box.

Expand Down
140 changes: 140 additions & 0 deletions tests/snapshots/snapshots__docx__handmade-manyrefs.docx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,143 @@ source: tests/snapshots.rs
expression: output
---
Seventy references to one image follow.

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)

![](asset:0)
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__docx__handmade-rich.docx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Rich objects follow.
- Build
- Ship

tiny dot image
![tiny dot image](asset:0)

Embedded object: Excel.Sheet.12

Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__docx__text.docx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark).

## Objects

Inline image: tiny red dot done.
Inline image: ![tiny red dot](asset:0) done.

Text box: after the box.

Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__epub__book.epub.snap
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ A list of things:

See [Chapter Two](#epub-text-ch002-xhtml-chapter-two) for the table, or jump straight to [the marked paragraph](#epub-text-ch002-xhtml-markpoint).

tiny dot tiny dot
![tiny dot](asset:0) ![tiny dot](asset:0)

<a id="epub-text-ch002-xhtml-chapter-two"></a>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark).

Objects

Inline image: tiny red dot done.
Inline image: ![tiny red dot](asset:0) done.

Text box: after the box.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Jump to [the bookmarked paragraph](#plainmark).

Objects

Inline image: tiny red dot done.
Inline image: ![tiny red dot](asset:0) done.

Text box: after the box.

Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__odt__text.odt.snap
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Jump to [the bookmarked paragraph](#plainmark).

## Objects

Inline image: tiny red dot done.
Inline image: ![tiny red dot](asset:0) done.

Text box: after the box.

Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshots__rtf__text.rtf.snap
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Jump to [the bookmarked paragraph](#plainmark).

## Objects

Inline image: done.
Inline image: ![](asset:0) done.

Text box: Inside the text box.

Expand Down