splitfunc.go (5073B)
1 package graphemes 2 3 import ( 4 "bufio" 5 ) 6 7 // is determines if lookup intersects propert(ies) 8 func (lookup property) is(properties property) bool { 9 return (lookup & properties) != 0 10 } 11 12 const _Ignore = _Extend 13 14 // incbState tracks state for GB9c rule (Indic conjunct clusters) 15 // Pattern: Consonant (Extend|Linker)* Linker (Extend|Linker)* × Consonant 16 type incbState int 17 18 const ( 19 incbNone incbState = iota // initial/reset 20 incbConsonant // seen Consonant, awaiting Linker 21 incbLinker // seen Consonant and Linker (conjunct ready) 22 ) 23 24 // SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner. 25 // 26 // See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries. 27 var SplitFunc bufio.SplitFunc = splitFunc[[]byte] 28 29 func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, err error) { 30 var empty T 31 if len(data) == 0 { 32 return 0, empty, nil 33 } 34 35 // These vars are stateful across loop iterations 36 var pos int 37 var lastExIgnore property = 0 // "last excluding ignored categories" 38 var lastLastExIgnore property = 0 // "last one before that" 39 var regionalIndicatorCount int 40 41 // GB9c state: tracking Indic conjunct clusters 42 var incb incbState 43 44 // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property 45 // to the right of the ×, from which we look back or forward 46 47 current, w := lookup(data[pos:]) 48 if w == 0 { 49 if !atEOF { 50 // Rune extends past current data, request more 51 return 0, empty, nil 52 } 53 pos = len(data) 54 return pos, data[:pos], nil 55 } 56 57 // https://unicode.org/reports/tr29/#GB1 58 // Start of text always advances 59 pos += w 60 61 for { 62 eot := pos == len(data) // "end of text" 63 64 if eot { 65 if !atEOF { 66 // Token extends past current data, request more 67 return 0, empty, nil 68 } 69 70 // https://unicode.org/reports/tr29/#GB2 71 break 72 } 73 74 /* 75 We've switched the evaluation order of GB1↓ and GB2↑. It's ok: 76 because we've checked for len(data) at the top of this function, 77 sot and eot are mutually exclusive, order doesn't matter. 78 */ 79 80 // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property 81 // to the right of the ×, from which we look back or forward 82 83 // Remember previous properties to avoid lookups/lookbacks 84 last := current 85 if !last.is(_Ignore) { 86 lastLastExIgnore = lastExIgnore 87 lastExIgnore = last 88 } 89 90 // Update GB9c state based on what we just advanced past 91 if last.is(_InCBConsonant | _InCBLinker | _InCBExtend) { 92 switch { 93 case last.is(_InCBConsonant): 94 if incb != incbLinker { 95 incb = incbConsonant 96 } 97 case last.is(_InCBLinker): 98 if incb >= incbConsonant { 99 incb = incbLinker 100 } 101 // case last.is(_InCBExtend): stay in current state 102 } 103 } else { 104 incb = incbNone 105 } 106 107 current, w = lookup(data[pos:]) 108 if w == 0 { 109 if atEOF { 110 // Just return the bytes, we can't do anything with them 111 pos = len(data) 112 break 113 } 114 // Rune extends past current data, request more 115 return 0, empty, nil 116 } 117 118 // Optimization: no rule can possibly apply 119 if current|last == 0 { // i.e. both are zero 120 break 121 } 122 123 // https://unicode.org/reports/tr29/#GB3 124 if current.is(_LF) && last.is(_CR) { 125 pos += w 126 continue 127 } 128 129 // https://unicode.org/reports/tr29/#GB4 130 // https://unicode.org/reports/tr29/#GB5 131 if (current | last).is(_Control | _CR | _LF) { 132 break 133 } 134 135 // https://unicode.org/reports/tr29/#GB6 136 if current.is(_L|_V|_LV|_LVT) && last.is(_L) { 137 pos += w 138 continue 139 } 140 141 // https://unicode.org/reports/tr29/#GB7 142 if current.is(_V|_T) && last.is(_LV|_V) { 143 pos += w 144 continue 145 } 146 147 // https://unicode.org/reports/tr29/#GB8 148 if current.is(_T) && last.is(_LVT|_T) { 149 pos += w 150 continue 151 } 152 153 // https://unicode.org/reports/tr29/#GB9 154 if current.is(_Extend | _ZWJ) { 155 pos += w 156 continue 157 } 158 159 // https://unicode.org/reports/tr29/#GB9a 160 if current.is(_SpacingMark) { 161 pos += w 162 continue 163 } 164 165 // https://unicode.org/reports/tr29/#GB9b 166 if last.is(_Prepend) { 167 pos += w 168 continue 169 } 170 171 // https://unicode.org/reports/tr29/#GB9c 172 // Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker. 173 if incb == incbLinker && current.is(_InCBConsonant) { 174 // After matching the pattern, reset state to start tracking a new pattern 175 // The current Consonant becomes the start of the new pattern 176 incb = incbConsonant 177 pos += w 178 continue 179 } 180 181 // https://unicode.org/reports/tr29/#GB11 182 if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) { 183 pos += w 184 continue 185 } 186 187 // https://unicode.org/reports/tr29/#GB12 188 // https://unicode.org/reports/tr29/#GB13 189 if (current & last).is(_RegionalIndicator) { 190 regionalIndicatorCount++ 191 192 odd := regionalIndicatorCount%2 == 1 193 if odd { 194 pos += w 195 continue 196 } 197 } 198 199 // If we fall through all the above rules, it's a grapheme cluster break 200 break 201 } 202 203 // Return token 204 return pos, data[:pos], nil 205 }