Skip to content

HTML import: <a href> links become hyperlinks (#1879) - #1911

Merged
yeroo merged 4 commits into
mainfrom
issue-1879-html-import-a-href-links-become
Nov 5, 2026
Merged

yeroo merged 4 commits into
mainfrom
issue-1879-html-import-a-href-links-become

Conversation

@yeroo

@yeroo yeroo commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

What and why

HTML import (docxcore::import::html) used to keep an <a>'s text and drop its target. Now an
<a href> becomes an Inline::Hyperlink:

  • An absolute http, https, ftp or mailto URL becomes the link's target. href="#name"
    becomes an anchor link (w:anchor, percent-decoded); # alone makes no link.
  • Every other href stays plain text, as before. This covers javascript:, vbscript:, data:,
    file:, any other scheme, relative and empty hrefs.
    • link_of reads an href the way a browser's URL parser does. It trims controls at both ends
      and removes tab, LF and CR inside, so java&#9;script: is caught. Then it checks an
      allow-list, so obfuscations fail closed.
    • Space, ", <, > and ` are percent-encoded.
    • The limit is 4096 bytes. An href longer than twice that is refused unread.
  • How the link is carried. The link is part of each element's computed look. So the cascade,
    restyle, mis-nesting and the formatting-element reopen carry it exactly as they carry
    formatting. A link that crosses <p> boundaries becomes a link in each paragraph. An <a>
    closes an open <a> first, as a browser's adoption agency does.
  • Look. Links get a browser's look (underline and Word's Hyperlink blue 0563C1) as direct
    formatting, and the page's CSS overrides it. color: inherit / currentcolor now gives back
    the parent's colour, over a link's blue or a <font color>. That makes Gmail's
    a{color:inherit;text-decoration:none} work.
  • Breaks, tabs and pictures. A break, tab or picture inside a link is written outside it, and
    the link goes on after it. An unresolved image's alt text inside a link is the link's text.
  • Relationships. None are made at import. save_package already mints an external
    relationship for every target-only link. A test shows the targets survive HtmlImport::package
    → save → load. The suite's converting child saves the same way, so an .htm opened in the suite
    shows its links.
  • Attribute values. A named character reference without ; that is followed by = or an
    alphanumeric is kept as written, as HTML does. So ?id=3&copy=2 stays a correct target
    (review r1).

Notes are in docs/claude/html-import.md under "HTML import links (#1879)", with the known
limits.

Closes #1879

Tests

  • html.rs link_tests (16):
    • formatting and the link look;
    • CSS overrides, including inherit;
    • anchors;
    • 22 unsafe or relative hrefs, including the obfuscations;
    • limits and encoding;
    • spaces inside and around links;
    • paragraphs, breaks and tabs;
    • pictures and alt text;
    • an empty link, and list markers;
    • nested <a>;
    • table cells;
    • &name= in attributes.
  • docxcore/tests/html_links.rs over a new newsletter-style fixture links.html: links in
    order, and package save/load keeping targets (TargetMode="External") and w:anchor.
  • html_css.rs: the Gmail fixture's mailto: link, and links.html in the every-prefix
    loop.
  • Suite: convert_tests::a_web_pages_links_open_as_hyperlinks.
  • Whole run on ae7e6b24:
    • It passed: cargo fmt, the workspace cargo clippy --all-targets,
      cargo test --workspace --locked, suite fmt / clippy / xvfb-run cargo test, the scripts'
      unittests and check-claude-md. 11954 tests passed, 0 failed.
    • Not run on this machine (left to CI): the html-export clippy/test configuration (no
      wasm32-unknown-unknown target here) and the UI sweep. In an earlier run of the sweep on
      2cc88c4, 139 of 144 passed and 1 was an expected failure. Of the 4 failures:
      • 3 hit the 218-character workbook path limit, because of this checkout's long sandbox path;
      • 1 was project-columns, in Project table code this PR does not touch.

Review record

yeroo and others added 4 commits November 5, 2026 16:10
An <a> with an http, https, ftp or mailto href is now an Inline::Hyperlink
to that URL, and href="#name" an anchor link; every other href (javascript:,
data:, file:, relative, ...) stays plain text, as before. The href is read
as a browser's URL parser reads it (controls and spaces trimmed, tabs and
line breaks inside removed), against an allow-list, so obfuscated schemes
fail closed.

The link lives in the element's computed look, so the cascade, restyle and
the formatting-element reopen carry it with no stack of its own; an <a>
closes an open <a> first, as the adoption agency does. A link gets a
browser's look (underline, Word's Hyperlink blue) under the page's CSS,
and color: inherit now gives the parent's colour back over it. Breaks,
tabs and pictures are written outside the link; an unresolved image's alt
text is the link's text. A link counts by its text for the empty-paragraph
rule, and is never cut as a typed list marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lto (#1879)

A links.html fixture with the shapes real pages use (styled text in a
link, mailto, an anchor link, a javascript: button, a relative link, a
link in a cell and one around a <br>) checks the links found and that each
URL comes back from a saved package as an external relationship, the
anchor as w:anchor. The Gmail quote header's address is now a mailto
link, and links.html joins the every-prefix loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening a plain .htm page with links in the suite gives a tab whose
document holds them as hyperlinks (the javascript: one as text). The area
notes describe how links are read, carried, styled and saved, with the
known limits (text-decoration propagation, bookmarks, the Hyperlink
style, relative links).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r1)

r1-m2: HTML leaves a named character reference that has no ';' and is
followed by '=' or an ASCII letter or digit as written inside an
attribute value, so href="?id=3&copy=2" must keep its '&copy=2' rather
than become a copyright sign. attrs_of now decodes in that attribute mode;
text keeps today's decoding.

r1-m1: Color::Keep gives the parent's colour back over one the element
set itself (a link's user-agent blue, <font color>); its doc said the
colour was already there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 98.09725% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docxcore/src/import/html.rs 98.09% 8 Missing ⚠️
docxcore/src/import/mod.rs 98.07% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@yeroo
yeroo merged commit 1581905 into main Nov 5, 2026
14 checks passed
@yeroo
yeroo deleted the issue-1879-html-import-a-href-links-become branch November 5, 2026 13:36
@yeroo

yeroo commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Merged automatically (auto-merge is on for this checkout): review stopped: round 1 had no Major; 1 minor finding(s) in #1912; merge rounds: none; whole suite green on ae7e6b2 (11954 tests; html-export and the UI sweep ran in CI, 14 checks passed); merge-check ok.

Follow-ups, all in the leftovers issue #1912 (none are duplicates):

  • Markdown export: escape hyperlink destinations (spaces, parentheses). This is the review r1 minor, deferred because it predates this PR and lives in markdown.rs, which the PR does not touch.
  • Four out-of-scope items from the plan:
    • bookmarks for <a name> / id anchor targets;
    • the Hyperlink character style on imported links;
    • resolving relative links against <base href>;
    • link title / target and hyperlinked pictures.

No finding was disputed. Both other r1 minors were fixed in ae7e6b2.

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.

HTML import: <a href> links become hyperlinks

2 participants