Right-to-left and bidirectional text support: run-level <w:rtl/>, paragraph-level <w:bidi/>, and explicit Unicode bidi-control characters (U+202A..U+202E and U+2066..U+2069).
Library verdicts: python-docx: — docxjs: —
| Feature id | docx/rtl-bidi--arabic-run-rtl |
|---|---|
| Format | docx |
| Category | text-formatting |
| Family | docx/rtl-bidi |
| Axis values | case=arabic-run-rtl |
| Spec | ecma-376-5-part-1 § 17.3.2.30 w:rtl |
| ID / part | Predicate | XPath | python-docx | docxjs |
|---|---|---|---|---|
rtl-element-arabic-run-rtlword/document.xml | existw:rtl presence on a run's rPr per the case specification. | //w:r/w:rPr/w:rtl | — | — |
bidi-element-arabic-run-rtlword/document.xml | absentw:bidi presence on a paragraph's pPr per the case specification. | //w:p/w:pPr/w:bidi | — | — |
text-preserved-arabic-run-rtlword/document.xml | existThe run payload must carry the characteristic sample string, including any literal Unicode bidi-control characters. | //w:t[contains(., 'مرحبا')] | — | — |
No render assertions declared.
scripts/gen_rtl_bidi.py
#!/usr/bin/env python3
"""Generate ``fixtures/docx/rtl-bidi--<case>.docx``.
Parameterised generator for the ``docx/rtl-bidi`` family. Each case
exercises a different combination of run-level ``<w:rtl/>``,
paragraph-level ``<w:bidi/>``, and literal Unicode bidi-control
characters in the run text payload.
Case index:
- ``arabic-run-rtl`` — single Arabic run, ``font.rtl = True`` only.
- ``arabic-paragraph-bidi`` — paragraph marked ``right_to_left = True``
plus the run is ``font.rtl = True``. Word's typical RTL paragraph.
- ``mixed-ltr-rtl`` — English run (no flags) followed by
Arabic run with ``font.rtl = True`` in a single LTR paragraph.
- ``bidi-control-lre`` — Latin text bracketed by LRE (U+202A) / PDF
(U+202C). No OOXML flags; the bidi controls are literal text.
- ``bidi-control-rle`` — RLE (U+202B) / PDF.
- ``bidi-control-lro`` — LRO (U+202D) / PDF.
- ``bidi-control-rlo`` — RLO (U+202E) / PDF.
- ``bidi-control-isolates`` — LRI (U+2066) / RLI (U+2067) / FSI (U+2068)
/ PDI (U+2069). The Unicode 6.3+ bidi-isolate family.
Usage::
python scripts/gen_rtl_bidi.py --case arabic-run-rtl \\
--out fixtures/docx/rtl-bidi--arabic-run-rtl.docx
"""
from __future__ import annotations
import argparse
from pathlib import Path
from typing import Callable
from docx import Document
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _case_arabic_run_rtl(doc):
p = doc.add_paragraph()
run = p.add_run("مرحبا")
run.font.rtl = True
def _case_arabic_paragraph_bidi(doc):
p = doc.add_paragraph()
p.paragraph_format.right_to_left = True
run = p.add_run("مرحبا")
run.font.rtl = True
def _case_mixed_ltr_rtl(doc):
p = doc.add_paragraph()
p.add_run("Hello ")
arabic = p.add_run("مرحبا")
arabic.font.rtl = True
p.add_run(" world")
def _case_bidi_control(controls: tuple[str, str, str]) -> Callable:
prefix, open_c, close_c = controls
def _inner(doc):
p = doc.add_paragraph()
p.add_run(f"{prefix}{open_c}inner{close_c}")
return _inner
def _case_bidi_isolates(doc):
# U+2066 LRI / U+2067 RLI / U+2068 FSI / U+2069 PDI
p = doc.add_paragraph()
p.add_run("LRIinner RLIi FSIi")
_CASES: dict[str, Callable] = {
"arabic-run-rtl": _case_arabic_run_rtl,
"arabic-paragraph-bidi": _case_arabic_paragraph_bidi,
"mixed-ltr-rtl": _case_mixed_ltr_rtl,
# U+202A LRE, U+202B RLE, U+202D LRO, U+202E RLO, U+202C PDF
"bidi-control-lre": _case_bidi_control(("LRE", "", "")),
"bidi-control-rle": _case_bidi_control(("RLE", "", "")),
"bidi-control-lro": _case_bidi_control(("LRO", "", "")),
"bidi-control-rlo": _case_bidi_control(("RLO", "", "")),
"bidi-control-isolates": _case_bidi_isolates,
}
def _write(case: str, out: Path) -> None:
if case not in _CASES:
raise SystemExit(f"Unknown case id {case!r}; expected one of {sorted(_CASES)}")
doc = Document()
_CASES[case](doc)
out.parent.mkdir(parents=True, exist_ok=True)
doc.save(out, reproducible=True)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--case", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args()
out_path = Path(args.out)
if not out_path.is_absolute():
out_path = _REPO_ROOT / out_path
_write(args.case, out_path)
print(out_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())
{
"$schema": "../manifest.schema.json",
"id": "docx/rtl-bidi--arabic-run-rtl",
"kind": "literal",
"title": "RTL / bidirectional text",
"format": "docx",
"category": "text-formatting",
"summary": "Right-to-left and bidirectional text support: run-level <w:rtl/>, paragraph-level <w:bidi/>, and explicit Unicode bidi-control characters (U+202A..U+202E and U+2066..U+2069).",
"spec": {
"source": "ecma-376-5-part-1",
"clause": "17.3.2.30",
"element": "w:rtl",
"rnc_reference": "spec/ecma-376-5/part-1/rnc/WordprocessingML.rnc",
"xsd_reference": "spec/ecma-376-5/part-1/xsd/wml.xsd",
"notes": "Word distinguishes two RTL markers. <w:rPr/w:rtl> flags a run as complex-script (Arabic / Hebrew / Farsi) and selects the w:rFonts/@w:cs typeface + w:szCs font size. <w:pPr/w:bidi> flags the paragraph itself as right-to-left, reversing visual alignment of an LTR script as well. The two are independent: a single Hebrew run in an otherwise LTR paragraph needs <w:rtl/> but not <w:bidi/>; a fully Arabic paragraph needs both. This family covers: (a) pure Arabic run with w:rtl, (b) pure Arabic paragraph + run with both bidi and rtl, (c) mixed LTR + RTL paragraph (English then Arabic), (d) runs with explicit Unicode bidi-control characters (U+202A/LRE, U+202B/RLE, U+202C/PDF, U+202D/LRO, U+202E/RLO, U+2066/LRI, U+2067/RLI, U+2068/FSI, U+2069/PDI). The bidi controls are literal characters that survive to the text payload; they do not trigger any OOXML element emission on their own but MUST round-trip byte-exact."
},
"fixtures": {
"machine": "docx/rtl-bidi--arabic-run-rtl"
},
"generator": {
"python": "scripts/gen_rtl_bidi.py",
"arg_template": "--case {case.id} --out fixtures/docx/rtl-bidi--{case.id}.docx"
},
"_expansion": {
"parent_id": "docx/rtl-bidi",
"bindings": {
"case": "arabic-run-rtl"
}
},
"assertions": [
{
"id": "rtl-element-arabic-run-rtl",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:r/w:rPr/w:rtl",
"must": "exist",
"description": "w:rtl presence on a run's rPr per the case specification."
},
{
"id": "bidi-element-arabic-run-rtl",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:p/w:pPr/w:bidi",
"must": "absent",
"description": "w:bidi presence on a paragraph's pPr per the case specification."
},
{
"id": "text-preserved-arabic-run-rtl",
"part": "word/document.xml",
"namespaces": {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
},
"xpath": "//w:t[contains(., 'مرحبا')]",
"must": "exist",
"description": "The run payload must carry the characteristic sample string, including any literal Unicode bidi-control characters."
}
]
}
No rendered reference is available for this case.