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