main bcc9a45

levels.go RawHistoryFolder

1package bidi
2
3import (
4	"fmt"
5	"slices"
6)
7
8// BaseDirection selects the paragraph embedding level. Auto follows P2/P3.
9// nefergui: expose the existing upstream paragraph algorithm's resolved levels.
10type BaseDirection uint8
11
12const (
13	Auto BaseDirection = iota
14	LTR
15	RTL
16)
17
18// Levels returns resolved embedding levels after L1, one per input rune.
19// lineBreaks contains strictly increasing, nonempty, rune offsets, ending in len(text).
20// Removed X9 formatting codes receive a convenience level; call Reorder with
21// the same text to omit those codes from the visual indices.
22// nefergui: adapt upstream newParagraph/getLevels for per-line layout consumers.
23func Levels(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, error) {
24	levels, _, err := Resolve(text, base, lineBreaks)
25	return levels, err
26}
27
28// Resolve returns the paragraph base level and the resolved levels after L1.
29// nefergui: surface existing levels without copying or reclassifying UAX#9 classes.
30func Resolve(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, uint8, error) {
31	return ResolveInto(nil, text, base, lineBreaks)
32}
33
34// ResolveInto is Resolve reusing dst's backing array for left-to-right-only
35// text, so it does not allocate. Text that needs the full UAX#9 algorithm
36// always gets a newly allocated slice; callers keep the returned slice.
37func ResolveInto(dst []uint8, text []rune, base BaseDirection, lineBreaks []int) ([]uint8, uint8, error) {
38	if base > RTL {
39		return nil, 0, fmt.Errorf("invalid bidi base %d", base)
40	}
41	if len(text) == 0 {
42		if len(lineBreaks) != 0 {
43			return nil, 0, fmt.Errorf("breaks on empty paragraph")
44		}
45		if base == RTL {
46			return nil, 1, nil
47		}
48		return nil, 0, nil
49	}
50	if len(lineBreaks) == 0 || lineBreaks[len(lineBreaks)-1] != len(text) {
51		return nil, 0, fmt.Errorf("line breaks must end at paragraph length")
52	}
53	prev := 0
54	for _, end := range lineBreaks {
55		if end <= prev || end > len(text) {
56			return nil, 0, fmt.Errorf("invalid line break %d", end)
57		}
58		prev = end
59	}
60	if base != RTL && leftToRightOnly(text) {
61		dst = slices.Grow(dst[:0], len(text))[:len(text)]
62		clear(dst)
63		return dst, 0, nil
64	}
65	return resolveFull(text, base, lineBreaks)
66}
67
68// resolveFull runs the complete UAX#9 algorithm on validated input.
69func resolveFull(text []rune, base BaseDirection, lineBreaks []int) ([]uint8, uint8, error) {
70	classes := make([]Class, len(text))
71	pairTypes := make([]bracketType, len(text))
72	pairValues := make([]rune, len(text))
73	for i, r := range text {
74		prop, _ := LookupRune(r)
75		classes[i] = prop.Class()
76		switch {
77		case prop.IsOpeningBracket():
78			pairTypes[i] = bpOpen
79			pairValues[i] = r
80			// nefergui: U+2329/232A are canonically equivalent to U+3008/3009.
81			if r == 0x2329 {
82				pairValues[i] = 0x3008
83			}
84		case prop.IsBracket():
85			pairTypes[i] = bpClose
86			// nefergui: canonicalize close brackets to the opening identifier
87			// required by upstream bracketPairer.matchOpener.
88			pairValues[i] = prop.reverseBracket(r)
89			if r == 0x232a {
90				pairValues[i] = 0x3008
91			}
92		}
93	}
94	embedding := implicitLevel
95	if base == LTR {
96		embedding = 0
97	} else if base == RTL {
98		embedding = 1
99	}
100	p, err := newParagraph(classes, pairTypes, pairValues, embedding)
101	if err != nil {
102		return nil, 0, err
103	}
104	// nefergui: exercise the upstream L2 entry point as a differential oracle;
105	// levels below are the same L1 result used by that entry point.
106	_ = p.getReordering(lineBreaks)
107	raw := p.getLevels(lineBreaks)
108	out := make([]uint8, len(raw))
109	for i, l := range raw {
110		out[i] = uint8(l)
111	}
112	return out, uint8(p.embeddingLevel), nil
113}
114
115// leftToRightOnly reports whether every rune resolves to level 0 in a level-0
116// paragraph: with no R, AL, AN, explicit formatting or BN, P2/P3 pick level 0,
117// W7 turns EN into L (sos is L), neutrals resolve to L and I1 raises nothing.
118// B is left to the full algorithm, which validates its position.
119func leftToRightOnly(text []rune) bool {
120	for _, r := range text {
121		if r < 0x80 && r >= 0x20 && r != 0x7f {
122			continue // printable ASCII: L, EN, ES, ET, CS, WS or ON
123		}
124		prop, _ := LookupRune(r)
125		switch prop.Class() {
126		case R, AL, AN, B, BN, LRO, RLO, LRE, RLE, PDF, LRI, RLI, FSI, PDI:
127			return false
128		}
129	}
130	return true
131}
132
133// Reorder returns a visual-to-logical rune map per line using L2; X9-removed
134// formatting characters are excluded from the result. Levels must be the
135// per-line L1 output of Resolve. Consumers may group neighboring indices into
136// shaped runs while preserving the shaper's glyph order within each run.
137// nefergui: reuse upstream multiline L2 rather than infer levels from parity.
138func Reorder(text []rune, levels []uint8, breaks []int) ([][]int, error) {
139	if len(text) != len(levels) {
140		return nil, fmt.Errorf("level count mismatch")
141	}
142	if len(text) == 0 {
143		return nil, nil
144	}
145	if len(breaks) == 0 || breaks[len(breaks)-1] != len(text) {
146		return nil, fmt.Errorf("invalid breaks")
147	}
148	var out [][]int
149	start := 0
150	for _, end := range breaks {
151		if end <= start || end > len(text) {
152			return nil, fmt.Errorf("invalid break %d", end)
153		}
154		lv := make([]level, end-start)
155		for i, v := range levels[start:end] {
156			lv[i] = level(v)
157		}
158		// nefergui: call upstream's multiline L2 helper to keep its code live.
159		order := computeMultilineReordering(lv, []int{len(lv)})
160		line := make([]int, 0, len(order))
161		for _, i := range order {
162			r := text[start+i]
163			cls, _ := LookupRune(r)
164			if !isRemovedByX9(cls.Class()) {
165				line = append(line, start+i)
166			}
167		}
168		out = append(out, line)
169		start = end
170	}
171	return out, nil
172}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close