Kite

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.

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.

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.

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.

BidiClass

enum BidiClass

A code point's part in the bidirectional algorithm, from UAX #9 table 4.

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.

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.

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.