Source File
segmenter.go
Belonging Package
github.com/go-text/typesetting/segmenter
// SPDX-License-Identifier: Unlicense OR BSD-3-Clause// Package segmenter implements Unicode rules used// to segment a paragraph of text according to several criteria.// In particular, it provides a way of delimiting line break opportunities.//// The API of the package follows the very nice iterator pattern proposed// in github.com/npillmayer/uax,// but use a somewhat simpler internal implementation, inspired by Pango.//// The reference documentation is at https://unicode.org/reports/tr14// and https://unicode.org/reports/tr29.package segmenterimport (ucd)// breakAttr is a flag storing the break properties between two runes of// the input text.type breakAttr uint8const (lineBoundary breakAttr = 1 << iotamandatoryLineBoundary // implies LineBoundary// graphemeBoundary is on if the cursor can appear in front of a character,// i.e. if we are at a grapheme boundary.graphemeBoundary// wordBoundary is on if we are at the beginning or end of a word.//// To actually detect words, you should also look for runes// with the [Alphabetic] property, or with a General_Category of Number.//// See also https://unicode.org/reports/tr29/#Word_Boundary_Rules,// http://unicode.org/reports/tr44/#Alphabetic and// http://unicode.org/reports/tr44/#General_Category_ValueswordBoundary)const paragraphSeparator rune = 0x2029// cursor holds the information for the current index// processed by `computeAttributes`, that is// the context provided by previous and next runes in the texttype cursor struct {index, len int // index of the current rune and text length, used for sot and eotisPreviousSot bool // are we after sot, applying LB9 and LB10 ?prevPrev rune // the rune at index i-2prev rune // the rune at index i-1r rune // the rune at index inext rune // the rune at index i+1// is r included in `ucd.Extended_Pictographic`,// cached for efficiencyisExtentedPic bool// the following fields persists across iterationsprevGeneralCategory ucd.GeneralCategory // for prevgeneralCategory ucd.GeneralCategory // for rprevGrapheme ucd.GraphemeBreak // the Grapheme Break property at index i-1grapheme ucd.GraphemeBreak // the Grapheme Break property at index i// true if the `prev` rune was an odd Regional_Indicator, false if it was even or not an RI// used for rules GB12 and GB13// see [updateGraphemeRIOdd]isPrevGraphemeRIOdd boolprevPrevWord ucd.WordBreak // the Word Break property at the previous previous, non Extend runeprevWord ucd.WordBreak // the Word Break property at the previous, non Extend runeword ucd.WordBreak // the Word Break property at index iprevWordNoExtend int // the index of the last rune NOT having a Extend word break property// true if the `prev` rune was an odd Regional_Indicator, false if it was even or not an RI// used for rules WB15 and WB16// see [updateWordRIOdd]isPrevWordRIOdd boolprevPrevLine ucd.LineBreak // the Line Break Class at index i-2 (see rules LB9 and LB10 for edge cases)prevLine ucd.LineBreak // the Line Break Class at index i-1 (see rules LB9 and LB10 for edge cases)prevLineRaw ucd.LineBreak // always for prev (index i-1), despite LB9 and LB10line ucd.LineBreak // the Line Break Class at index inextLine ucd.LineBreak // the Line Break Class at index i+1isPrevPrevDottedCircle bool // following LB9 and LB10isPrevDottedCircle bool // following LB9 and LB10isPrevNonAssignedExtendedPic bool // following LB9 and LB10// the last rune before spaces, used in rules LB14,LB15,LB16,LB17// to match ... SP* ...prevBeforeSpaces, beforeSpaces runeprevBeforeSpacesLine, beforeSpacesLine ucd.LineBreakbeforeSpaceLineRaw ucd.LineBreak // do not follow LB9 and LB10beforeSpacesIndex int// true if the `prev` rune was an odd Regional_Indicator, false if it was even or not an RI// used for rules LB30aisPrevLinebreakRIOdd bool// cached value of ucd.LookupIndicConjunctBreak(cr.r)indicConjunctBreak ucd.IndicConjunctBreak// are we in a numeric sequence, as defined in Example 7 of customisation for LB25numSequence numSequenceState// are we in an emoji sequence, as defined in rule GB11// see [updatePictoSequence]pictoSequence pictoSequenceState// are we in an indic sequence, as defined in rule GB9c// see [updateIndicConjunctBreakSequence]indicConjunctBreakSequence indicCBSequenceState}// initialise the cursor properties// some of them are set in [startIteration]func newCursor( []rune) cursor {:= cursor{len: len(),prevWordNoExtend: -1,}// `startIteration` set `breakCl` from `nextBreakCl`// so we need to init this field before the first iterationif len() != 0 {.nextLine = ucd.LookupLineBreak([0])}return}// computeBreakAttributes does the heavy lifting of text segmentation,// by computing a break attribute for each rune.//// More precisely, `attributes` must be a slice of length len(text)+1,// which will be filled at index i by the attribute describing the// break between rune at index i-1 and index i.//// Unicode defines a lot of properties; for now we only handle// grapheme, word and line breaking.func computeBreakAttributes( []rune, []breakAttr) {// The rules are somewhat complex, but the general logic is pretty simple:// iterate through the input slice, fetch context information// from previous and following runes required by the rules,// and finaly apply them.// Some rules require variable length lookup, which we handle by keeping// a state in a [cursor] object.// initialise the cursor properties:= newCursor()for := 0; <= len(); ++ { // note that we accept i == len(text) to fill the last attribute.startIteration(, )var breakAttr// UAX#29 Grapheme and word Boundaries:= .applyGraphemeBoundaryRules()if {|= graphemeBoundary}, := .applyWordBoundaryRules()if {|= wordBoundary}if {[.prevWordNoExtend] &^= wordBoundary}// UAX#14 Line Breaking:= .applyLineBoundaryRules()switch {case breakEmpty:// rule LB31 : default to allow line break|= lineBoundarycase breakProhibited:&^= lineBoundarycase breakAllowed:|= lineBoundarycase breakMandatory:|= lineBoundary|= mandatoryLineBoundary}.endIteration()[] =}// start and end of the paragraph are always// grapheme boundaries and word boundaries[0] |= graphemeBoundary | wordBoundary // Rule GB1 and WB1[len()] |= graphemeBoundary | wordBoundary // Rule GB2 and WB2// never break before the first char,// but always break after the last[0] &^= lineBoundary // Rule LB2[len()] |= lineBoundary // Rule LB3[len()] |= mandatoryLineBoundary // Rule LB3}// Segmenter is the entry point of the package.//// Usage ://// var seg Segmenter// seg.Init(...)// iter := seg.LineIterator()// for iter.Next() {// ... // do something with iter.Line()// }type Segmenter struct {text []rune// with length len(text) + 1 :// the attribute at indice i is about the// rune at i-1 and i.// See also [ComputeBreakAttributes]// Example :// text : [b, u, l, l]// attributes : [<start> b, b u, u l, l l, l <end>]attributes []breakAttr}// Init resets the segmenter storage with the given input,// and computes the attributes required to segment the text.func ( *Segmenter) ( []rune) {.text = append(.text[:0], ...).initAttributes()}// InitWithString resets the segmenter storage with the given string input,// and computes the attributes required to segment the text.//// If paragraph includes an invalid UTF-8 sequence, these are replaced with U+FFFD.//// InitWithString is more efficient than [Init] if the input is a string.// No allocation for the text is made if its internal buffer capacity is already large enough.func ( *Segmenter) ( string) {.text = .text[:0]for , := range {.text = append(.text, )}.initAttributes()}// InitWithBytes resets the segmenter storage with the given byte slice input,// and computes the attributes required to segment the text.//// If paragraph includes an invalid UTF-8 sequence, these are replaced with U+FFFD.//// InitWithBytes is more efficient than [Init] if the input is a byte slice.// No allocation for the text is made if its internal buffer capacity is already large enough.func ( *Segmenter) ( []byte) {.text = .text[:0]// The Go compiler should optimize this without allocating a string.for , := range string() {.text = append(.text, )}.initAttributes()}func ( *Segmenter) () {.attributes = append(.attributes[:0], make([]breakAttr, len(.text)+1)...)computeBreakAttributes(.text, .attributes)}// attributeIterator is an helper type used to// handle iterating over a slice of runeAttrtype attributeIterator struct {src *Segmenterpos int // the current position in the input slicelastBreak int // the start of the current segmentflag breakAttr // break where this flag is on}// next returns true if there is still a segment to process,// and advances the iterator; or return false.// if returning true, the segment is at [iter.lastBreak:iter.pos]func ( *attributeIterator) () bool {.lastBreak = .pos // remember the start of the next segment.pos++for .pos <= len(.src.text) {// can we break before i ?if .src.attributes[.pos]&.flag != 0 {return true}.pos++}return false}// Line is the content of a line delimited by the segmenter.type Line struct {// Text is a subslice of the original input slice, containing the delimited lineText []rune// Offset is the start of the line in the input rune sliceOffset int// IsMandatoryBreak is true if breaking (at the end of the line)// is mandatoryIsMandatoryBreak bool}// LineIterator provides a convenient way of// iterating over the lines delimited by a `Segmenter`.type LineIterator struct {attributeIterator}// Next returns true if there is still a line to process,// and advances the iterator; or return false.func ( *LineIterator) () bool { return .next() }// Line returns the current `Line`func ( *LineIterator) () Line {return Line{Offset: .lastBreak,Text: .src.text[.lastBreak:.pos], // pos is not included since we break right beforeIsMandatoryBreak: .src.attributes[.pos]&mandatoryLineBoundary != 0,}}// LineIterator returns an iterator on the lines// delimited in [Init].func ( *Segmenter) () *LineIterator {return &LineIterator{attributeIterator: attributeIterator{src: , flag: lineBoundary}}}// Grapheme is the content of a grapheme delimited by the segmenter.type Grapheme struct {// Text is a subslice of the original input slice, containing the delimited graphemeText []rune// Offset is the start of the grapheme in the input rune sliceOffset int}// GraphemeIterator provides a convenient way of// iterating over the graphemes delimited by a `Segmenter`.type GraphemeIterator struct {attributeIterator}// Next returns true if there is still a grapheme to process,// and advances the iterator; or return false.func ( *GraphemeIterator) () bool { return .next() }// Grapheme returns the current `Grapheme`func ( *GraphemeIterator) () Grapheme {return Grapheme{Offset: .lastBreak,Text: .src.text[.lastBreak:.pos],}}// GraphemeIterator returns an iterator over the graphemes// delimited in [Init].func ( *Segmenter) () *GraphemeIterator {return &GraphemeIterator{attributeIterator: attributeIterator{src: , flag: graphemeBoundary}}}// Word is the content of a word delimited by the segmenter.//// More precisely, a word is formed by runes// with the [Alphabetic] property, or with a General_Category of Number,// delimited by the Word Boundary Unicode Property.//// See also https://unicode.org/reports/tr29/#Word_Boundary_Rules,// http://unicode.org/reports/tr44/#Alphabetic and// http://unicode.org/reports/tr44/#General_Category_Valuestype Word struct {// Text is a subslice of the original input slice, containing the delimited wordText []rune// Offset is the start of the word in the input rune sliceOffset int}type WordIterator struct {attributeIteratorinWord bool // true if we have seen the start of a word}// Next returns true if there is still a word to process,// and advances the iterator; or return false.func ( *WordIterator) () bool {:= .next()if ! {return false}if .inWord { // we are have reached the END of a word.inWord = falsereturn true}// do we start a word ? if so, mark itif .pos < len(.src.text) {.inWord = ucd.IsWord(.src.text[.pos])}// in any case, advance againreturn .()}// Word returns the current `Word`func ( *WordIterator) () Word {return Word{Offset: .lastBreak,Text: .src.text[.lastBreak:.pos],}}// WordIterator returns an iterator over the word// delimited in [Init].func ( *Segmenter) () *WordIterator {// check is we start at a word:= falseif len(.text) != 0 {= ucd.IsWord(.text[0])}return &WordIterator{attributeIterator: attributeIterator{src: , flag: wordBoundary}, inWord: }}
![]() |
The pages are generated with Golds v0.8.7. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |