Skip to content
Closed
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 @@ -39,7 +39,7 @@ rows are the enforced release-mode bounds plus one dated observation.

| Measurement | Value | Version | Platform | Build mode | Input | Command | Statistic | Measured on |
|---|---|---|---|---|---|---|---|---|
| Crates.io archive: rdocx | 4,105,912 compressed bytes, 54,118,469 member bytes, 37 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Crates.io archive: rdocx | 4,112,736 compressed bytes, 54,143,654 member bytes, 37 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Large-document layout throughput | minimum 250 pages/s, observed 31,019.1 pages/s | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 one-page paragraphs with deterministic fonts | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | pages per wall-clock second | 2026-09-19 |
| Large-document layout peak allocation | maximum 64 MiB, observed 29.03 MiB | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 one-page paragraphs with deterministic fonts | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | peak live allocation | 2026-09-19 |
| Large-document PDF throughput | minimum 1,000 pages/s, observed 60,058.0 pages/s | rdocx 0.14.0 | macOS 26.6.2, Apple M5 Max, arm64 | release, one test thread | 1,000 deterministic layout pages | `cargo test -p rdocx --test regression_test --release a_thousand_page_document_paginates_and_renders_within_the_declared_limits -- --ignored --exact --nocapture --test-threads=1` | pages per wall-clock second | 2026-09-19 |
Expand Down
2 changes: 1 addition & 1 deletion crates/rdocx-oxml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ schema order, and retains unmodelled XML alongside typed edits.

| Measurement | Value | Version | Platform | Build mode | Input | Command | Statistic | Measured on |
|---|---|---|---|---|---|---|---|---|
| Crates.io archive: rdocx-oxml | 565,559 compressed bytes, 3,484,893 member bytes, 32 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx-oxml` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |
| Crates.io archive: rdocx-oxml | 565,548 compressed bytes, 3,484,892 member bytes, 32 members | 0.16.4 | macOS 26.6.2, Apple M5 Max, arm64 | `cargo publish --workspace --dry-run --allow-dirty --locked` | Tracked `rdocx-oxml` package inventory | `python3 scripts/readme_doctests.py --record-measurements` | gzip archive bytes, tar member bytes, tar member count | 2026-10-28 |

## Use it when

Expand Down
2 changes: 1 addition & 1 deletion crates/rdocx-oxml/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ impl MathRadical {
degree: MathArgument::default(),
base,
hide_degree: true,
degree_present: false,
degree_present: true,
preservation: Preservation::default(),
}
}
Expand Down
14 changes: 13 additions & 1 deletion crates/rdocx-py/src/paragraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,11 +621,23 @@ impl PyParagraph {
}
None => None,
};
let link_style = match url {
Some(_) => Some(
candidate
.ensure_hyperlink_style()
.map_err(|error| crate::rdocx_to_pyerr(py, error))?,
),
None => None,
};
let append = |paragraph: &mut rdocx::Paragraph<'_>| {
let run_index = paragraph.run_count();
match &relationship_id {
Some(relationship_id) => {
paragraph.add_hyperlink_with_tooltip(text, relationship_id, tooltip)?;
let mut run =
paragraph.add_hyperlink_with_tooltip(text, relationship_id, tooltip)?;
if let Some(link_style) = &link_style {
run.set_style(link_style);
}
}
None => {
paragraph.add_run(text);
Expand Down
74 changes: 70 additions & 4 deletions crates/rdocx-py/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5265,7 +5265,8 @@ def test_fx201_note_reference_creation_uses_checked_paragraph():
reference = document.add_paragraph("reference")
retired_run = reference.runs[0]
note = document.add_footnote(reference, "note content")
assert note.paragraphs[0].text == "note content"
# The space run Word writes after the note's reference mark.
assert note.paragraphs[0].text == " note content"
assert reference.text == "reference"
with pytest.raises(rdocx.StaleElementError):
_ = retired_run.text
Expand Down Expand Up @@ -5695,7 +5696,7 @@ def test_fx201_checked_note_cell_boundary_preserves_other_owners_and_refuses_sta
table.cell(0, 0).text = "cell reference"
reference = document.tables[0].cell(0, 0).paragraphs[0].runs[0]
note = getattr(document, "add_" + family)(reference, family + " content")
assert note.paragraphs[0].text == family + " content"
assert note.paragraphs[0].text == " " + family + " content"
before = document.to_bytes()
with pytest.raises(rdocx.StaleElementError):
getattr(document, "add_" + family)(reference, "stale refused")
Expand Down Expand Up @@ -10048,7 +10049,7 @@ def test_form_protection_and_permission_composition():
(cell_run, "cell held"),
(header, "header held"),
(header_run, "header held"),
(note, "note held"),
(note, " note held"),
(note_run, "note held"),
)
before = document.to_bytes()
Expand Down Expand Up @@ -10094,7 +10095,7 @@ def test_form_protection_and_permission_composition():
assert document.paragraphs[0].text == body.text == "body held"
assert document.tables[0].cell(0, 0).text == cell.text == "cell held"
assert document.section(0).header.paragraphs[0].text == header.text == "header held"
assert _story_paragraph(document, "footnote").text == note.text == "note held"
assert _story_paragraph(document, "footnote").text == note.text == " note held"
removed = document.remove_document_protection()
assert removed is not None and _protection_fields(removed) == _protection_fields(full)
with pytest.raises(AttributeError):
Expand Down Expand Up @@ -15251,3 +15252,68 @@ def test_fx268_picture_normalization_reuses_actual_checked_rounds(publisher, par
assert staging[5] <= 3 * pieces + 20, staging
assert picture.bytes == _one_pixel_png()
assert len(rdocx.Document.from_bytes(document.to_bytes()).pictures()) == 1


def test_issue432_python_links_take_the_hyperlink_character_style():
import rdocx

def link_runs(document):
xml = document.part_xml("word/document.xml")
return re.findall(r"<w:hyperlink\b.*?</w:hyperlink>", xml, re.S)

styled = '<w:rStyle w:val="Hyperlink"/>'
document = rdocx.Document()
document.add_paragraph("See ").add_hyperlink("the site", "https://example.com")
document.add_heading("Target", 1)
document.add_paragraph("And ")
document.add_heading_hyperlink(document.paragraphs[2], document.paragraphs[1], "Target")
links = link_runs(document)
assert len(links) == 2 and all(styled in link for link in links), links
styles = document.part_xml("word/styles.xml")
assert styles.count('w:styleId="Hyperlink"') == 1, styles

template = rdocx.Document()
template.add_style("Hyperlink", style_type="character", color="FF0000", underline=True)
for document in (
rdocx.Document.from_markdown("See [the site](https://example.com).", template=template)[0],
rdocx.Document.from_html('<p>See <a href="https://example.com">the site</a>.</p>')[0],
):
links = link_runs(document)
assert links and all(styled in link for link in links), links
# The run of a plain add_run is left alone.
document = rdocx.Document()
document.add_paragraph("plain").add_run(" text")
assert "Hyperlink" not in document.part_xml("word/document.xml")


def test_issue432_a_cell_with_only_paragraph_properties_merges_as_empty():
import rdocx

def build():
document = rdocx.Document()
table = document.add_table(1, 3, style="Table Grid")
table.cell(0, 0).text = "Total"
table.cell(0, 1).paragraphs[0].alignment = rdocx.WD_ALIGN_PARAGRAPH.RIGHT
table.cell(0, 2).text = "Kept"
return document

document = build()
document.tables[0].set_cell_grid_span(0, 0, 2)
assert [p.text for p in document.tables[0].cell(0, 0).paragraphs] == ["Total"]
assert [c.text for c in document.tables[0].rows[0].cells] == ["Total", "Kept"]

document = build()
merged = document.tables[0].cell(0, 0).merge(document.tables[0].cell(0, 1))
assert [p.text for p in merged.paragraphs] == ["Total"]

document = rdocx.Document()
table = document.add_table(2, 1)
table.cell(0, 0).text = "Total"
table.cell(1, 0).paragraphs[0].alignment = rdocx.WD_ALIGN_PARAGRAPH.RIGHT
merged = document.tables[0].cell(0, 0).merge(document.tables[0].cell(1, 0))
assert [p.text for p in merged.paragraphs] == ["Total"]

# A cell that holds text is still refused.
document = build()
with pytest.raises(Exception, match="nonempty cell"):
document.tables[0].set_cell_grid_span(0, 1, 2)
Loading
Loading