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 classes := make([]Class, len(text))
51 pairTypes := make([]bracketType, len(text))
52 pairValues := make([]rune, len(text))
53 for i, r := range text {
54 prop, _ := LookupRune(r)
55 classes[i] = prop.Class()
56 switch {
57 case prop.IsOpeningBracket():
58 pairTypes[i] = bpOpen
59 pairValues[i] = r
60 // nefergui: U+2329/232A are canonically equivalent to U+3008/3009.
61 if r == 0x2329 {
62 pairValues[i] = 0x3008
63 }
64 case prop.IsBracket():
65 pairTypes[i] = bpClose
66 // nefergui: canonicalize close brackets to the opening identifier
67 // required by upstream bracketPairer.matchOpener.
68 pairValues[i] = prop.reverseBracket(r)
69 if r == 0x232a {
70 pairValues[i] = 0x3008
71 }
72 }
73 }
74 embedding := implicitLevel
75 if base == LTR {
76 embedding = 0
77 } else if base == RTL {
78 embedding = 1
79 }
80 p, err := newParagraph(classes, pairTypes, pairValues, embedding)
81 if err != nil {
82 return nil, 0, err
83 }
84 // nefergui: exercise the upstream L2 entry point as a differential oracle;
85 // levels below are the same L1 result used by that entry point.
86 _ = p.getReordering(lineBreaks)
87 raw := p.getLevels(lineBreaks)
88 out := make([]uint8, len(raw))
89 for i, l := range raw {
90 out[i] = uint8(l)
91 }
92 return out, uint8(p.embeddingLevel), nil
93}
94
95// Reorder returns a visual-to-logical rune map per line using L2; X9-removed
96// formatting characters are excluded from the result. Levels must be the
97// per-line L1 output of Resolve. Consumers may group neighboring indices into
98// shaped runs while preserving the shaper's glyph order within each run.
99// nefergui: reuse upstream multiline L2 rather than infer levels from parity.
100func Reorder(text []rune, levels []uint8, breaks []int) ([][]int, error) {
101 if len(text) != len(levels) {
102 return nil, fmt.Errorf("level count mismatch")
103 }
104 if len(text) == 0 {
105 return nil, nil
106 }
107 if len(breaks) == 0 || breaks[len(breaks)-1] != len(text) {
108 return nil, fmt.Errorf("invalid breaks")
109 }
110 var out [][]int
111 start := 0
112 for _, end := range breaks {
113 if end <= start || end > len(text) {
114 return nil, fmt.Errorf("invalid break %d", end)
115 }
116 lv := make([]level, end-start)
117 for i, v := range levels[start:end] {
118 lv[i] = level(v)
119 }
120 // nefergui: call upstream's multiline L2 helper to keep its code live.
121 order := computeMultilineReordering(lv, []int{len(lv)})
122 line := make([]int, 0, len(order))
123 for _, i := range order {
124 r := text[start+i]
125 cls, _ := LookupRune(r)
126 if !isRemovedByX9(cls.Class()) {
127 line = append(line, start+i)
128 }
129 }
130 out = append(out, line)
131 start = end
132 }
133 return out, nil
134}