text
Text.
The algorithms that decide what order text is drawn in and which glyph a letter becomes — the parts of text that are decisions rather than pixels.
They exist for std/canvas, and only for it. A browser laying out a document does all of this itself, so a Kite program writing into the DOM never reaches here; a program painting into a <canvas> places every glyph itself and has nobody to ask. Computing it in Kite rather than at the boundary is what makes the answer the same wherever the program runs.
Everything in this module is a named subset of a Unicode algorithm: what is implemented is stated, what is not is stated, and nothing is implied.
bidi_runsis UAX #9, the bidirectional algorithm: rules P2–P3, X1–X10,
W1–W7, N0–N2 (bracket pairs included), I1–I2 and L1–L2. What it does not do: L3 (combining marks at a direction boundary keep their place) and L4 (mirroring) are left to the renderer, which mirrors inside a single-direction run on its own; explicit overrides survive resolution but a renderer that re-derives a run's direction from its first strong character — which is what both of ours do — will not honour an override that contradicts the letters. Higher-level protocols (HL1–HL6) are not implemented at all.
break_opportunitiesis UAX #14, the line breaking algorithm: rules
LB1–LB31 over a Line_Break class table. Two departures, both of which the annex itself names: SA (Thai, Lao, Khmer, and Burmese syllable breaking) is treated as AL, which §5.1 prescribes in the absence of a dictionary, and CB is not implemented because nothing can be embedded in a run of Kite text to break around.
join_arabicis the joining half of Arabic shaping: Joining_Type over
the Arabic block's own letters, mapped to Arabic Presentation Forms-B, with the mandatory lam-alef ligature. It is not HarfBuzz-quality shaping, which is OpenType GSUB/GPOS — a font's own substitution and positioning programs — and cannot be written against a boundary that only measures. A real shaping engine would additionally reorder Indic clusters for Devanagari, position Thai marks above and below their base, and reorder Burmese medial consonants; none of that happens here, and a program drawing those scripts is relying on the host's font stack to do it — which a browser drawing into a <canvas> largely does, and a glyph-at-a-time renderer does not.
BidiClassbidi_classis_combiningparagraph_levelRunbidi_runsbidi_runs_withbidi_levelsbidi_visualjoin_arabicLineBreakline_break_classbreak_opportunities
BidiClass
enum BidiClass
A code point's part in the bidirectional algorithm, from UAX #9 table 4.
LRALENESETANCSNSMBNBSWSONLRERLELRORLOPDFLRIRLIFSIPDI
bidi_class
pub fn bidi_class(code: int) -> BidiClass
The Bidi_Class of a code point, from a compact range table.
The table is a subset of the Unicode character database, and the subset is listed rather than implied. It covers: the C0 and C1 controls; ASCII and Latin-1; Latin Extended-A and -B (0100–024F); the combining diacritics (0300–036F); Greek (0370–03FF); Cyrillic with its combining marks (0400–04FF); Hebrew (0590–05FF) and its presentation forms (FB1D–FB4F); the Arabic block (0600–06FF) with its digits, points and signs, the Arabic Supplement (0750–077F), Arabic Extended-B (0870–089F), Arabic Extended-A (08A0–08FF), and both Arabic presentation-forms blocks (FB50–FDFF, FE70–FEFF); the combining marks of Devanagari, Thai and Myanmar (whose letters are already L); general punctuation (2000–206F) including every explicit bidi formatting character; currency signs (20A0–20CF); the arrow, mathematical and symbol blocks (2190–2BFF) and supplemental punctuation (2E00–2E7F) as ON; CJK punctuation and brackets (3000–303F); the variation selectors and combining half marks (FE00–FE2F); the small form variants (FE50–FE6F) as ON; and the fullwidth forms (FF01–FF65).
Anything outside these ranges is treated as L. For CJK, Hangul, kana, and the letters of Devanagari, Thai and Myanmar that is the correct answer; for Syriac, Thaana and N'Ko it is wrong and unclaimed; for emoji — which are ON, and which sit outside the Basic Multilingual Plane — it makes a symbol behave like a letter, which matters only when one sits between two runs of opposite direction. Within the covered Arabic and Greek blocks a handful of signs are coarser than the character database: they are named below where they are folded.
is_combining
pub fn is_combining(code: int) -> bool
Whether a code point is a nonspacing mark — Mn, the class the bidi table calls NSM. A mark advances nothing: it draws over the letter before it, so it is transparent to joining and takes its direction from its base.
paragraph_level
pub fn paragraph_level(body: str) -> int
The paragraph embedding level — rules P2 and P3.
The first strong character decides: L means 0, R or AL means 1, and a paragraph with no strong character at all reads left to right. Characters inside an isolate are skipped, because an isolate is exactly a promise that its content does not influence its surroundings.
This is asked once per paragraph and applied to every line wrapped out of it — a line does not re-decide its direction, or the second line of a Hebrew paragraph beginning with a number would come out backwards.
Run
struct Run
One single-direction stretch of a reordered line, in the order it is painted, left to right.
body is in logical order — the order the text was written in — and a renderer displays a single-direction run correctly on its own: a browser re-derives the run's direction from its first strong character, which is rule P2 again, and mirrors brackets inside it, which is rule L4. A run of neutrals alone at an odd level has no strong character to re-derive from, so it arrives already reversed and mirrored, with rtl false.
pub body: strpub rtl: bool
bidi_runs
pub fn bidi_runs(line: str) -> [Run]
A line reordered for display — the whole of UAX #9 this module implements, applied: P2–P3 for the base direction, X1–X10 for the explicit formatting characters, W1–W7 for the weak types, N0–N2 for the neutrals and bracket pairs, I1–I2 for the levels, and L1–L2 for the final order.
The runs come out in visual order and are drawn left to right; explicit formatting characters are dropped from the bodies, because they were instructions and have been obeyed. An RTL paragraph is not right-aligned by this — alignment belongs to the box, and the box speaks Justify, so a right-aligned Hebrew paragraph is a Justify.End box.
bidi_runs_with
pub fn bidi_runs_with(line: str, base: int) -> [Run]
bidi_runs with the paragraph level supplied, for a line wrapped out of a longer paragraph — the paragraph decides once, the lines inherit.
bidi_levels
pub fn bidi_levels(line: str) -> [int]
The resolved embedding level of every character, after L1. Exposed for tests: a level is the algorithm's whole intermediate state in one number.
bidi_visual
pub fn bidi_visual(line: str) -> [int]
The character indices of a line in visual order, leftmost first — rule L2 applied at character granularity. Characters that are not drawn — the explicit formatting characters — are absent.
join_arabic
pub fn join_arabic(run: str) -> str
Arabic contextual joining: each letter of the Arabic block becomes the presentation form its neighbours demand — isolated, final, initial or medial — and lam followed by alef becomes the mandatory ligature.
This is the Joining_Type algorithm over the table above, and it is not shaping: a font's own GSUB rules choose better glyphs than the presentation-forms block offers, kern them, and position their marks. The a canvas asked to draw a whole run through fillText gets all of that from the host and never needs this function. What needs it is anything drawing one glyph at a time — a glyph atlas cannot cache a letter whose shape depends on its neighbours, and can cache a presentation form, whose shape is the point of it.
Marks are transparent: a fatha or shadda between two letters neither breaks their joining nor is itself changed. Letters outside the table — Persian, Urdu, anything beyond the block's own alphabet — pass through unshaped and do not join, which is stated rather than guessed at.
LineBreak
enum LineBreak
The Line_Break classes this module distinguishes.
BK— Mandatory break: newline and friends.CR— Carriage return, line feed — sequenced by LB5.LFNL— Next line, U+0085.SP— Space.ZW— Zero-width space: a break opportunity that shows nothing.WJ— Word joiner and friends: never break either side.GL— Non-breaking glue: a no-break space, a narrow no-break space.CM— Combining mark: takes the class of what it attaches to.OP— Opening punctuation — no break after.CL— Closing punctuation — no break before.CP— Closing parenthesis, which LB30 treats slightly differently from CL.QU— Quotation mark — no break either side.EX— Exclamation and interrogation — no break before.IS— Infix separator: a comma or a full stop inside a number.SY— Symbol allowing a break after: the solidus.PR— Numeric prefix: a currency sign.PO— Numeric postfix: a percent sign, a degree sign.HY— Hyphen-minus.BA— Break after: an en dash, a slash-like separator.BB— Break before.B2— Em dash: breaks either side.NS— Non-starter: a small kana, a middle dot.ID— Ideographic: CJK, which breaks between almost any two.NU— Numeric.AL— Alphabetic — the default, and what an unknown code point becomes.H2— Hangul syllable and jamo classes, which LB26/LB27 keep together.H3JLJVJT
line_break_class
pub fn line_break_class(code: int) -> LineBreak
The Line_Break class of a code point.
Range checks rather than a lookup table, for the reason bidi_class uses them: a table of every assigned code point is a megabyte, and the ranges that matter are few enough to read.
break_opportunities
pub fn break_opportunities(body: str) -> [bool]
Where a line may break.
Answers one flag per character: whether a break is allowed before it. Position 0 is always false — UAX #14 rule LB2, never break at the start — and a mandatory break is reported as an allowed one, because a caller already knows a \n ends a line and needs to be told the rest.
The classes are resolved first, then the pairs. Doing it in two passes is what makes LB9 — a combining mark takes the class of what it attaches to — a single loop rather than a special case at every rule.