src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }