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}