Skip to content

feat(style): render a right-to-left document right-to-left - #819

Merged
andiwand merged 3 commits into
mainfrom
feat/text-direction
Sep 5, 2026
Merged

feat(style): render a right-to-left document right-to-left#819
andiwand merged 3 commits into
mainfrom
feat/text-direction

Conversation

@andiwand

@andiwand andiwand commented Sep 5, 2026

Copy link
Copy Markdown
Member

Implements OpenDocument.droid#653. All three parts, in one PR — they share one reference-output regen, and style.hpp / document_style.cpp carry hunks from all three, so splitting would be artificial.

The view has a base direction

HtmlWriter::write_begin took no options and wrote a bare <html>. It now takes HtmlElementOptions, and front() puts the document's direction on it. WritingState carries that direction so a paragraph writes direction: only where it differs — without that, every docx paragraph gets a direction:ltr (Word writes <w:bidi w:val="0"/> in docDefaults).

style:writing-mode is parsed

New TextDirection on ParagraphStyle and PageLayout, read from:

paragraph page
odf style:writing-mode style:page-layout-properties/@style:writing-mode
docx w:bidi w:sectPr/w:bidi
pptx a:pPr/@rtl

The vertical modes (tb-*) and page name no horizontal direction, so they stay unset. In odf this rides the existing style cascade, so a document that states the direction on its default paragraph style — which is what LibreOffice writes — inherits it everywhere for free.

start/end alignment — and a correction to the issue

The issue says both odf and ooxml resolve these to the wrong edge. That holds for ooxml and is fixed here. It does not hold for odf, and I left odf alone.

TextAlign::start/end carry the css meaning; a reader whose format means something else by the names maps into the absolute values. w:jc passes them through, fo:text-align does not — LibreOffice inverts the two on a round-trip, so a w:jc="start" paragraph it flushes right comes back out as fo:text-align="end":

docx in                   LibreOffice renders    exported to odf as
w:bidi=1 w:jc=start   ->  right              ->  fo:text-align="end"
w:bidi=1 w:jc=end     ->  left               ->  fo:text-align="start"

So odf's start is a plain left — which is why writers emit it for ordinary body text — and mapping it to css start would have flipped every right-to-left odf document to the wrong edge. odt/style+charset+svm-various-1.odt in the private data is exactly that document: 224 Hebrew paragraphs at fo:text-align="end" on an rl-tb page, which LibreOffice flushes right. It keeps rendering right, and now also gets <html dir="rtl">, which is what fixes the ordering of its mixed Hebrew/Latin lines.

TextAlign gains start and end for the ooxml side. Appended after justify, so existing values are stable in every binding; only an exhaustive switch needs an arm.

Fixtures

odt/text-direction.odt and docx/text-direction.docx in the public input repo — hand-authored, one attribute per paragraph, short lines so each one's edge is readable. Rendered both with LibreOffice and measured the emitted html in Chrome, paragraph by paragraph:

odt paragraph LibreOffice odrcore
unaligned left right
fo:text-align=start left left
fo:text-align=end right right
center / justify center / right center / right
lr-tb left left
rl-tb left right
rl-tb + start left left
lr-tb + start left left
docx paragraph LibreOffice odrcore
w:bidi, unaligned right right
w:bidi + w:jc=start right right
w:bidi + w:jc=end left left
w:jc=left / right / center left / right / center left / right / center
w:bidi=0 left left
w:bidi=0 + w:jc=start left left

docx matches LibreOffice 8 of 8. odt matches 7 of 9.

The known divergence

The two odt misses are one case: a paragraph that states a right-to-left direction but no alignment. LibreOffice flushes it left — its internal default adjust is left whatever the direction — while <html dir="rtl"> with no text-align resolves to the start edge, so we flush it right.

I left it, because there is no clean fix: matching LibreOffice means emitting an explicit text-align:left for odf but not for docx, which is format knowledge the renderer deliberately does not have. It is also narrow — a real LibreOffice-authored right-to-left document always writes an explicit alignment (the Hebrew document above does), so only a synthetic file like this fixture hits it. The fixture pins it, so if we ever change our mind it shows up as a diff. Worth your call.

lang — not done

Left out deliberately. The reliable source is per-script (fo:language vs style:language-complex, w:lang/@w:val vs @w:bidi), so a single lang needs a script-selection rule that depends on the resolved direction — and direction and text style resolve independently in the cascade. dc:language is not a substitute: 7 of 138 odf test inputs carry it. It wants its own design decision.

Verified

  • 1436 gtest, -Werror, jni (1/1 junit), python (69 pytest), and ODRStyle.mm all clean.
  • Reference output regenerated and pushed, all four pins advanced. 677 files change, no visible text moves. Exactly three substitutions across all of them — <html><html dir="ltr"> (676) / dir="rtl"> (1), and text-align:lefttext-align:start (146, all docx) — plus the four files of the two new fixtures. A fresh run is byte-identical to the pushed references.

🤖 Generated with Claude Code

andiwand added a commit to opendocument-app/OpenDocument.test that referenced this pull request Sep 5, 2026
Two minimal packages for opendocument-app/OpenDocument.core#819, each
paragraph stating one attribute under test.

The odt is a right-to-left page layout (`style:writing-mode="rl-tb"`) with
paragraphs for `fo:text-align` start / end / center and for a `lr-tb`
paragraph inside it. The docx is a right-to-left section (`w:sectPr/w:bidi`)
with `w:jc` start / end / left / center and a `w:bidi="0"` paragraph.

They pin the contrast between the two formats: LibreOffice renders odf
`start` left and `end` right whatever the direction, and `w:jc` start right
and end left in a right-to-left paragraph. Both carry a mixed
hebrew/arabic + latin + digit line, whose ordering the base direction
decides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP23uvwVtgEzZZTJnWsJC2
andiwand added a commit to opendocument-app/OpenDocument.test.output that referenced this pull request Sep 5, 2026
For opendocument-app/OpenDocument.core#819. Every view's root now carries
the document's base direction, so `<html>` becomes `<html dir="ltr">`
throughout; no visible text moves.

Adds the output of the two new text-direction fixtures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP23uvwVtgEzZZTJnWsJC2
andiwand and others added 3 commits September 5, 2026 22:29
`style:writing-mode`, `w:bidi` and `a:pPr@rtl` are read into a new
`TextDirection` on `ParagraphStyle` and `PageLayout`. The view's root
carries the document's as `<html dir>`; a paragraph states its own only
where it differs, which keeps `direction:ltr` off every docx paragraph.

`w:jc`'s `start`/`end` stop resolving to `left`/`right`: they follow the
paragraph's direction, so a right-to-left `start` paragraph rendered
flush left where Word flushes it right. `TextAlign` gains the two values
and they reach the css as `text-align:start`/`end`.

`fo:text-align`'s same-named values stay absolute. [OpenDocument] 20.386
calls them direction-relative, but no producer writes them that way -
LibreOffice inverts the two against `w:jc` on a round-trip, so a
`w:jc="start"` paragraph it flushes right comes back out as
`fo:text-align="end"`.

Towards opendocument-app/OpenDocument.droid#653.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP23uvwVtgEzZZTJnWsJC2
A reader whose format means something else by the names maps into the
absolute values, which is what `fo:text-align` does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP23uvwVtgEzZZTJnWsJC2
Two hand-authored packages, one attribute per paragraph, pinning the
contrast the formats disagree on: odf `start`/`end` are the absolute
sides, `w:jc`'s follow the direction.

Checked against LibreOffice edge by edge. All eight docx paragraphs match
it. Seven of nine odt paragraphs do; the two that do not are the same
case, an `rl-tb` paragraph stating no alignment, which LibreOffice flushes
left and css flushes to the start edge.

Reference output: 677 files gain the root `dir`, 146 docx paragraphs move
from `text-align:left` to `start`, and no visible text moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TP23uvwVtgEzZZTJnWsJC2
@andiwand
andiwand force-pushed the feat/text-direction branch from e737367 to ac66210 Compare September 5, 2026 20:31
@andiwand
andiwand merged commit be8e45b into main Sep 5, 2026
20 of 23 checks passed
@andiwand
andiwand deleted the feat/text-direction branch September 5, 2026 20:33
andiwand added a commit that referenced this pull request Sep 6, 2026
…every field (#825)

#819 added `PageLayout::direction` and left three designated initializers
naming every other field, which `-Wmissing-field-initializers` refuses under
gcc-14 and clang-18. gcc stops at the first, so only the iwork one was visible.

The msvc job fails separately: a raw string inside an `EXPECT_EQ` argument,
which the traditional preprocessor does not read, so the quotes inside it come
out as a stray escape. Built outside the macro instead.

main is red on all three today.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant