splitfunc.go 5.0 KB

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