diff --git a/.golden/paginatedParagraphLayout/spannedArabicFiller20em.golden b/.golden/paginatedParagraphLayout/spannedArabicFiller20em.golden
--- a/.golden/paginatedParagraphLayout/spannedArabicFiller20em.golden
+++ b/.golden/paginatedParagraphLayout/spannedArabicFiller20em.golden
@@ -180,7 +180,7 @@
             { fragmentUserData = ()
             , fragmentLine = 2
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = -1500, x_size = 3213, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -240,7 +240,7 @@
             { fragmentUserData = ()
             , fragmentLine = 3
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 14300, y_origin = 0, x_size = 4862, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -320,7 +320,7 @@
             { fragmentUserData = ()
             , fragmentLine = 3
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 3898, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -339,7 +339,7 @@
             { fragmentUserData = ()
             , fragmentLine = 4
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 12238, y_origin = -1500, x_size = 2357, y_size = -1500}
             , fragmentPen = (0, -1085)
diff --git a/.golden/paragraphLayout/arabicFiller20em.golden b/.golden/paragraphLayout/arabicFiller20em.golden
--- a/.golden/paragraphLayout/arabicFiller20em.golden
+++ b/.golden/paragraphLayout/arabicFiller20em.golden
@@ -6,7 +6,7 @@
             { fragmentUserData = ()
             , fragmentLine = 1
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 19850, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -184,7 +184,7 @@
             { fragmentUserData = ()
             , fragmentLine = 4
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = -4500, x_size = 14595, y_size = -1500}
             , fragmentPen = (0, -1085)
diff --git a/.golden/paragraphLayout/devanagariAccent.golden b/.golden/paragraphLayout/devanagariAccent.golden
--- a/.golden/paragraphLayout/devanagariAccent.golden
+++ b/.golden/paragraphLayout/devanagariAccent.golden
@@ -1,5 +1,5 @@
 ParagraphLayout
-    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 645, y_size = -1000}
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 645, y_size = -1630}
     , spanLayouts = [
         SpanLayout
         [ Fragment
@@ -8,8 +8,8 @@
             , fragmentAncestorBoxes =
                 [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = SpacedEdge 0}
                 ]
-            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 645, y_size = -1000}
-            , fragmentPen = (0, -500)
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 645, y_size = -1630}
+            , fragmentPen = (0, -1171)
             , fragmentGlyphs =
                 [ (GlyphInfo {codepoint = 529, cluster = 0, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 645, y_advance = 0, x_offset = 0, y_offset = 0})
                 , (GlyphInfo {codepoint = 424, cluster = 0, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 0, y_advance = 0, x_offset = 0, y_offset = 0})
diff --git a/.golden/paragraphLayout/devanagariPrefixedAccent.golden b/.golden/paragraphLayout/devanagariPrefixedAccent.golden
--- a/.golden/paragraphLayout/devanagariPrefixedAccent.golden
+++ b/.golden/paragraphLayout/devanagariPrefixedAccent.golden
@@ -1,5 +1,5 @@
 ParagraphLayout
-    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 0, y_size = -1000}
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 0, y_size = -1630}
     , spanLayouts = [
         SpanLayout
         [ Fragment
@@ -8,8 +8,8 @@
             , fragmentAncestorBoxes =
                 [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = SpacedEdge 0}
                 ]
-            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 0, y_size = -1000}
-            , fragmentPen = (0, -500)
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 0, y_size = -1630}
+            , fragmentPen = (0, -1171)
             , fragmentGlyphs =
                 [ (GlyphInfo {codepoint = 424, cluster = 1, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 0, y_advance = 0, x_offset = 0, y_offset = 0})
                 ]
diff --git a/.golden/paragraphLayout/hardBreaksRTL.golden b/.golden/paragraphLayout/hardBreaksRTL.golden
--- a/.golden/paragraphLayout/hardBreaksRTL.golden
+++ b/.golden/paragraphLayout/hardBreaksRTL.golden
@@ -6,7 +6,7 @@
             { fragmentUserData = ()
             , fragmentLine = 1
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 2808, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -137,7 +137,7 @@
             { fragmentUserData = ()
             , fragmentLine = 9
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = -12000, x_size = 1960, y_size = -1500}
             , fragmentPen = (0, -1085)
diff --git a/.golden/paragraphLayout/spannedArabicFiller20em.golden b/.golden/paragraphLayout/spannedArabicFiller20em.golden
--- a/.golden/paragraphLayout/spannedArabicFiller20em.golden
+++ b/.golden/paragraphLayout/spannedArabicFiller20em.golden
@@ -180,7 +180,7 @@
             { fragmentUserData = ()
             , fragmentLine = 2
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = -1500, x_size = 3213, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -200,7 +200,7 @@
             { fragmentUserData = ()
             , fragmentLine = 3
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 14300, y_origin = -3000, x_size = 4862, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -280,7 +280,7 @@
             { fragmentUserData = ()
             , fragmentLine = 3
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
                 ]
             , fragmentRect = Rect {x_origin = 0, y_origin = -3000, x_size = 3898, y_size = -1500}
             , fragmentPen = (0, -1085)
@@ -299,7 +299,7 @@
             { fragmentUserData = ()
             , fragmentLine = 4
             , fragmentAncestorBoxes =
-                [ AncestorBox {boxUserData = (), boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                [ AncestorBox {boxUserData = (), boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
                 ]
             , fragmentRect = Rect {x_origin = 12238, y_origin = -4500, x_size = 2357, y_size = -1500}
             , fragmentPen = (0, -1085)
diff --git a/.golden/richParagraphLayout/mixedDirectionComplexLTR.golden b/.golden/richParagraphLayout/mixedDirectionComplexLTR.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/mixedDirectionComplexLTR.golden
@@ -0,0 +1,147 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 24557, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text1"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 6732, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 90, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 8, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 10, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 11, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 6, cluster = 12, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 16, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 17, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 6, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 19, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 20, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 21, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 22, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 23, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 24, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                ]
+            , fragmentRect = Rect {x_origin = 6732, y_origin = 0, x_size = 1580, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 25, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 26, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 27, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 28, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 29, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = NoEdge, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = NoEdge}
+                ]
+            , fragmentRect = Rect {x_origin = 8312, y_origin = 0, x_size = 3166, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 293, cluster = 41, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 270, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 389, cluster = 39, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 38, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 37, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 36, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 35, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 34, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 512, cluster = 32, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 839, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 286, cluster = 30, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 224, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                ]
+            , fragmentRect = Rect {x_origin = 11478, y_origin = 0, x_size = 6578, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 43, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 44, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 45, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 46, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 47, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 26, cluster = 48, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 49, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 50, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 51, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 52, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 53, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 54, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 55, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 26, cluster = 56, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 57, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 58, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 59, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 60, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 61, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text3"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 18056, y_origin = 0, x_size = 1580, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 62, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 63, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 64, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 65, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 66, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text3"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 19636, y_origin = 0, x_size = 3577, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 517, cluster = 78, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 839, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 955, cluster = 76, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 455, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 75, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 74, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 73, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 72, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 71, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 971, cluster = 69, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 450, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 394, cluster = 67, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text3"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 23213, y_origin = 0, x_size = 1344, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 80, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 81, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 82, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 83, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/richParagraphLayout/mixedDirectionComplexRTL.golden b/.golden/richParagraphLayout/mixedDirectionComplexRTL.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/mixedDirectionComplexRTL.golden
@@ -0,0 +1,147 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 24557, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text3"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 6501, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 147, cluster = 83, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 82, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 81, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 80, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 517, cluster = 78, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 839, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 955, cluster = 76, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 455, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 75, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 74, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 73, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 72, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 71, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 971, cluster = 69, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 450, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 394, cluster = 67, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 66, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 65, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 64, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 63, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 62, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = SpacedEdge 0, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = SpacedEdge 0}
+                ]
+            , fragmentRect = Rect {x_origin = 6501, y_origin = 0, x_size = 1580, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 61, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 60, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 59, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 58, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 57, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = NoEdge, boxRightEdge = NoEdge, boxStartEdge = NoEdge, boxEndEdge = NoEdge}
+                ]
+            , fragmentRect = Rect {x_origin = 8081, y_origin = 0, x_size = 3418, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 26, cluster = 48, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 49, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 50, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 51, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 52, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 53, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 54, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 55, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 26, cluster = 56, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text2"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                [ AncestorBox {boxUserData = "box1", boxLeftEdge = NoEdge, boxRightEdge = SpacedEdge 0, boxStartEdge = SpacedEdge 0, boxEndEdge = NoEdge}
+                ]
+            , fragmentRect = Rect {x_origin = 11499, y_origin = 0, x_size = 6326, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 47, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 46, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 45, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 44, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 43, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 293, cluster = 41, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 270, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 389, cluster = 39, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 253, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 38, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 37, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 36, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 35, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 34, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 512, cluster = 32, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 839, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 286, cluster = 30, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 224, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 29, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 28, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 27, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 26, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 25, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text1"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 17825, y_origin = 0, x_size = 1580, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 24, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 23, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 22, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 21, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 20, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text1"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 19405, y_origin = 0, x_size = 3808, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 4, cluster = 11, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 6, cluster = 12, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 16, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 17, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 6, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 19, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text1"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 23213, y_origin = 0, x_size = 1344, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 3, cluster = 10, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 147, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 3, cluster = 8, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 236, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 90, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 272, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/richParagraphLayout/mixedDirectionSimpleLTR.golden b/.golden/richParagraphLayout/mixedDirectionSimpleLTR.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/mixedDirectionSimpleLTR.golden
@@ -0,0 +1,50 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 7954, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 3397, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 6, cluster = 0, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 1, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 14, cluster = 2, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 24, cluster = 3, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 363, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 4, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 15, cluster = 5, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 250, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 20, cluster = 6, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 3397, y_origin = 0, x_size = 1578, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 1143, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 1095, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 920, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 483, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 4975, y_origin = 0, x_size = 2979, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 17, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 517, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 29, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 759, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 16, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 15, cluster = 17, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 250, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 26, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/richParagraphLayout/mixedDirectionSimpleRTL.golden b/.golden/richParagraphLayout/mixedDirectionSimpleRTL.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/mixedDirectionSimpleRTL.golden
@@ -0,0 +1,50 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 7954, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 2979, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 17, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 517, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 27, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 29, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 759, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 16, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 15, cluster = 17, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 250, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 26, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 351, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 2979, y_origin = 0, x_size = 1578, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 1143, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 1095, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 920, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 483, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        , Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 4557, y_origin = 0, x_size = 3397, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 6, cluster = 0, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 580, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 1, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 14, cluster = 2, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 24, cluster = 3, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 363, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 4, cluster = 4, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 534, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 15, cluster = 5, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 250, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 20, cluster = 6, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 568, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/richParagraphLayout/neutralDirectionLTR.golden b/.golden/richParagraphLayout/neutralDirectionLTR.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/neutralDirectionLTR.golden
@@ -0,0 +1,18 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 2003, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 2003, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 91, cluster = 3, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 803, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 1392, cluster = 6, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 148, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/richParagraphLayout/neutralDirectionRTL.golden b/.golden/richParagraphLayout/neutralDirectionRTL.golden
new file mode 100644
--- /dev/null
+++ b/.golden/richParagraphLayout/neutralDirectionRTL.golden
@@ -0,0 +1,18 @@
+ParagraphLayout
+    { paragraphRect = Rect {x_origin = 0, y_origin = 0, x_size = 2003, y_size = -1500}
+    , paragraphFragments =
+        [ Fragment
+            { fragmentUserData = "text"
+            , fragmentLine = 1
+            , fragmentAncestorBoxes =
+                []
+            , fragmentRect = Rect {x_origin = 0, y_origin = 0, x_size = 2003, y_size = -1500}
+            , fragmentPen = (0, -1085)
+            , fragmentGlyphs =
+                [ (GlyphInfo {codepoint = 147, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 1392, cluster = 6, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 600, y_advance = 0, x_offset = 0, y_offset = 0})
+                , (GlyphInfo {codepoint = 91, cluster = 3, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 803, y_advance = 0, x_offset = 0, y_offset = 0})
+                ]
+            }
+        ]
+    }
diff --git a/.golden/shapedRuns/mixedDirectionComplexRTL.fontInfo b/.golden/shapedRuns/mixedDirectionComplexRTL.fontInfo
new file mode 100644
--- /dev/null
+++ b/.golden/shapedRuns/mixedDirectionComplexRTL.fontInfo
@@ -0,0 +1,1 @@
+assets/fonts/plex/IBMPlexSansArabic-Regular.ttf 0 32,32 32,32
diff --git a/.golden/shapedRuns/mixedDirectionComplexRTL.golden b/.golden/shapedRuns/mixedDirectionComplexRTL.golden
new file mode 100644
--- /dev/null
+++ b/.golden/shapedRuns/mixedDirectionComplexRTL.golden
@@ -0,0 +1,84 @@
+[ (0, -35, 
+    [ (GlyphInfo {codepoint = 147, cluster = 83, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 82, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 81, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 80, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 517, cluster = 78, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 27, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 955, cluster = 76, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 15, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 75, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 74, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 73, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 72, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 71, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 971, cluster = 69, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 14, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 394, cluster = 67, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 66, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 65, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 64, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 63, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 62, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (212, -35, 
+    [ (GlyphInfo {codepoint = 3, cluster = 61, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 60, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 59, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 58, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 57, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (264, -35, 
+    [ (GlyphInfo {codepoint = 26, cluster = 48, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 11, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 27, cluster = 49, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 18, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 50, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 51, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 52, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 148, cluster = 53, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 54, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 27, cluster = 55, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 18, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 26, cluster = 56, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 11, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (374, -35, 
+    [ (GlyphInfo {codepoint = 3, cluster = 47, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 46, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 45, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 44, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 43, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 293, cluster = 41, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 389, cluster = 39, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 38, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 37, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 36, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 35, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 34, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 512, cluster = 32, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 27, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 286, cluster = 30, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 7, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 29, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 28, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 27, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 26, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 25, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (581, -35, 
+    [ (GlyphInfo {codepoint = 3, cluster = 24, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 23, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 22, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 21, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 20, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (633, -35, 
+    [ (GlyphInfo {codepoint = 4, cluster = 11, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 17, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 6, cluster = 12, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 13, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 14, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 15, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 148, cluster = 16, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 17, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 6, cluster = 18, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 4, cluster = 19, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 17, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+, (757, -35, 
+    [ (GlyphInfo {codepoint = 3, cluster = 10, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 147, cluster = 9, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 19, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 3, cluster = 8, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 8, y_advance = 0, x_offset = 0, y_offset = 0})
+    , (GlyphInfo {codepoint = 90, cluster = 7, unsafeToBreak = False, unsafeToConcat = False, safeToInsertTatweel = False}, GlyphPos {x_advance = 9, y_advance = 0, x_offset = 0, y_offset = 0})
+    ])
+]
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
 # Revision history for Balkón
 
+## 1.1.0.0 -- 2023-05-31
+
+* Updated support for bidirectional text:
+
+    * Implemented correct order of directional runs for paragraphs whose
+      base direction is RTL.
+
+    * Numbers now create an implicit LTR embedding level.
+
+    * The direction of weak directional characters and neutral characters is
+      now determined by the embedding level of surrounding characters, or if
+      that does not match, by the base paragraph direction.
+
+    * For the legacy plain text interface, base paragraph direction is now set
+      according to the first strong character.
+
+    * Shaping uses the base paragraph direction when no strongly directional
+      characters are present.
+
 ## 1.0.0.1 -- 2023-05-18
 
 * Added another test with "shaped runs" format to allow manual testing of
diff --git a/balkon.cabal b/balkon.cabal
--- a/balkon.cabal
+++ b/balkon.cabal
@@ -14,7 +14,7 @@
 -- PVP summary:     +-+------- breaking API changes
 --                  | | +----- non-breaking API additions
 --                  | | | +--- code changes with no API change
-version:            1.0.0.1
+version:            1.1.0.0
 
 stability:          experimental
 
@@ -106,6 +106,7 @@
     exposed-modules:
         Data.Text.ParagraphLayout.Internal.AncestorBox,
         Data.Text.ParagraphLayout.Internal.ApplyBoxes,
+        Data.Text.ParagraphLayout.Internal.BiDiLevels,
         Data.Text.ParagraphLayout.Internal.BiDiReorder,
         Data.Text.ParagraphLayout.Internal.BoxOptions,
         Data.Text.ParagraphLayout.Internal.Break,
@@ -139,6 +140,7 @@
         Data.Text.ParagraphLayout.Internal.ParagraphLine,
         Data.Text.ParagraphLayout.Internal.ProtoFragment,
         Data.Text.ParagraphLayout.Internal.ProtoLine,
+        Data.Text.ParagraphLayout.Internal.ProtoRun,
         Data.Text.ParagraphLayout.Internal.Script
 
     build-depends:
@@ -182,6 +184,7 @@
     other-modules:
         Data.Text.ParagraphLayout.FontLoader,
         Data.Text.ParagraphLayout.Internal.ApplyBoxesSpec,
+        Data.Text.ParagraphLayout.Internal.BiDiLevelsSpec,
         Data.Text.ParagraphLayout.Internal.BiDiReorderSpec,
         Data.Text.ParagraphLayout.Internal.BreakSpec,
         Data.Text.ParagraphLayout.Internal.LinePaginationSpec,
@@ -196,7 +199,8 @@
         Data.Text.ParagraphLayout.RectSpec,
         Data.Text.ParagraphLayout.Rich.ParagraphData,
         Data.Text.ParagraphLayout.RichSpec,
-        Data.Text.ParagraphLayout.SpanData
+        Data.Text.ParagraphLayout.RunLengthEncoding,
+        Data.Text.ParagraphLayout.TextData
 
     -- Test dependencies.
     build-depends:
diff --git a/lib/Data/Text/ParagraphLayout/Plain.hs b/lib/Data/Text/ParagraphLayout/Plain.hs
--- a/lib/Data/Text/ParagraphLayout/Plain.hs
+++ b/lib/Data/Text/ParagraphLayout/Plain.hs
@@ -4,7 +4,7 @@
 -- the entire paragraph. As a consequence, all lines will have the same height.
 module Data.Text.ParagraphLayout.Plain
 {-# DEPRECATED "Use \"Data.Text.ParagraphLayout.Rich\" instead." #-}
-    --  Input paragraph
+    -- * Input paragraph
     ( Paragraph (Paragraph)
     , LineHeight (Absolute, Normal)
     , ParagraphOptions
diff --git a/lib/Data/Text/ParagraphLayout/Rich.hs b/lib/Data/Text/ParagraphLayout/Rich.hs
--- a/lib/Data/Text/ParagraphLayout/Rich.hs
+++ b/lib/Data/Text/ParagraphLayout/Rich.hs
@@ -2,7 +2,7 @@
 --
 -- The main entry point is the function `layoutRich`.
 module Data.Text.ParagraphLayout.Rich
-    --  Input paragraph
+    -- * Input paragraph
     ( Paragraph (Paragraph)
     , constructParagraph
     , BoxSpacing (BoxSpacingLeftRight)
diff --git a/src/Data/Text/ParagraphLayout/Internal/BiDiLevels.hs b/src/Data/Text/ParagraphLayout/Internal/BiDiLevels.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/ParagraphLayout/Internal/BiDiLevels.hs
@@ -0,0 +1,177 @@
+module Data.Text.ParagraphLayout.Internal.BiDiLevels
+    ( Level
+    , TextLevels (TextLevels)
+    , WithLevel
+    , dropLevels
+    , firstStrongDirection
+    , headLevel
+    , level
+    , levelDirectionH
+    , tailLevels
+    , textLevels
+    )
+where
+
+import Data.Maybe (catMaybes, listToMaybe)
+import Data.Text (Text, uncons, unpack)
+import Data.Text.Glyphize (Direction (..))
+import qualified Data.Text.ICU.Char as ICUChar
+import Data.Word (Word8)
+
+-- | BiDi level, between 0 and 125 inclusive.
+-- Even values mean left-to-right text.
+-- Odd values mean right-to-left text.
+type Level = Word8
+
+-- | Typeclass for any data structure with an associated BiDi level.
+class WithLevel a where
+    level :: a -> Level
+
+-- | BiDi levels for each character from a given input text.
+--
+-- This wrapper is meant to ease the transition to a different internal
+-- representation, if required when integrating with the ICU.
+newtype TextLevels = TextLevels [Level]
+    deriving (Eq, Show)
+
+headLevel :: TextLevels -> Level
+headLevel (TextLevels xs) = head xs
+
+tailLevels :: TextLevels -> TextLevels
+tailLevels (TextLevels xs) = TextLevels (tail xs)
+
+dropLevels :: Int -> TextLevels -> TextLevels
+dropLevels n (TextLevels xs) = TextLevels (drop n xs)
+
+-- | Determine the BiDi level of each character in the input text
+-- using a simplified algorithm with the following main limitations:
+--
+-- - Explicit directional formatting characters are ignored.
+--   This, by extension, means that levels cannot be controlled manually.
+--
+-- - Arabic and European numbers are treated equally and do not absorb
+--   separators, terminators, or nonspacing marks.
+--
+-- - Paired brackets have no effect on text direction.
+--
+-- TODO: Use Haskell bindings to the ICU BiDi implementation once available.
+textLevels :: Direction -> Text -> TextLevels
+textLevels baseDir txt = TextLevels $ textLevels' baseLevel baseLevel 0 txt
+    where
+        baseLevel = directionLevel 0 baseDir
+
+-- | Determine the BiDi level of each character in the input text
+-- using a simplified algorithm.
+textLevels' :: Level -> Level -> Int -> Text -> [Level]
+textLevels' baseLevel previousLevel neutrals txt = case uncons txt of
+    Just (char, rest) -> case charLevel baseLevel char of
+        Just currentLevel ->
+            replicate neutrals (mergeLevel baseLevel previousLevel currentLevel)
+            ++
+            currentLevel
+            :
+            textLevels' baseLevel currentLevel 0 rest
+        Nothing ->
+            textLevels' baseLevel previousLevel (neutrals + 1) rest
+    Nothing ->
+        replicate neutrals baseLevel
+
+-- | Determine the level of a neutral character based on its surrounding levels.
+mergeLevel :: Level -> Level -> Level -> Level
+mergeLevel base prev cur
+    | prev == cur
+        = prev
+    | low > base && even high -- Avoid breaking a RTL run by numbers.
+        = low
+    | otherwise
+        = base
+    where
+        low = min prev cur
+        high = max prev cur
+
+data SimpleType = StrongLTR | StrongRTL | Number | Neutral | Reset
+
+-- | Determine the BiDi level of one character
+-- using a simplified algorithm.
+charLevel :: Level -> Char -> Maybe Level
+charLevel baseLevel c = case simpleType c of
+    StrongLTR -> Just $ directionLevel baseLevel DirLTR
+    StrongRTL -> Just $ directionLevel baseLevel DirRTL
+    Number -> Just $ directionLevel (baseLevel + 1) DirLTR
+    Neutral -> Nothing
+    Reset -> Just baseLevel
+
+-- | Reduce the character's BiDi class into a simpler category
+-- for the purposes of the simplified algorithm.
+simpleType :: Char -> SimpleType
+simpleType c = case ICUChar.direction c of
+    -- Strong characters have a definitive type.
+    ICUChar.LeftToRight -> StrongLTR
+    ICUChar.RightToLeft -> StrongRTL
+    ICUChar.RightToLeftArabic -> StrongRTL
+    -- Ignoring separators and terminators, all numbers behave the same.
+    ICUChar.EuropeanNumber -> Number
+    ICUChar.ArabicNumber -> Number
+    -- Treating the following weak characters as neutral for simplicity.
+    ICUChar.EuropeanNumberSeparator -> Neutral
+    ICUChar.EuropeanNumberTerminator -> Neutral
+    ICUChar.CommonNumberSeparator -> Neutral
+    ICUChar.DirNonSpacingMark -> Neutral
+    ICUChar.BoundaryNeutral -> Neutral
+    -- The following characters are always neutral.
+    ICUChar.WhiteSpaceNeutral -> Neutral
+    ICUChar.OtherNeutral -> Neutral
+    -- The following characters get their level reset.
+    ICUChar.BlockSeparator -> Reset
+    ICUChar.SegmentSeparator -> Reset
+    -- Explicit formatting is not handled by this algorithm.
+    ICUChar.LeftToRightEmbedding -> Neutral
+    ICUChar.LeftToRightOverride -> Neutral
+    ICUChar.RightToLeftEmbedding -> Neutral
+    ICUChar.RightToLeftOverride -> Neutral
+    ICUChar.PopDirectionalFormat -> Neutral
+    ICUChar.FirstStrongIsolate -> Neutral
+    ICUChar.LeftToRightIsolate -> Neutral
+    ICUChar.RightToLeftIsolate -> Neutral
+    ICUChar.PopDirectionalIsolate -> Neutral
+
+-- | `Just` the direction of the first strongly directional character,
+-- or `Nothing` if there is no strongly directional character.
+firstStrongDirection :: Text -> Maybe Direction
+firstStrongDirection = firstJust . map strongDirection . unpack
+
+-- | `Just` the direction of a strongly directional character,
+-- or `Nothing` if the character is not strongly directional.
+strongDirection :: Char -> Maybe Direction
+strongDirection c = case ICUChar.direction c of
+    ICUChar.LeftToRight -> Just DirLTR
+    ICUChar.RightToLeft -> Just DirRTL
+    ICUChar.RightToLeftArabic -> Just DirRTL
+    _ -> Nothing
+
+-- | The first `Just` value found in the input list, if there is one,
+-- otherwise `Nothing`.
+firstJust :: [Maybe a] -> Maybe a
+firstJust = listToMaybe . catMaybes
+
+-- | Convert embedding level to horizontal text direction.
+levelDirectionH :: Level -> Direction
+levelDirectionH lvl
+    | even lvl = DirLTR
+    | otherwise = DirRTL
+
+-- | Convert text direction to the smallest corresponding embedding level,
+-- but no smaller than the given minimum.
+directionLevel :: Level -> Direction -> Level
+directionLevel low DirLTR = smallestEvenAtLeast low
+directionLevel low DirRTL = smallestOddAtLeast low
+directionLevel low DirTTB = smallestEvenAtLeast low
+directionLevel low DirBTT = smallestOddAtLeast low
+
+-- | Smallest even integer greater than or equal to @x@.
+smallestEvenAtLeast :: Integral a => a -> a
+smallestEvenAtLeast x = if even x then x else x + 1
+
+-- | Smallest odd integer greater than or equal to @x@.
+smallestOddAtLeast :: Integral a => a -> a
+smallestOddAtLeast x = if odd x then x else x + 1
diff --git a/src/Data/Text/ParagraphLayout/Internal/BiDiReorder.hs b/src/Data/Text/ParagraphLayout/Internal/BiDiReorder.hs
--- a/src/Data/Text/ParagraphLayout/Internal/BiDiReorder.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/BiDiReorder.hs
@@ -1,24 +1,11 @@
-module Data.Text.ParagraphLayout.Internal.BiDiReorder
-    ( Level
-    , WithLevel
-    , level
-    , reorder
-    )
+module Data.Text.ParagraphLayout.Internal.BiDiReorder (reorder)
 where
 
 import Data.List.NonEmpty (NonEmpty ((:|)))
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Semigroup (sconcat)
-import Data.Word (Word8)
 
--- | BiDi level, between 0 and 125 inclusive.
--- Even values mean left-to-right text.
--- Odd values mean right-to-left text.
-type Level = Word8
-
--- | Typeclass for any data structure with an associated BiDi level.
-class WithLevel a where
-    level :: a -> Level
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 
 -- | Generic reordering of bidirectional text according to rule L2 of UAX #9
 -- <https://www.unicode.org/reports/tr9/#Reordering_Resolved_Levels>.
diff --git a/src/Data/Text/ParagraphLayout/Internal/BoxOptions.hs b/src/Data/Text/ParagraphLayout/Internal/BoxOptions.hs
--- a/src/Data/Text/ParagraphLayout/Internal/BoxOptions.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/BoxOptions.hs
@@ -15,7 +15,7 @@
 data BoxOptions = BoxOptions
 
     { boxSpacing :: BoxSpacing
-    -- Determines amount of empty space to add before the first fragment
+    -- ^ Determines amount of empty space to add before the first fragment
     -- and/or after the last fragment of this box.
     --
     -- This can be used to reserve space for a left margin, border,
diff --git a/src/Data/Text/ParagraphLayout/Internal/Layout.hs b/src/Data/Text/ParagraphLayout/Internal/Layout.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Layout.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Layout.hs
@@ -137,7 +137,7 @@
         normalLineHeight = ascent + descent
         ascent = ascender extents
         descent = - descender extents
-        extents = fontExtentsForDir (textFont opts) (PF.direction pf)
+        extents = fontExtentsForDir (textFont opts) (Just $ PF.direction pf)
         opts = RS.spanTextOptions rs
 
 ancestorBoxes
@@ -194,10 +194,12 @@
         -- TODO: Consider optimising.
         --       We do not need to look for soft breaks further than the
         --       shortest hard break.
+        -- TODO: Untrimmed whitespace should be reset to paragraph BiDi level
+        --       per rule L1.
         splits = hardSplit runs :| softSplits runs
 
--- | Construct a `ProtoLine`, peeking at the text run on the following line
--- to determine `nextOpenBoxes`.
+-- | Construct a `PL.ProtoLine`, peeking at the text run on the following line
+-- to determine `PL.nextOpenBoxes`.
 protoLine
     :: [RB.ResolvedBox d]
     -> [ProtoFragmentWithSpan d]
@@ -295,9 +297,10 @@
 layoutRunH :: WithSpan d Run -> ProtoFragmentWithSpan d
 layoutRunH (WithSpan rs run) = WithSpan rs pf
     where
-        pf = PF.protoFragmentH dir glyphs
+        pf = PF.protoFragmentH dir lvl glyphs
         glyphs = shapeRun (WithSpan rs run)
         dir = runDirection run
+        lvl = runLevel run
 
 -- | Calculate layout for the given run independently of its position.
 shapeRun :: WithSpan d Run -> [(GlyphInfo, GlyphPos)]
@@ -307,7 +310,7 @@
         buffer = defaultBuffer
             { text = Lazy.fromStrict $ runText run
             , contentType = Just ContentTypeUnicode
-            , direction = runDirection run
+            , direction = Just $ runDirection run
             , script = runScript run
             , language = Just $ textLanguage opts
             -- Perhaps counter-intuitively, the `beginsText` and `endsText`
diff --git a/src/Data/Text/ParagraphLayout/Internal/ParagraphLine.hs b/src/Data/Text/ParagraphLayout/Internal/ParagraphLine.hs
--- a/src/Data/Text/ParagraphLayout/Internal/ParagraphLine.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/ParagraphLine.hs
@@ -56,11 +56,11 @@
     shiftFragments dy = R.mapFragments (shiftFragment dy)
     appendFragments = R.appendFragments
 
--- | Split the given `ParagraphLayout` into single-line layouts.
+-- | Split the given paragraph layout into single-line layouts.
 cutLines :: GenericLayout pl => pl -> [pl]
 cutLines pl = map (\ n -> cutLine n pl) (lineNumbers pl)
 
--- | Reduce the given `ParagraphLayout` to fragments with the given line number.
+-- | Reduce the given paragraph layout to fragments with the given line number.
 cutLine :: GenericLayout pl => Int -> pl -> pl
 cutLine n pl = trimTop $ limitFragments n pl
 
@@ -78,7 +78,7 @@
 dedupe :: Eq a => [a] -> [a]
 dedupe xs = map NonEmpty.head $ NonEmpty.group xs
 
--- | Put the given `ParagraphLayout`s together as a vertically contiguous
+-- | Put the given paragraph layouts together as a vertically contiguous
 -- sequence.
 mergeLines :: GenericLayout pl => [pl] -> pl
 mergeLines lls = foldl mergeLine empty lls
diff --git a/src/Data/Text/ParagraphLayout/Internal/Plain.hs b/src/Data/Text/ParagraphLayout/Internal/Plain.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Plain.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Plain.hs
@@ -5,6 +5,7 @@
 import Data.Text.Glyphize (Direction (DirLTR))
 
 import Data.Text.ParagraphLayout.Internal.AncestorBox
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.BoxOptions
 import Data.Text.ParagraphLayout.Internal.Fragment
 import Data.Text.ParagraphLayout.Internal.ParagraphOptions
@@ -29,7 +30,8 @@
 -- Span indexes are added to the user data internally, then used to split the
 -- resulting fragments according to their corresponding spans.
 plainToRich :: P.Paragraph d -> R.Paragraph (Int, d)
-plainToRich (P.Paragraph arr off spans opts) = R.Paragraph arr off rootNode opts
+plainToRich p@(P.Paragraph arr off spans opts) =
+    R.Paragraph arr off rootNode opts
     where
         rootNode = RootBox rootBox
         rootBox = Box spanNodes baseOpts
@@ -39,10 +41,21 @@
             (boxFromPlain baseOpts i s)
             defaultBoxOptions
         indexedSpans = zip [0 ..] spans
-        baseOpts = (defaultTextOptions DirLTR)
+        baseOpts = (defaultTextOptions $ detectDirection p)
             { textFont = paragraphFont opts
             , textLineHeight = paragraphLineHeight opts
             }
+
+-- | Simplified autodetection of text direction for plain text,
+-- to provide temporary compatibility with the old interface which
+-- did not allow setting text direction explicitly.
+--
+-- Note that the detected direction carries over hard line breaks,
+-- which is not compliant with the Unicode Bidirectional Algorithm.
+detectDirection :: P.Paragraph d -> Direction
+detectDirection p = case firstStrongDirection (P.paragraphText p) of
+    Just dir -> dir
+    Nothing -> DirLTR
 
 -- | Convert a legacy `Span` to a rich text box with one text node inside.
 --
diff --git a/src/Data/Text/ParagraphLayout/Internal/ProtoFragment.hs b/src/Data/Text/ParagraphLayout/Internal/ProtoFragment.hs
--- a/src/Data/Text/ParagraphLayout/Internal/ProtoFragment.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/ProtoFragment.hs
@@ -7,12 +7,15 @@
 import Data.Int (Int32)
 import Data.Text.Glyphize (Direction (..), GlyphInfo, GlyphPos (x_advance))
 
-import Data.Text.ParagraphLayout.Internal.BiDiReorder
+import qualified Data.Text.ParagraphLayout.Internal.BiDiLevels as BiDi
 
 -- | A box fragment which has not been positioned yet.
 data ProtoFragment = ProtoFragment
-    { direction :: Maybe Direction
+    { direction :: Direction
     -- ^ Text direction, which is constant within a fragment.
+    , level :: BiDi.Level
+    -- ^ BiDi embedding level.
+    -- Should be even for LTR fragments and odd for RTL fragments.
     , advance :: Int32
     -- ^ Total advance of glyphs in this fragment,
     -- depending on the text direction.
@@ -21,17 +24,11 @@
 
 -- | Construct a `ProtoFragment`, automatically calculating the total advance
 -- for a horizontal text direction.
-protoFragmentH :: Maybe Direction -> [(GlyphInfo, GlyphPos)] -> ProtoFragment
-protoFragmentH dir gs = ProtoFragment dir adv gs
+protoFragmentH :: Direction -> BiDi.Level -> [(GlyphInfo, GlyphPos)] ->
+    ProtoFragment
+protoFragmentH dir lvl gs = ProtoFragment dir lvl adv gs
     where
         adv = sum $ map (x_advance . snd) gs
 
-instance WithLevel ProtoFragment where
-    level pf = case direction pf of
-        -- TODO: Allow externally set paragraph embedding level.
-        -- TODO: Properly calculate BiDi levels.
-        Just DirLTR -> 0
-        Just DirRTL -> 1
-        Just DirTTB -> 0
-        Just DirBTT -> 1
-        Nothing -> 0
+instance BiDi.WithLevel ProtoFragment where
+    level = level -- BiDi.level = ProtoFragment.level
diff --git a/src/Data/Text/ParagraphLayout/Internal/ProtoRun.hs b/src/Data/Text/ParagraphLayout/Internal/ProtoRun.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Text/ParagraphLayout/Internal/ProtoRun.hs
@@ -0,0 +1,15 @@
+module Data.Text.ParagraphLayout.Internal.ProtoRun (ProtoRun (..))
+where
+
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
+import Data.Text.ParagraphLayout.Internal.Script
+import Data.Text.ParagraphLayout.Internal.Zipper
+
+-- | Intermediate structure for creating
+-- `Data.Text.ParagraphLayout.Internal.Run.Run` values.
+data ProtoRun = ProtoRun
+    { zipper :: Zipper
+    , followingLevels :: TextLevels
+    , level :: Level
+    , script :: ScriptCode
+    }
diff --git a/src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs b/src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs
--- a/src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/ResolvedSpan.hs
@@ -4,6 +4,7 @@
 import Data.Text (Text)
 import qualified Data.Text.ICU as BreakStatus (Line)
 
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.ResolvedBox
 import Data.Text.ParagraphLayout.Internal.TextContainer
 import Data.Text.ParagraphLayout.Internal.TextOptions
@@ -17,6 +18,7 @@
     , spanText :: Text
     , spanTextOptions :: TextOptions
     , spanBoxes :: [ResolvedBox d]
+    , spanBiDiLevels :: TextLevels
     , spanLineBreaks :: [(Int, BreakStatus.Line)]
     -- TODO: Can be optimised by starting with the shortest line break.
     , spanCharacterBreaks :: [(Int, ())]
diff --git a/src/Data/Text/ParagraphLayout/Internal/Rich.hs b/src/Data/Text/ParagraphLayout/Internal/Rich.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Rich.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Rich.hs
@@ -5,8 +5,10 @@
 import Data.List.NonEmpty (nonEmpty)
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Text (Text)
+import qualified Data.Text as Text
 import Data.Text.ICU (Breaker, LocaleName, breakCharacter, breakLine)
 
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.Break
 import Data.Text.ParagraphLayout.Internal.Fragment
 import Data.Text.ParagraphLayout.Internal.Layout
@@ -43,7 +45,9 @@
 spanToRunsWrapped s = map (WithSpan s) (spanToRuns s)
 
 resolveSpans :: Paragraph d -> [RS.ResolvedSpan d]
-resolveSpans p@(Paragraph _ pStart root _) = do
+resolveSpans p = do
+    let Paragraph _ pStart root _ = p
+    let RootBox (Box _ rootTextOpts) = root
     let leaves = flatten root
     let sTexts = paragraphSpanTexts p
     let sBounds = paragraphSpanBounds p
@@ -57,8 +61,14 @@
         <*> ZipList sTexts
     let (TextLeaf userData _ textOpts boxes) = leaf
     let lang = textLanguage textOpts
+    -- TODO: Allow BiDi embedding/isolation for inner nodes.
+    let pLevels = textLevels (textDirection rootTextOpts) pText
     let lBreaks = paragraphBreaks breakLine pText lang
     let cBreaks = paragraphBreaks breakCharacter pText lang
+    -- TODO: Optimise. This has time complexity O(n*s), where n is number of
+    --       characters and s is number of resolved spans.
+    --       Maybe include byte offsets in the TextLevels data structure?
+    let pPrefixLen = Text.length $ paragraphPrefix p sStart
     return RS.ResolvedSpan
         { RS.spanUserData = userData
         , RS.spanIndex = i
@@ -67,6 +77,7 @@
         , RS.spanText = sText
         , RS.spanTextOptions = textOpts
         , RS.spanBoxes = boxes
+        , RS.spanBiDiLevels = dropLevels pPrefixLen pLevels
         , RS.spanLineBreaks = subOffsetsDesc (sStart - pStart) lBreaks
         , RS.spanCharacterBreaks = subOffsetsDesc (sStart - pStart) cBreaks
         }
diff --git a/src/Data/Text/ParagraphLayout/Internal/Rich/Paragraph.hs b/src/Data/Text/ParagraphLayout/Internal/Rich/Paragraph.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Rich/Paragraph.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Rich/Paragraph.hs
@@ -1,6 +1,7 @@
 module Data.Text.ParagraphLayout.Internal.Rich.Paragraph
     ( Paragraph (..)
     , constructParagraph
+    , paragraphPrefix
     , paragraphSpanBounds
     , paragraphSpanTexts
     , paragraphText
@@ -68,7 +69,8 @@
     -> ParagraphOptions
     -- ^ Options to apply to the paragraph as a whole.
     -> Paragraph d
-    -- ^ Constructed paragraph that can be passed to `layoutRich`.
+    -- ^ Constructed paragraph that can be passed to
+    -- `Data.Text.ParagraphLayout.Rich.layoutRich`.
 constructParagraph prefix root suffix = Paragraph arr afterPrefix root'
     where
         (Text arr beforePrefix _) = txt
@@ -109,4 +111,12 @@
     where
         start = NonEmpty.head sBounds
         end = NonEmpty.last sBounds
+        sBounds = paragraphSpanBounds p
+
+-- | Turn all text nodes from the input `Paragraph` up to the given end offset
+-- into one combined `Text`. No bounds checking is performed!
+paragraphPrefix :: Paragraph d -> Int -> Text
+paragraphPrefix p@(Paragraph arr _ _ _) end = Text arr start (end - start)
+    where
+        start = NonEmpty.head sBounds
         sBounds = paragraphSpanBounds p
diff --git a/src/Data/Text/ParagraphLayout/Internal/Run.hs b/src/Data/Text/ParagraphLayout/Internal/Run.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Run.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Run.hs
@@ -1,4 +1,4 @@
-module Data.Text.ParagraphLayout.Internal.Run (Run (..), ScriptCode, spanToRuns)
+module Data.Text.ParagraphLayout.Internal.Run (Run (..), spanToRuns)
 where
 
 import Data.List (mapAccumL)
@@ -7,22 +7,23 @@
 import qualified Data.Text as Text
 import Data.Text.Foreign (dropWord8, lengthWord8, takeWord8)
 import Data.Text.Glyphize (Direction (..))
-import qualified Data.Text.ICU.Char as ICUChar
 
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
+import Data.Text.ParagraphLayout.Internal.ProtoRun (ProtoRun (ProtoRun))
+import qualified Data.Text.ParagraphLayout.Internal.ProtoRun as PR
 import Data.Text.ParagraphLayout.Internal.ResolvedSpan
 import Data.Text.ParagraphLayout.Internal.Script
 import Data.Text.ParagraphLayout.Internal.TextContainer
 import Data.Text.ParagraphLayout.Internal.Zipper
 
-type ScriptCode = String
-
 -- | Each span can be broken into one or more runs by Balkón.
 --
 -- Each run could have a different script, language, or direction.
 data Run = Run
     { runOffsetInSpan :: Int
     , runText :: Text
-    , runDirection :: Maybe Direction
+    , runLevel :: Level
+    , runDirection :: Direction
     , runScript :: Maybe ScriptCode
     }
     deriving (Eq, Show)
@@ -50,8 +51,6 @@
             o' = o + l - l'
     dropWhileEnd p r = r { runText = Text.dropWhileEnd p (runText r) }
 
-type ProtoRun = (Zipper, Maybe Direction, ScriptCode)
-
 -- | Represents a zipper that can advance by at least one character.
 data ZipperChoice = ZipperChoice
     { nextChar :: Char
@@ -75,53 +74,58 @@
 data Merged a = Incompatible | Merged a
 
 spanToRuns :: ResolvedSpan d -> [Run]
-spanToRuns s = snd $ mapAccumL run 0 $ protoRuns zipper
+spanToRuns s = snd $ mapAccumL run 0 $ protoRuns zipper levels
     where
-        zipper = start $ spanText s
-        run acc (z, d, sc) = let t = preceding z in
+        wholeText = spanText s
+        zipper = start wholeText
+        levels = spanBiDiLevels s
+        run acc pr = let t = preceding (PR.zipper pr) in
             ( acc + lengthWord8 t
             , Run
                 { runOffsetInSpan = acc
                 , runText = t
-                , runDirection = d
-                , runScript = Just sc
+                , runLevel = PR.level pr
+                , runDirection = levelDirectionH $ PR.level pr
+                , runScript = Just $ PR.script pr
                 }
             )
 
-protoRuns :: Zipper -> [ProtoRun]
-protoRuns z = reverse $ protoRuns' z []
+protoRuns :: Zipper -> TextLevels -> [ProtoRun]
+protoRuns z bl = reverse $ protoRuns' z bl []
 
-protoRuns' :: Zipper -> [ProtoRun] -> [ProtoRun]
-protoRuns' curZipper curRuns = case considerNext curZipper of
+protoRuns' :: Zipper -> TextLevels -> [ProtoRun] -> [ProtoRun]
+protoRuns' curZipper curLevels curRuns = case considerNext curZipper of
     Nothing -> curRuns
     Just choice ->
-        let headRun@(nextZipper, _, _) :| tailRuns = foldRun choice curRuns
-        in protoRuns' nextZipper (headRun : tailRuns)
+        let headRun :| tailRuns = foldRun (choice, curLevels) curRuns
+        in protoRuns' (PR.zipper headRun) curLevels (headRun : tailRuns)
 
-foldRun :: ZipperChoice -> [ProtoRun] -> NonEmpty ProtoRun
+foldRun :: (ZipperChoice, TextLevels) -> [ProtoRun] -> NonEmpty ProtoRun
 
 -- If there are no runs, create a new run with a single character.
-foldRun x [] = (continuingRun x, d, s) :| []
+foldRun (x, bl) [] = ProtoRun (continuingRun x) lvls lvl s :| []
     where
-        d = charDirection (nextChar x)
+        lvl = headLevel bl
+        lvls = tailLevels bl
         s = charScript (nextChar x)
 
-foldRun x (previousRun@(_, d1, s1) : tailRuns) =
-    case (mergeDirections d1 d2, mergeScripts s1 s2) of
-        (Merged d, Merged s) -> (continuingRun x, d, s) :| tailRuns
-        _ -> (startingNewRun x, d2, s2) :| previousRun : tailRuns
+foldRun (x, _) (previousRun : tailRuns) =
+    case (mergeLevels l1 l2, mergeScripts s1 s2) of
+        (Merged l, Merged s) ->
+            ProtoRun (continuingRun x) bl' l s :| tailRuns
+        _ ->
+            ProtoRun (startingNewRun x) bl' l2 s2 :| previousRun : tailRuns
     where
-        d2 = charDirection (nextChar x)
+        bl = PR.followingLevels previousRun
+        l1 = PR.level previousRun
+        s1 = PR.script previousRun
+        l2 = headLevel bl
         s2 = charScript (nextChar x)
+        bl' = tailLevels bl
 
--- | Simplified detection of text direction for unidirectional text.
-mergeDirections :: Maybe Direction -> Maybe Direction ->
-    Merged (Maybe Direction)
-mergeDirections Nothing Nothing = Merged Nothing
-mergeDirections (Just d1) Nothing = Merged (Just d1)
-mergeDirections Nothing (Just d2) = Merged (Just d2)
-mergeDirections (Just d1) (Just d2)
-    | d1 == d2 = Merged (Just d1)
+mergeLevels :: Level -> Level -> Merged Level
+mergeLevels l1 l2
+    | l1 == l2 = Merged l1
     | otherwise = Incompatible
 
 -- TODO: Implement proper inheritance rules.
@@ -132,11 +136,3 @@
 mergeScripts s1 s2
     | s1 == s2 = Merged s1
     | otherwise = Incompatible
-
--- TODO: Use the BiDi algorithm to support bidirectional text.
-charDirection :: Char -> Maybe Direction
-charDirection c = case ICUChar.direction c of
-    ICUChar.LeftToRight -> Just DirLTR
-    ICUChar.RightToLeft -> Just DirRTL
-    ICUChar.RightToLeftArabic -> Just DirRTL
-    _ -> Nothing
diff --git a/src/Data/Text/ParagraphLayout/Internal/Script.hs b/src/Data/Text/ParagraphLayout/Internal/Script.hs
--- a/src/Data/Text/ParagraphLayout/Internal/Script.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/Script.hs
@@ -1,4 +1,4 @@
-module Data.Text.ParagraphLayout.Internal.Script (charScript)
+module Data.Text.ParagraphLayout.Internal.Script (ScriptCode, charScript)
 where
 
 -- TODO: Use a direct interface to the ICU library, if possible.
@@ -6,12 +6,14 @@
 -- hardcoded ranges, which may get out of sync with ICU.
 import Unicode.Char.General.Scripts
 
+type ScriptCode = String
+
 -- | A short script code for the given character, as expected by HarfBuzz.
-charScript :: Char -> String
+charScript :: Char -> ScriptCode
 charScript = code . script
 
 -- Hardcoded shortenings of script codes as of Unicode 15.0.0.
-code :: Script -> String
+code :: Script -> ScriptCode
 code Adlam = "Adlm"
 code Ahom = "Ahom"
 code AnatolianHieroglyphs = "Hluw"
diff --git a/src/Data/Text/ParagraphLayout/Internal/TextOptions.hs b/src/Data/Text/ParagraphLayout/Internal/TextOptions.hs
--- a/src/Data/Text/ParagraphLayout/Internal/TextOptions.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/TextOptions.hs
@@ -33,10 +33,12 @@
 
     , textDirection :: Direction
     -- ^ Base text direction.
+    --
     -- Used to determine which box fragment gets the left spacing and which
     -- gets the right spacing when broken over multiple lines.
     --
-    -- TODO: Also use for the visual order of boxes.
+    -- When applied to the root box in a paragraph, this is also used to
+    -- determine the base direction of the paragraph.
 
     -- TODO: textVerticalAlign
     -- TODO: textLetterSpacing
diff --git a/src/Data/Text/ParagraphLayout/Internal/WithSpan.hs b/src/Data/Text/ParagraphLayout/Internal/WithSpan.hs
--- a/src/Data/Text/ParagraphLayout/Internal/WithSpan.hs
+++ b/src/Data/Text/ParagraphLayout/Internal/WithSpan.hs
@@ -4,7 +4,7 @@
     )
 where
 
-import Data.Text.ParagraphLayout.Internal.BiDiReorder
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.ResolvedBox
 import Data.Text.ParagraphLayout.Internal.ResolvedSpan
 import Data.Text.ParagraphLayout.Internal.TextContainer
diff --git a/test/Data/Text/ParagraphLayout/Internal/ApplyBoxesSpec.hs b/test/Data/Text/ParagraphLayout/Internal/ApplyBoxesSpec.hs
--- a/test/Data/Text/ParagraphLayout/Internal/ApplyBoxesSpec.hs
+++ b/test/Data/Text/ParagraphLayout/Internal/ApplyBoxesSpec.hs
@@ -6,6 +6,7 @@
 
 import Test.Hspec
 import Data.Text.ParagraphLayout.Internal.ApplyBoxes
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.BoxOptions
 import Data.Text.ParagraphLayout.Internal.ResolvedBox
 import Data.Text.ParagraphLayout.Internal.ResolvedSpan
@@ -23,6 +24,7 @@
     , spanText = empty
     , spanTextOptions = defaultTextOptions dir
     , spanBoxes = bs
+    , spanBiDiLevels = TextLevels []
     , spanLineBreaks = []
     , spanCharacterBreaks = []
     }
diff --git a/test/Data/Text/ParagraphLayout/Internal/BiDiLevelsSpec.hs b/test/Data/Text/ParagraphLayout/Internal/BiDiLevelsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Text/ParagraphLayout/Internal/BiDiLevelsSpec.hs
@@ -0,0 +1,81 @@
+module Data.Text.ParagraphLayout.Internal.BiDiLevelsSpec where
+
+import Data.Text (Text)
+import Data.Text.Glyphize (Direction (DirLTR, DirRTL))
+
+import Test.Hspec
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
+import Data.Text.ParagraphLayout.RunLengthEncoding
+import Data.Text.ParagraphLayout.TextData
+
+-- | Test that `textLevels` produces the levels of the lengths given in
+-- "Data.Text.ParagraphLayout.TextData" and values given here.
+shouldHaveLevels :: (Direction, a, Text, [Int]) -> [Level] -> SpecWith ()
+shouldHaveLevels (dir, _, text, lens) levels = it description $
+    result `shouldBe` TextLevels (runLengthDecode rls)
+    where
+        result = textLevels dir text
+        rls = zip lens levels
+        description = case rls of
+            [] -> "should be empty"
+            [(_, lvl)] -> "should all have level " ++ show lvl
+            _ -> "should have multiple levels"
+
+-- | Override the paragraph direction of sample data.
+setDirection :: Direction -> (Direction, a, b, c) -> (Direction, a, b, c)
+setDirection dir (_, a, b, c) = (dir, a, b, c)
+
+-- | Override the paragraph direction of sample data to LTR.
+setLTR :: (Direction, a, b, c) -> (Direction, a, b, c)
+setLTR = setDirection DirLTR
+
+-- | Override the paragraph direction of sample data to RTL.
+setRTL :: (Direction, a, b, c) -> (Direction, a, b, c)
+setRTL = setDirection DirRTL
+
+spec :: Spec
+spec = do
+
+    describe "textLevels" $ do
+
+        -- Empty input should produce empty output.
+        -- Infinite list of level 0 is also acceptable.
+        describe "on English input" $
+            englishEmpty `shouldHaveLevels` []
+
+        -- Empty input should produce empty output.
+        -- Infinite list of level 1 is also acceptable.
+        describe "on Arabic empty" $
+            arabicEmpty `shouldHaveLevels` []
+
+        -- All LTR text without numbers should always stay at the base level.
+        describe "on English word in LTR" $
+            setLTR englishWord `shouldHaveLevels` [0]
+
+        -- All characters in a RTL paragraph must be at least at level 1.
+        describe "on English word in RTL" $
+            setRTL englishWord `shouldHaveLevels` [2]
+
+        describe "on Arabic word in LTR" $
+            setLTR arabicHello `shouldHaveLevels` [1]
+
+        describe "on Arabic word in RTL" $
+            setRTL arabicHello `shouldHaveLevels` [1]
+
+        describe "on Serbian mixed script" $
+            serbianMixedScript `shouldHaveLevels` [0]
+
+        describe "on mixed direction with base LTR" $
+            mixedDirectionSimple DirLTR `shouldHaveLevels` [0, 1, 0]
+
+        describe "on mixed direction with base RTL" $
+            mixedDirectionSimple DirRTL `shouldHaveLevels` [2, 1, 2]
+
+        describe "on Arabic around English" $
+            arabicAroundEnglish `shouldHaveLevels` [1, 2, 1, 2, 1]
+
+        describe "on English around Arabic" $
+            englishAroundArabic `shouldHaveLevels` [0, 1, 0]
+
+        describe "on numbers in RTL run in LTR" $
+            mixedDirectionNumbers `shouldHaveLevels` [0, 1, 2, 1]
diff --git a/test/Data/Text/ParagraphLayout/Internal/BiDiReorderSpec.hs b/test/Data/Text/ParagraphLayout/Internal/BiDiReorderSpec.hs
--- a/test/Data/Text/ParagraphLayout/Internal/BiDiReorderSpec.hs
+++ b/test/Data/Text/ParagraphLayout/Internal/BiDiReorderSpec.hs
@@ -5,6 +5,7 @@
 import Data.Word (Word8)
 
 import Test.Hspec
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
 import Data.Text.ParagraphLayout.Internal.BiDiReorder
 
 ne :: [a] -> NonEmpty a
diff --git a/test/Data/Text/ParagraphLayout/Internal/RunSpec.hs b/test/Data/Text/ParagraphLayout/Internal/RunSpec.hs
--- a/test/Data/Text/ParagraphLayout/Internal/RunSpec.hs
+++ b/test/Data/Text/ParagraphLayout/Internal/RunSpec.hs
@@ -1,44 +1,273 @@
 module Data.Text.ParagraphLayout.Internal.RunSpec (spec) where
 
-import Data.Text (pack)
+import Data.Text (Text, pack)
 import Data.Text.Glyphize (Direction (..), emptyFont)
 
 import Test.Hspec
+import Data.Text.ParagraphLayout.Internal.BiDiLevels
+import Data.Text.ParagraphLayout.Internal.BoxOptions
+import Data.Text.ParagraphLayout.Internal.LineHeight
+import Data.Text.ParagraphLayout.Internal.ResolvedBox
 import Data.Text.ParagraphLayout.Internal.ResolvedSpan
 import Data.Text.ParagraphLayout.Internal.Run
-import Data.Text.ParagraphLayout.SpanData
+import Data.Text.ParagraphLayout.Internal.TextOptions
+import Data.Text.ParagraphLayout.RunLengthEncoding
+import Data.Text.ParagraphLayout.TextData
 
+defaultBox :: Direction -> ResolvedBox ()
+defaultBox dir = ResolvedBox () 0 defaultBoxOptions dir
+
+sampleSpan :: (Direction, String, Text, a) -> TextLevels -> ResolvedSpan ()
+sampleSpan (dir, lang, text, _) levels = ResolvedSpan
+    { spanUserData = ()
+    , spanIndex = 0
+    , spanOffsetInParagraph = 0
+    , spanText = text
+    , spanTextOptions = (defaultTextOptions dir)
+        { textFont = emptyFont
+        , textLineHeight = Normal
+        , textLanguage = lang
+        }
+    , spanBoxes = [defaultBox dir]
+    , spanBiDiLevels = levels
+    , spanLineBreaks = []
+    , spanCharacterBreaks = []
+    }
+
+allLTR :: TextLevels
+allLTR = TextLevels $ repeat 0
+
+allRTL :: TextLevels
+allRTL = TextLevels $ repeat 1
+
+levelsRLE :: [(Int, Level)] -> TextLevels
+levelsRLE rls = TextLevels $ runLengthDecode rls
+
 spec :: Spec
 spec = do
     describe "spanToRuns" $ do
         it "handles span with no text" $ do
-            spanToRuns (emptySpan emptyFont) `shouldBe` []
+            let inputSpan = sampleSpan englishEmpty allLTR
+            let runs = spanToRuns inputSpan
+            runs `shouldBe` []
         it "handles Czech hello" $ do
-            let inputSpan = czechHello emptyFont
+            let inputSpan = sampleSpan czechHello allLTR
             let runs = spanToRuns inputSpan
             runs `shouldBe`
                 [ Run
                     { runOffsetInSpan = 0
                     , runText = spanText inputSpan
-                    , runDirection = Just DirLTR
+                    , runLevel = 0
+                    , runDirection = DirLTR
                     , runScript = Just "Latn"
                     }
                 ]
+        it "handles Arabic hello" $ do
+            let inputSpan = sampleSpan arabicHello allRTL
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = spanText inputSpan
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                ]
         it "handles Serbian with mixed script" $ do
-            let inputSpan = serbianMixedScript emptyFont
+            let inputSpan = sampleSpan serbianMixedScript allLTR
             let runs = spanToRuns inputSpan
             runs `shouldBe`
                 [ Run
                     -- TODO: We might want both parentheses in the same run.
                     { runOffsetInSpan = 0
                     , runText = pack "Vikipedija ("
-                    , runDirection = Just DirLTR
+                    , runLevel = 0
+                    , runDirection = DirLTR
                     , runScript = Just "Latn"
                     }
                 , Run
                     { runOffsetInSpan = 12
                     , runText = pack "Википедија)"
-                    , runDirection = Just DirLTR
+                    , runLevel = 0
+                    , runDirection = DirLTR
+                    , runScript = Just "Cyrl"
+                    }
+                ]
+        it "handles mixed direction with base LTR" $ do
+            let levels = levelsRLE [(7, 0), (3, 1), (6, 0)]
+            let inputSpan = sampleSpan (mixedDirectionSimple DirLTR) levels
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = pack "bahrain"
+                    , runLevel = 0
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                , Run
+                    { runOffsetInSpan = 7
+                    , runText = pack "مصر"
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                , Run
+                    { runOffsetInSpan = 13
+                    , runText = pack "kuwait"
+                    , runLevel = 0
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                ]
+        it "handles mixed direction with base RTL" $ do
+            let levels = levelsRLE [(7, 2), (3, 1), (6, 2)]
+            let inputSpan = sampleSpan (mixedDirectionSimple DirRTL) levels
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = pack "bahrain"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                , Run
+                    { runOffsetInSpan = 7
+                    , runText = pack "مصر"
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                , Run
+                    { runOffsetInSpan = 13
+                    , runText = pack "kuwait"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                ]
+        it "handles Arabic text with English inside" $ do
+            let levels = levelsRLE [(3, 1), (9, 2), (36, 1), (3, 2), (1, 1)]
+            let inputSpan = sampleSpan arabicAroundEnglish levels
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = pack "في "
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                , Run
+                    { runOffsetInSpan = 5
+                    , runText = pack "XHTML 1.0"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                , Run
+                    { runOffsetInSpan = 14
+                    , runText = pack " يتم تحقيق ذلك بإضافة العنصر المضمن "
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                , Run
+                    { runOffsetInSpan = 79
+                    , runText = pack "bdo"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                , Run
+                    { runOffsetInSpan = 82
+                    , runText = pack "."
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Zyyy"
+                    }
+                ]
+        it "handles English text with Arabic inside" $ do
+            let levels = levelsRLE [(13, 0), (18, 1), (11, 0)]
+            let inputSpan = sampleSpan englishAroundArabic levels
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = pack "The title is "
+                    , runLevel = 0
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                , Run
+                    { runOffsetInSpan = 13
+                    , runText = pack "مفتاح معايير الويب"
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Arab"
+                    }
+                , Run
+                    { runOffsetInSpan = 47
+                    , runText = pack " in Arabic."
+                    , runLevel = 0
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                ]
+        -- Unrealistic example where text changes direction
+        -- without changing script.
+        it "handles script and direction changes" $ do
+            let levels = levelsRLE [(4, 1), (4, 2), (8, 1), (4, 2), (3, 1)]
+            let inputSpan = sampleSpan serbianMixedScript levels
+            let runs = spanToRuns inputSpan
+            runs `shouldBe`
+                [ Run
+                    { runOffsetInSpan = 0
+                    , runText = pack "Viki"
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Latn"
+                    }
+                -- direction change
+                , Run
+                    { runOffsetInSpan = 4
+                    , runText = pack "pedi"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Latn"
+                    }
+                -- direction change
+                , Run
+                    { runOffsetInSpan = 8
+                    , runText = pack "ja ("
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Latn"
+                    }
+                -- script change
+                , Run
+                    { runOffsetInSpan = 12
+                    , runText = pack "Вики"
+                    , runLevel = 1
+                    , runDirection = DirRTL
+                    , runScript = Just "Cyrl"
+                    }
+                -- direction change
+                , Run
+                    { runOffsetInSpan = 20
+                    , runText = pack "педи"
+                    , runLevel = 2
+                    , runDirection = DirLTR
+                    , runScript = Just "Cyrl"
+                    }
+                -- direction change
+                , Run
+                    { runOffsetInSpan = 28
+                    , runText = pack "ја)"
+                    , runLevel = 1
+                    , runDirection = DirRTL
                     , runScript = Just "Cyrl"
                     }
                 ]
diff --git a/test/Data/Text/ParagraphLayout/Rich/ParagraphData.hs b/test/Data/Text/ParagraphLayout/Rich/ParagraphData.hs
--- a/test/Data/Text/ParagraphLayout/Rich/ParagraphData.hs
+++ b/test/Data/Text/ParagraphLayout/Rich/ParagraphData.hs
@@ -2,15 +2,18 @@
     ( hardBoxBreakLTRParagraph
     , hardBoxBreakRTLParagraph
     , loremIpsumParagraph
+    , mixedDirectionComplexParagraph
+    , mixedDirectionSimpleParagraph
     , mixedScriptParagraph
     , mixedSizesParagraph
     , nestedBoxesParagraph
+    , neutralDirectionParagraph
     , softBreakParagraph
     )
 where
 
 import Data.Int (Int32)
-import Data.Text (Text, pack)
+import Data.Text (Text, empty, pack)
 import Data.Text.Glyphize (Direction (DirLTR, DirRTL), Font)
 
 import Data.Text.ParagraphLayout.Internal.BoxOptions
@@ -18,6 +21,7 @@
 import Data.Text.ParagraphLayout.Internal.Rich.Paragraph
 import Data.Text.ParagraphLayout.Internal.TextOptions
 import Data.Text.ParagraphLayout.Internal.Tree
+import Data.Text.ParagraphLayout.TextData
 
 tLTR :: TextOptions
 tLTR = defaultTextOptions DirLTR
@@ -88,6 +92,47 @@
     where
         t = tLTR { textFont = font, textLanguage = "zxx" }
 
+-- | Edge case:
+-- Directionally neutral characters between box boundary and direction boundary.
+--
+-- In the case of a LTR paragraph, neutral characters should only be considered
+-- RTL when surrounded by RTL characters on both sides, otherwise they should
+-- be LTR.
+--
+-- In the case of a RTL paragraph, neutral characters should only be considered
+-- LTR when surrounded by LTR characters on both sides, otherwise they should
+-- be RTL.
+--
+-- Box boundaries should not, by default, affect the direction of neutral
+-- characters.
+mixedDirectionComplexParagraph :: Direction -> Font -> ParagraphOptions ->
+    Paragraph String
+mixedDirectionComplexParagraph dir font = constructParagraph
+    (pack "prefix")
+    (rootBox t
+        [ text "text1" " . > ab . > ba . > "
+        , box "box1" b_ t
+            [ text "text2" " . > اب . > با . > tu . > ut . > "
+            ]
+        , text "text3" " . > تو . > وت . > "
+        ]
+    )
+    (pack "suffix")
+    where
+        t = (defaultTextOptions dir) { textFont = font, textLanguage = "zxx" }
+
+-- | A paragraph that contains only strongly directional letters of mixed
+-- directions, no spaces or other neutral characters, for basic BiDi tests.
+mixedDirectionSimpleParagraph :: Direction -> Font -> ParagraphOptions ->
+    Paragraph String
+mixedDirectionSimpleParagraph dir font = constructParagraph
+    empty
+    (rootBox t [TextSequence "text" textContents])
+    empty
+    where
+        t = (defaultTextOptions dir) { textFont = font, textLanguage = lang }
+        (_, lang, textContents, _) = mixedDirectionSimple dir
+
 mixedScriptParagraph :: Font -> ParagraphOptions -> Paragraph String
 mixedScriptParagraph font = constructParagraph
     (pack "prefix")
@@ -149,6 +194,17 @@
     (pack "suffix")
     where
         t = tLTR { textFont = font, textLanguage = "en" }
+
+-- | A paragraph that contains only characters in the BiDi class
+-- /Other neutrals (ON)/. This includes characters that can be mirrored.
+neutralDirectionParagraph :: Direction -> Font -> ParagraphOptions ->
+    Paragraph String
+neutralDirectionParagraph dir font = constructParagraph
+    (pack "_»")
+    (rootBox t [text "text" "…―>"])
+    (pack "*†")
+    where
+        t = (defaultTextOptions dir) { textFont = font, textLanguage = "zxx" }
 
 softBreakParagraph :: Int32 -> Font -> ParagraphOptions -> Paragraph String
 softBreakParagraph spacing font = constructParagraph
diff --git a/test/Data/Text/ParagraphLayout/RichSpec.hs b/test/Data/Text/ParagraphLayout/RichSpec.hs
--- a/test/Data/Text/ParagraphLayout/RichSpec.hs
+++ b/test/Data/Text/ParagraphLayout/RichSpec.hs
@@ -1,12 +1,14 @@
 module Data.Text.ParagraphLayout.RichSpec (spec) where
 
 import Data.Int (Int32)
+import Data.Text.Glyphize (Direction (DirLTR, DirRTL))
 
 import Test.Hspec
 import System.FilePath ((</>))
 import Data.Text.ParagraphLayout
 import Data.Text.ParagraphLayout.FontLoader
 import Data.Text.ParagraphLayout.Internal.Paginable (paginateAll)
+import Data.Text.ParagraphLayout.Internal.Rich.ParagraphLayout (shapedRuns)
 import Data.Text.ParagraphLayout.PrettyShow
 import Data.Text.ParagraphLayout.PrettyShow.Golden
 import Data.Text.ParagraphLayout.Rect
@@ -120,6 +122,42 @@
                 let result = layoutRich input
                 result `shouldBeGolden` "hardBoxBreakRTL"
 
+            it "handles neutral characters in RTL paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = neutralDirectionParagraph DirRTL font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "neutralDirectionRTL"
+
+            it "handles neutral characters in LTR paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = neutralDirectionParagraph DirLTR font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "neutralDirectionLTR"
+
+            it "handles simple mixed direction in RTL paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = mixedDirectionSimpleParagraph DirRTL font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "mixedDirectionSimpleRTL"
+
+            it "handles simple mixed direction in LTR paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = mixedDirectionSimpleParagraph DirLTR font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "mixedDirectionSimpleLTR"
+
+            it "handles complex mixed direction in RTL paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = mixedDirectionComplexParagraph DirRTL font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "mixedDirectionComplexRTL"
+
+            it "handles complex mixed direction in LTR paragraph" $ do
+                let opts = defaultParagraphOptions
+                let input = mixedDirectionComplexParagraph DirLTR font opts
+                let result = layoutRich input
+                result `shouldBeGolden` "mixedDirectionComplexLTR"
+
     describe "paginate" $ do
         let
             goldenDir = ".golden" </> "paginatedRichParagraphLayout"
@@ -140,3 +178,30 @@
                         }
                 let pages = paginateAll popts pl
                 pages `shouldBeGolden` "loremIpsum20em"
+
+    describe "shaped runs for demo" $ do
+        let
+            goldenDir = ".golden" </> "shapedRuns"
+            shouldBeGolden = goldenTest goldenDir getShapedRuns ShapedRuns
+            -- | Test shaped runs against an expected value,
+            -- and write metadata about the used font afterwards.
+            shapedRunsSpecWithFont fontPath font subject name result = do
+                let infoPath = fontInfoPath goldenDir name
+                let writeInfo = writeFontInfo infoPath fontPath font
+                after_ writeInfo $ it subject $
+                    shapedRuns result `shouldBeGolden` name
+
+        describe "with Arabic font" $ do
+            let fontPath = arabicFont
+            font <- runIO $ loadFont fontPath 0 demoOptions
+
+            let
+                opts = defaultParagraphOptions
+                input = mixedDirectionComplexParagraph DirRTL font opts
+                result = layoutRich input
+                shapedRunsSpec = shapedRunsSpecWithFont fontPath font
+
+            shapedRunsSpec
+                "handles complex mixed direction in RTL paragraph"
+                "mixedDirectionComplexRTL"
+                result
diff --git a/test/Data/Text/ParagraphLayout/RunLengthEncoding.hs b/test/Data/Text/ParagraphLayout/RunLengthEncoding.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Text/ParagraphLayout/RunLengthEncoding.hs
@@ -0,0 +1,8 @@
+module Data.Text.ParagraphLayout.RunLengthEncoding (runLengthDecode) where
+
+-- | Decode a run-length encoded list.
+--
+-- Helpful for concisely representing lists with many repeated values.
+runLengthDecode :: [(Int, a)] -> [a]
+runLengthDecode [] = []
+runLengthDecode ((len, val) : xs) = replicate len val ++ runLengthDecode xs
diff --git a/test/Data/Text/ParagraphLayout/SpanData.hs b/test/Data/Text/ParagraphLayout/SpanData.hs
deleted file mode 100644
--- a/test/Data/Text/ParagraphLayout/SpanData.hs
+++ /dev/null
@@ -1,66 +0,0 @@
-module Data.Text.ParagraphLayout.SpanData
-    ( emptySpan
-    , czechHello
-    , serbianMixedScript
-    )
-where
-
-import Data.Text (pack)
-import Data.Text.Glyphize (Direction (DirLTR), Font)
-
-import Data.Text.ParagraphLayout.Internal.BoxOptions
-import Data.Text.ParagraphLayout.Internal.LineHeight
-import Data.Text.ParagraphLayout.Internal.ResolvedBox
-import Data.Text.ParagraphLayout.Internal.ResolvedSpan
-import Data.Text.ParagraphLayout.Internal.TextOptions
-
-defaultBox :: Direction -> ResolvedBox ()
-defaultBox dir = ResolvedBox () 0 defaultBoxOptions dir
-
-emptySpan :: Font -> ResolvedSpan ()
-emptySpan font = ResolvedSpan
-    { spanUserData = ()
-    , spanIndex = 0
-    , spanOffsetInParagraph = 0
-    , spanText = pack ""
-    , spanTextOptions = (defaultTextOptions DirLTR)
-        { textFont = font
-        , textLineHeight = Normal
-        , textLanguage = "en"
-        }
-    , spanBoxes = [defaultBox DirLTR]
-    , spanLineBreaks = []
-    , spanCharacterBreaks = []
-    }
-
-czechHello :: Font -> ResolvedSpan ()
-czechHello font = ResolvedSpan
-    { spanUserData = ()
-    , spanIndex = 0
-    , spanOffsetInParagraph = 0
-    , spanText = pack "Ahoj, světe!"
-    , spanTextOptions = (defaultTextOptions DirLTR)
-        { textFont = font
-        , textLineHeight = Normal
-        , textLanguage = "cs"
-        }
-    , spanBoxes = [defaultBox DirLTR]
-    , spanLineBreaks = []
-    , spanCharacterBreaks = []
-    }
-
-serbianMixedScript :: Font -> ResolvedSpan ()
-serbianMixedScript font = ResolvedSpan
-    { spanUserData = ()
-    , spanIndex = 0
-    , spanOffsetInParagraph = 0
-    , spanText = pack "Vikipedija (Википедија)"
-    , spanTextOptions = (defaultTextOptions DirLTR)
-        { textFont = font
-        , textLineHeight = Normal
-        , textLanguage = "sr"
-        }
-    , spanBoxes = [defaultBox DirLTR]
-    , spanLineBreaks = []
-    , spanCharacterBreaks = []
-    }
diff --git a/test/Data/Text/ParagraphLayout/TextData.hs b/test/Data/Text/ParagraphLayout/TextData.hs
new file mode 100644
--- /dev/null
+++ b/test/Data/Text/ParagraphLayout/TextData.hs
@@ -0,0 +1,110 @@
+module Data.Text.ParagraphLayout.TextData
+    ( arabicEmpty
+    , englishEmpty
+    , englishWord
+    , arabicHello
+    , czechHello
+    , serbianMixedScript
+    , mixedDirectionNumbers
+    , mixedDirectionSimple
+    , arabicAroundEnglish
+    , englishAroundArabic
+    )
+where
+
+import Data.Text (Text, empty, pack)
+import Data.Text.Glyphize (Direction (DirLTR, DirRTL))
+
+type Language = String
+type DirectionRunLengths = [Int]
+type Sample = (Direction, Language, Text, DirectionRunLengths)
+
+arabicEmpty :: Sample
+arabicEmpty =
+    ( DirRTL
+    , "ar"
+    , empty
+    , []
+    )
+
+englishEmpty :: Sample
+englishEmpty =
+    ( DirLTR
+    , "en"
+    , empty
+    , []
+    )
+
+englishWord :: Sample
+englishWord =
+    ( DirRTL
+    , "en"
+    , pack "word"
+    , [4]
+    )
+
+arabicHello :: Sample
+arabicHello =
+    ( DirRTL
+    , "ar"
+    , pack "سلام"
+    , [4]
+    )
+
+czechHello :: Sample
+czechHello =
+    ( DirLTR
+    , "cs"
+    , pack "Ahoj, světe!"
+    , [12]
+    )
+
+serbianMixedScript :: Sample
+serbianMixedScript =
+    ( DirLTR
+    , "sr"
+    , pack "Vikipedija (Википедија)"
+    , [23]
+    )
+
+-- | Source:
+-- <https://www.w3.org/International/articles/inline-bidi-markup/uba-basics>
+mixedDirectionNumbers :: Sample
+mixedDirectionNumbers =
+    ( DirLTR
+    , "mul"
+    , pack "one two ثلاثة 1234 خمسة"
+    , [8, 6, 4, 5]
+    )
+
+-- | Source:
+-- <https://www.w3.org/International/articles/inline-bidi-markup/uba-basics>
+--
+-- Used to test the effect of base direction.
+mixedDirectionSimple :: Direction -> Sample
+mixedDirectionSimple dir =
+    ( dir
+    , "ar"
+    , pack "bahrainمصرkuwait"
+    , [7, 3, 6]
+    )
+
+-- | Source:
+-- <https://www.w3.org/International/articles/inline-bidi-markup/uba-basics>
+arabicAroundEnglish :: Sample
+arabicAroundEnglish =
+    ( DirRTL
+    , "ar"
+    , pack "في XHTML 1.0 يتم تحقيق ذلك بإضافة العنصر المضمن bdo."
+    , [3, 9, 36, 3, 1]
+    )
+
+-- | Source:
+-- <https://www.w3.org/International/articles/inline-bidi-markup/uba-basics>
+englishAroundArabic :: Sample
+englishAroundArabic =
+    ( DirLTR
+    , "en"
+    , pack "The title is مفتاح معايير الويب in Arabic."
+    , [13, 18, 11]
+    )
