bidi

package
v0.14.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package bidi lays out right-to-left text for terminals that cannot. Most of them - kitty, Ghostty, Alacritty, WezTerm, foot - draw cells strictly left to right and never join Arabic-script letters, so Persian, Arabic or Hebrew typed into nem shows backwards and, for the Arabic script, as a string of isolated letters. Emacs gets round this by doing the Unicode Bidirectional Algorithm itself and drawing text in visual order; nem does the same, and this is the algorithm.

The renderer asks HasRTL of every line, which costs next to nothing for text with no right-to-left in it. For a line that has some it takes the line's direction from ParagraphDirection, resolves an embedding level for every rune with Levels, groups the runes into grapheme clusters each given the level of its first rune, and draws the clusters in the order Order gives, the Mirror of a bracket at an odd level in its place, and the letters as Shape joins them.

It implements UAX #9 for one paragraph at a time - nem takes each line as its own paragraph - through rule L2, and checks itself against Unicode's conformance data. What it leaves to the renderer is what UAX #9 leaves to one: which glyph to draw (L4, through Mirror) and keeping combining marks with their base (L3, which clustering does).

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func HasRTL

func HasRTL(rs []rune) bool

HasRTL reports whether rs holds any strong right-to-left character (bidi class R or AL) - the cheap check the renderer makes on every line so text without any costs nothing. It also reports an explicit right-to-left embedding, override or isolate (RLE, RLO, RLI), which turn even left-to-right letters around.

Everything below U+0590 is passed over with one comparison, and the character database is only asked about the ranges that hold right-to-left scripts.

func Levels

func Levels(rs []rune, dir Direction) []uint8

Levels resolves the embedding level of every rune of one paragraph - nem treats each line as its own paragraph - laid out with base direction dir. It applies X1–X10 (explicit embeddings, overrides and isolates: LRE RLE LRO RLO PDF LRI RLI FSI PDI), W1–W7, N0 (bracket pairs), N1–N2, I1–I2 and L1 (segment separators, and whitespace/isolate formatting characters before them and at the end of the line, reset to the paragraph level). Characters removed by X9 (BN and the explicit embedding controls) get the level of the character before them (or the paragraph level at the start), so len(result) == len(rs) and every index is usable.

A paragraph separator inside rs does not start a new paragraph: it is given the paragraph level, and ends the whitespace before it for L1, and that is all.

func Mirror

func Mirror(r rune) (rune, bool)

Mirror returns the mirrored glyph for r (UAX #9 L4, Unicode's Bidi_Mirroring_Glyph) - '(' for ')', '<' for '>', '«' for '»' and so on - and whether it has one. The renderer draws the mirror for a mirrored character at an odd (right-to-left) level. A character with no mirror comes back as it is.

func Order

func Order(levels []uint8) []int

Order returns the visual order for items with the given levels (UAX #9 L2): out[v] is the index of the item drawn at visual position v, left to right. Items are whatever the caller levelled - runes, or grapheme clusters each given the level of its first rune.

It reverses every run of items at or above each level, from the highest down to the lowest odd one, so text at an odd level reads right to left and a number or a Latin word inside it, two levels up, reads left to right again.

Example
rs := []rune("سال 2024")
levels := Levels(rs, ParagraphDirection(rs))
for _, i := range Order(levels) {
	fmt.Printf("%c", rs[i])
}
fmt.Println()
Output:
2024 لاس

func Shape

func Shape(rs []rune) []rune

Shape returns rs with each Arabic-script letter replaced by its contextual presentation form - isolated, initial, medial or final - from the Arabic Presentation Forms-A and -B blocks, one rune for one rune so every index is unchanged. rs is in logical order. Letters with no presentation form, and everything that is not Arabic script, are returned unchanged.

A letter joins the one before it when that one reaches forward - it is dual-joining, left-joining or a tatweel or ZWJ - and joins the one after it when it reaches forward itself and that one reaches back. Harakat and other marks between them are passed over and left as they are; a ZWNJ, as in "می‌خواهم", keeps the letters either side of it apart. Lam and alef stay two letters: their ligature is one character, and would take one cell for two runes.

The result is a new slice; rs is not changed.

Types

type Direction

type Direction int

Direction is a paragraph's (or run's) direction.

const (
	LTR Direction = iota
	RTL
)

func ParagraphDirection

func ParagraphDirection(rs []rune) Direction

ParagraphDirection is the direction of the first strong character (UAX #9 P2/P3, skipping characters between an isolate initiator and its matching PDI), LTR when there is none.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL