Skip to content

fix(serializer): render relative hyperlinks with portable separators - #780

Open
eren-YU wants to merge 2 commits into
docling-project:mainfrom
eren-YU:fix/hyperlink-relative-path-separators
Open

eren-YU wants to merge 2 commits into
docling-project:mainfrom
eren-YU:fix/hyperlink-relative-path-separators

Conversation

@eren-YU

@eren-YU eren-YU commented Sep 16, 2026

Copy link
Copy Markdown

Fixes docling-project/docling#4220.

What changed

TextItem.hyperlink is typed AnyUrl | Path. A Path hyperlink read out of a document is a native path, so on Windows str() yields backslash separators and every serializer that stringified it emitted a non-portable destination:

exporter before
Markdown [next page](sub\next.html)
HTML <a href="sub\next.html">
LaTeX \href{sub\textbackslash{}next.html}
DocLang <href uri="sub\next.html"/>

and save_as_json wrote "sub\\next.html", so a document converted on Windows loaded on POSIX as a single path component containing a backslash.

A shared hyperlink_uri() helper now renders a Path destination with as_posix(), and the four serializers that write a hyperlink destination use it.

Why not the existing image-path encoder

MarkdownPictureSerializer._escape_uri_path() is the obvious-looking reuse, but it percent-encodes URL delimiters because it assumes a filesystem path: sub/next.html#section becomes sub/next.html%23section and sub/next.html?x=1#section becomes sub/next.html%3Fx=1%23section. That changes the link target rather than just its separator, so the fragment/query delimiters would stop working.

as_posix() only swaps the separator and leaves everything else — ?, #, % escapes and spaces — untouched.

How I checked

Exported a document with Path and AnyUrl hyperlinks on Windows:

input before after
Path("sub/next.html") sub\next.html sub/next.html
Path("sub/next.html#section") sub\next.html#section sub/next.html#section
Path("sub/next.html?x=1#section") sub\next.html?x=1#section sub/next.html?x=1#section
Path("a%20b/x.html") a%20b\x.html a%20b/x.html
Path("dir with space/x.html") dir with space\x.html dir with space/x.html
AnyUrl("https://example.com/a/b?q=1#f") unchanged unchanged

11 tests added to tests/test_serialization.py covering the helper, Markdown and HTML export, the fragment/query cases from the issue discussion, and a JSON round trip. All 11 fail before the change and pass after.

uv run pytest tests/test_serialization.py tests/test_plain_text_serialization.py -q

119 passed. No new failures elsewhere in the suite. ruff check and ruff format --check are clean.

@github-actions

Copy link
Copy Markdown
Contributor

DCO Check Passed

Thanks @eren-YU, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 1 of 2 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require two reviewer for test updates 👀 reviews
🟢 Enforce conventional commit

🔴 Require two reviewer for test updates

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.

When test data is updated, we require two reviewers

  • #approved-reviews-by >= 2

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

Signed-off-by: alan <erenyu@163.com>

# Conflicts:
#	tests/test_serialization.py
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.

Relative hyperlinks are exported with backslashes on Windows (Markdown, HTML, LaTeX, DocLang, JSON)

1 participant