Skip to content

Commit b9930c9

Browse files
committed
feat: add bidirectional text (RTL) documentation page
Cover w:bidi paragraph layout, w:rtl run direction, logical alignment (start/end), indentation flipping, tab stop measurement from the leading edge, and mixed bidi documents. Includes live preview examples and implementation notes from building RTL support in SuperDoc.
1 parent f2f33b1 commit b9930c9

File tree

3 files changed

+249
-2
lines changed

3 files changed

+249
-2
lines changed

apps/web/src/components/Sidebar.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const NAV_SECTIONS = [
1515
to: "/docs/paragraph-borders",
1616
label: "Paragraph Borders",
1717
path: "paragraph-borders",
18+
},
19+
{
20+
to: "/docs/bidirectional-text",
21+
label: "Bidirectional Text (RTL)",
22+
path: "bidirectional-text",
1823
isNew: true,
1924
},
2025
{ to: "/docs/tables", label: "Tables", path: "tables" },

apps/web/src/data/docs.ts

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,248 @@ Each border element has:
452452
],
453453
},
454454

455+
"bidirectional-text": {
456+
title: "Bidirectional Text (RTL)",
457+
description:
458+
"Right-to-left paragraph layout, run-level text direction, and how bidi interacts with alignment, indentation, and tab stops.",
459+
badge: "w:bidi",
460+
content: [
461+
{
462+
type: "paragraph",
463+
text: "Bidirectional (bidi) support in OOXML handles right-to-left scripts like Arabic and Hebrew. It operates at two levels: `w:bidi` sets the paragraph's base direction, and `w:rtl` controls individual run reading order. Getting these right is the difference between text that renders correctly and text that's backwards.",
464+
},
465+
{ type: "heading", level: 2, text: "Structure" },
466+
{
467+
type: "code",
468+
code: `w:pPr (paragraph properties)
469+
├── w:bidi Paragraph base direction (RTL layout)
470+
├── w:jc Alignment — start/end are logical, flip with bidi
471+
├── w:ind Indentation — start/end flip with bidi
472+
└── w:tabs
473+
└── w:tab Tab stops — measured from leading edge (right for RTL)
474+
475+
w:rPr (run properties)
476+
├── w:rtl Run reading order (right-to-left)
477+
├── w:cs Treat run as complex script
478+
├── w:rFonts @w:cs — complex script font
479+
└── w:lang @w:bidi — bidi language (ar-SA, he-IL, etc.)`,
480+
},
481+
{ type: "heading", level: 2, text: "Paragraph Direction — w:bidi" },
482+
{
483+
type: "paragraph",
484+
text: "The `w:bidi` element on `w:pPr` sets the paragraph's base direction to right-to-left. This flips four things: indentation (start/end swap sides), alignment (start/end resolve to opposite edges), tab stop measurement (from right edge instead of left), and text flow direction. It does NOT reorder characters within runs — that's `w:rtl`'s job.",
485+
},
486+
{
487+
type: "preview",
488+
title: "RTL paragraph with Arabic text",
489+
xml: `<w:p>
490+
<w:pPr>
491+
<w:bidi/>
492+
</w:pPr>
493+
<w:r>
494+
<w:rPr><w:rtl/></w:rPr>
495+
<w:t>مرحبا بالعالم</w:t>
496+
</w:r>
497+
<w:r>
498+
<w:t xml:space="preserve"> - Hello World</w:t>
499+
</w:r>
500+
</w:p>`,
501+
},
502+
{ type: "heading", level: 2, text: "Alignment with Bidi" },
503+
{
504+
type: "paragraph",
505+
text: 'The `w:jc` element uses logical values `start` and `end` that flip based on paragraph direction. `start` means the leading edge: left for LTR, right for RTL. `end` means the trailing edge. The values `left`, `right`, and `center` are always physical and don\'t flip. Arabic justify variants (`lowKashida`, `mediumKashida`, `highKashida`) extend joiners between characters instead of adding word spacing.',
506+
},
507+
{
508+
type: "preview",
509+
title: "RTL paragraph with center alignment",
510+
xml: `<w:p>
511+
<w:pPr>
512+
<w:bidi/>
513+
<w:jc w:val="center"/>
514+
</w:pPr>
515+
<w:r>
516+
<w:rPr><w:rtl/></w:rPr>
517+
<w:t>نص عربي في الوسط</w:t>
518+
</w:r>
519+
</w:p>`,
520+
},
521+
{
522+
type: "preview",
523+
title: "RTL paragraph — start alignment resolves to right",
524+
xml: `<w:p>
525+
<w:pPr>
526+
<w:bidi/>
527+
<w:jc w:val="start"/>
528+
</w:pPr>
529+
<w:r>
530+
<w:rPr><w:rtl/></w:rPr>
531+
<w:t>محاذاة البداية — تعني اليمين في الفقرات العربية</w:t>
532+
</w:r>
533+
</w:p>`,
534+
},
535+
{
536+
type: "table",
537+
headers: ["w:jc value", "LTR result", "RTL result"],
538+
rows: [
539+
["`start`", "Left", "Right"],
540+
["`end`", "Right", "Left"],
541+
["`center`", "Center", "Center"],
542+
["`both`", "Justify (word spacing)", "Justify (word spacing + lowKashida)"],
543+
["`left`", "Left", "Left (physical, doesn't flip)"],
544+
["`right`", "Right", "Right (physical, doesn't flip)"],
545+
["`lowKashida`", "Justify", "Justify (short kashida extension)"],
546+
["`mediumKashida`", "Justify", "Justify (medium kashida)"],
547+
["`highKashida`", "Justify", "Justify (widest kashida)"],
548+
["`distribute`", "Justify (char + word spacing)", "Justify (char + word spacing)"],
549+
],
550+
},
551+
{ type: "heading", level: 2, text: "Indentation with Bidi" },
552+
{
553+
type: "paragraph",
554+
text: "The `w:ind` element uses `start`/`end` attributes that are logical — they refer to the leading and trailing edges of the paragraph. For an RTL paragraph, `start` is the right margin and `end` is the left margin. The `firstLine` and `hanging` attributes also apply relative to the start edge.",
555+
},
556+
{
557+
type: "preview",
558+
title: "RTL paragraph with start indent (appears on right side)",
559+
xml: `<w:p>
560+
<w:pPr>
561+
<w:bidi/>
562+
<w:ind w:start="720"/>
563+
</w:pPr>
564+
<w:r>
565+
<w:rPr><w:rtl/></w:rPr>
566+
<w:t>فقرة مع مسافة بادئة من اليمين</w:t>
567+
</w:r>
568+
</w:p>`,
569+
},
570+
{ type: "heading", level: 2, text: "Tab Stops with Bidi" },
571+
{
572+
type: "paragraph",
573+
text: "Tab stop positions in RTL paragraphs are measured from the right edge of the text area, not the left. The `w:tab` alignment values `start` and `end` also flip. A `start`-aligned tab in an RTL paragraph aligns text to the right of the tab position.",
574+
},
575+
{
576+
type: "preview",
577+
title: "RTL paragraph with dot leader tab stop",
578+
xml: `<w:p>
579+
<w:pPr>
580+
<w:bidi/>
581+
<w:tabs>
582+
<w:tab w:val="left" w:pos="8640" w:leader="dot"/>
583+
</w:tabs>
584+
</w:pPr>
585+
<w:r>
586+
<w:rPr><w:rtl/></w:rPr>
587+
<w:t>عنوان الفصل</w:t>
588+
</w:r>
589+
<w:r><w:tab/></w:r>
590+
<w:r>
591+
<w:rPr><w:rtl/></w:rPr>
592+
<w:t>٤٢</w:t>
593+
</w:r>
594+
</w:p>`,
595+
},
596+
{ type: "heading", level: 2, text: "Run-Level Direction — w:rtl" },
597+
{
598+
type: "paragraph",
599+
text: "The `w:rtl` element on `w:rPr` sets the reading order of a single run to right-to-left. This is separate from `w:bidi` — you can have an LTR paragraph with RTL runs (inline Arabic in English text) or an RTL paragraph with LTR runs (English words in Arabic text). The spec warns: don't use `w:rtl` on strong LTR characters — behavior is undefined.",
600+
},
601+
{
602+
type: "preview",
603+
title: "LTR paragraph with inline Arabic run",
604+
xml: `<w:p>
605+
<w:r>
606+
<w:t xml:space="preserve">English text then </w:t>
607+
</w:r>
608+
<w:r>
609+
<w:rPr><w:rtl/></w:rPr>
610+
<w:t>نص عربي</w:t>
611+
</w:r>
612+
<w:r>
613+
<w:t xml:space="preserve"> then English again</w:t>
614+
</w:r>
615+
</w:p>`,
616+
},
617+
{ type: "heading", level: 2, text: "Mixed Document — RTL and LTR Paragraphs" },
618+
{
619+
type: "preview",
620+
title: "Document with alternating paragraph directions",
621+
xml: `<w:p>
622+
<w:pPr><w:bidi/></w:pPr>
623+
<w:r>
624+
<w:rPr><w:rtl/></w:rPr>
625+
<w:t>هذه فقرة كاملة باللغة العربية</w:t>
626+
</w:r>
627+
</w:p>
628+
<w:p>
629+
<w:r>
630+
<w:t>This is a complete English paragraph</w:t>
631+
</w:r>
632+
</w:p>
633+
<w:p>
634+
<w:pPr><w:bidi/></w:pPr>
635+
<w:r>
636+
<w:rPr><w:rtl/></w:rPr>
637+
<w:t>فقرة عربية أخرى بعد الإنجليزية</w:t>
638+
</w:r>
639+
</w:p>`,
640+
},
641+
{ type: "heading", level: 2, text: "Implementation Notes" },
642+
{
643+
type: "note",
644+
noteType: "critical",
645+
title: "w:bidi and w:rtl are independent",
646+
text: "`w:bidi` flips paragraph layout (indentation, alignment, tabs, text direction). `w:rtl` on `w:rPr` controls run reading order. You need both — without `w:rtl` on the runs, text lands in the right position but characters read backwards.",
647+
},
648+
{
649+
type: "note",
650+
noteType: "critical",
651+
title: "Tab positions are from the right edge in RTL",
652+
text: "Tab `pos` is measured from the leading edge (§17.3.1.37). For RTL, that's the right margin. If your layout engine always measures from the left, every RTL tab stop lands on the wrong side.",
653+
},
654+
{
655+
type: "note",
656+
noteType: "warning",
657+
title: "start/end are logical, left/right are physical",
658+
text: "`jc=\"start\"` flips with direction; `jc=\"left\"` does not. Don't resolve `start` → `left` during import — you'll lose the logical intent. Same applies to `w:ind start`/`end` attributes.",
659+
app: "Word",
660+
},
661+
{
662+
type: "note",
663+
noteType: "info",
664+
title: "Section bidi is separate from paragraph bidi",
665+
text: "`w:sectPr > w:bidi` controls page chrome (page numbers, column order). It doesn't affect text. You need paragraph-level `w:bidi` for text layout.",
666+
},
667+
{ type: "heading", level: 2, text: "Schema" },
668+
{
669+
type: "table",
670+
headers: ["Element", "Parent", "Description"],
671+
rows: [
672+
["`w:bidi`", "`w:pPr`", "Paragraph base direction — sets RTL layout for indentation, alignment, tabs"],
673+
["`w:rtl`", "`w:rPr`", "Run reading order — right-to-left character ordering"],
674+
["`w:cs`", "`w:rPr`", "Complex script flag — forces complex script font/size"],
675+
["`w:bidi`", "`w:sectPr`", "Section layout direction — page-level RTL (separate from paragraph bidi)"],
676+
],
677+
},
678+
{
679+
type: "table",
680+
headers: ["Attribute / Value", "Context", "Description"],
681+
rows: [
682+
["`w:jc val=\"start\"`", "`w:pPr`", "Leading edge alignment — left for LTR, right for RTL"],
683+
["`w:jc val=\"end\"`", "`w:pPr`", "Trailing edge alignment — right for LTR, left for RTL"],
684+
["`w:ind start`", "`w:pPr`", "Leading edge indent — right side for RTL"],
685+
["`w:ind end`", "`w:pPr`", "Trailing edge indent — left side for RTL"],
686+
["`w:tab pos`", "`w:pPr > w:tabs`", "Tab position from leading edge — from right for RTL"],
687+
["`w:lang bidi`", "`w:rPr`", "Bidi language tag (ar-SA, he-IL) — affects neutral char resolution"],
688+
],
689+
},
690+
{
691+
type: "paragraph",
692+
text: "Spec reference: ECMA-376 [§17.3.1.6 (bidi)](/spec?section=17.3.1.6&part=1), [§17.3.2.30 (rtl)](/spec?section=17.3.2.30&part=1), [§17.3.1.13 (jc)](/spec?section=17.3.1.13&part=1), [§17.18.44 (ST_Jc)](/spec?section=17.18.44&part=1), [§17.3.1.37 (tab)](/spec?section=17.3.1.37&part=1), [§17.3.1.12 (ind)](/spec?section=17.3.1.12&part=1), [§I.2 (Bidi annex)](/spec?section=I.2&part=1)",
693+
},
694+
],
695+
},
696+
455697
"creating-documents": {
456698
title: "Creating Documents",
457699
description: "Step-by-step guide to creating a valid OOXML document from scratch.",

apps/web/src/pages/Home.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ export function Home() {
3535
NEW
3636
</span>
3737
<span className="text-[var(--color-text-secondary)]">
38-
Paragraph Borders - between-border groups, nil/none semantics, and rendering gotchas
38+
Bidirectional Text — RTL layout, logical alignment, tab stop flipping, and bidi pitfalls
3939
</span>
4040
<Link
41-
to="/docs/paragraph-borders"
41+
to="/docs/bidirectional-text"
4242
className="text-[var(--color-accent)] hover:text-[var(--color-accent-hover)] font-medium text-xs"
4343
>
4444
Read it →

0 commit comments

Comments
 (0)