docx/rtl-bidi--arabic-paragraph-bidi

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: —

Metadata

Feature iddocx/rtl-bidi--arabic-paragraph-bidi
Formatdocx
Categorytext-formatting
Familydocx/rtl-bidi
Axis valuescase=arabic-paragraph-bidi
Spececma-376-5-part-1 § 17.3.2.30 w:rtl

XPath assertions

ID / partPredicateXPathpython-docxdocxjs
rtl-element-arabic-paragraph-bidi
word/document.xml
exist
w:rtl presence on a run's rPr per the case specification.
//w:r/w:rPr/w:rtl
bidi-element-arabic-paragraph-bidi
word/document.xml
exist
w:bidi presence on a paragraph's pPr per the case specification.
//w:p/w:pPr/w:bidi
text-preserved-arabic-paragraph-bidi
word/document.xml
exist
The run payload must carry the characteristic sample string, including any literal Unicode bidi-control characters.
//w:t[contains(., 'مرحبا')]

Render assertions

No render assertions declared.

Generator source

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("LRI⁦inner⁩ RLI⁧i⁩ FSI⁨i⁩")


_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())

Manifest (expanded)

{
  "$schema": "../manifest.schema.json",
  "id": "docx/rtl-bidi--arabic-paragraph-bidi",
  "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-paragraph-bidi"
  },
  "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-paragraph-bidi"
    }
  },
  "assertions": [
    {
      "id": "rtl-element-arabic-paragraph-bidi",
      "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-paragraph-bidi",
      "part": "word/document.xml",
      "namespaces": {
        "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
      },
      "xpath": "//w:p/w:pPr/w:bidi",
      "must": "exist",
      "description": "w:bidi presence on a paragraph's pPr per the case specification."
    },
    {
      "id": "text-preserved-arabic-paragraph-bidi",
      "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."
    }
  ]
}

Fixture

Download rtl-bidi--arabic-paragraph-bidi.docx (18.6 KB)

Reference preview

No rendered reference is available for this case.

Spec 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.