| // Copyright 2012 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package build // import "golang.org/x/text/collate/build" |
| |
| import ( |
| "fmt" |
| "io" |
| "log" |
| "sort" |
| "strings" |
| "unicode/utf8" |
| |
| "golang.org/x/text/internal/colltab" |
| "golang.org/x/text/language" |
| "golang.org/x/text/unicode/norm" |
| ) |
| |
| // TODO: optimizations: |
| // - expandElem is currently 20K. By putting unique colElems in a separate |
| // table and having a byte array of indexes into this table, we can reduce |
| // the total size to about 7K. By also factoring out the length bytes, we |
| // can reduce this to about 6K. |
| // - trie valueBlocks are currently 100K. There are a lot of sparse blocks |
| // and many consecutive values with the same stride. This can be further |
| // compacted. |
| // - Compress secondary weights into 8 bits. |
| // - Some LDML specs specify a context element. Currently we simply concatenate |
| // those. Context can be implemented using the contraction trie. If Builder |
| // could analyze and detect when using a context makes sense, there is no |
| // need to expose this construct in the API. |
| |
| // A Builder builds a root collation table. The user must specify the |
| // collation elements for each entry. A common use will be to base the weights |
| // on those specified in the allkeys* file as provided by the UCA or CLDR. |
| type Builder struct { |
| index *trieBuilder |
| root ordering |
| locale []*Tailoring |
| t *table |
| err error |
| built bool |
| |
| minNonVar int // lowest primary recorded for a variable |
| varTop int // highest primary recorded for a non-variable |
| |
| // indexes used for reusing expansions and contractions |
| expIndex map[string]int // positions of expansions keyed by their string representation |
| ctHandle map[string]ctHandle // contraction handles keyed by a concatenation of the suffixes |
| ctElem map[string]int // contraction elements keyed by their string representation |
| } |
| |
| // A Tailoring builds a collation table based on another collation table. |
| // The table is defined by specifying tailorings to the underlying table. |
| // See https://unicode.org/reports/tr35/ for an overview of tailoring |
| // collation tables. The CLDR contains pre-defined tailorings for a variety |
| // of languages (See https://www.unicode.org/Public/cldr/<version>/core.zip.) |
| type Tailoring struct { |
| id string |
| builder *Builder |
| index *ordering |
| |
| anchor *entry |
| before bool |
| } |
| |
| // NewBuilder returns a new Builder. |
| func NewBuilder() *Builder { |
| return &Builder{ |
| index: newTrieBuilder(), |
| root: makeRootOrdering(), |
| expIndex: make(map[string]int), |
| ctHandle: make(map[string]ctHandle), |
| ctElem: make(map[string]int), |
| } |
| } |
| |
| // Tailoring returns a Tailoring for the given locale. One should |
| // have completed all calls to Add before calling Tailoring. |
| func (b *Builder) Tailoring(loc language.Tag) *Tailoring { |
| t := &Tailoring{ |
| id: loc.String(), |
| builder: b, |
| index: b.root.clone(), |
| } |
| t.index.id = t.id |
| b.locale = append(b.locale, t) |
| return t |
| } |
| |
| // Add adds an entry to the collation element table, mapping |
| // a slice of runes to a sequence of collation elements. |
| // A collation element is specified as list of weights: []int{primary, secondary, ...}. |
| // The entries are typically obtained from a collation element table |
| // as defined in https://www.unicode.org/reports/tr10/#Data_Table_Format. |
| // Note that the collation elements specified by colelems are only used |
| // as a guide. The actual weights generated by Builder may differ. |
| // The argument variables is a list of indices into colelems that should contain |
| // a value for each colelem that is a variable. (See the reference above.) |
| func (b *Builder) Add(runes []rune, colelems [][]int, variables []int) error { |
| str := string(runes) |
| elems := make([]rawCE, len(colelems)) |
| for i, ce := range colelems { |
| if len(ce) == 0 { |
| break |
| } |
| elems[i] = makeRawCE(ce, 0) |
| if len(ce) == 1 { |
| elems[i].w[1] = defaultSecondary |
| } |
| if len(ce) <= 2 { |
| elems[i].w[2] = defaultTertiary |
| } |
| if len(ce) <= 3 { |
| elems[i].w[3] = ce[0] |
| } |
| } |
| for i, ce := range elems { |
| p := ce.w[0] |
| isvar := false |
| for _, j := range variables { |
| if i == j { |
| isvar = true |
| } |
| } |
| if isvar { |
| if p >= b.minNonVar && b.minNonVar > 0 { |
| return fmt.Errorf("primary value %X of variable is larger than the smallest non-variable %X", p, b.minNonVar) |
| } |
| if p > b.varTop { |
| b.varTop = p |
| } |
| } else if p > 1 { // 1 is a special primary value reserved for FFFE |
| if p <= b.varTop { |
| return fmt.Errorf("primary value %X of non-variable is smaller than the highest variable %X", p, b.varTop) |
| } |
| if b.minNonVar == 0 || p < b.minNonVar { |
| b.minNonVar = p |
| } |
| } |
| } |
| elems, err := convertLargeWeights(elems) |
| if err != nil { |
| return err |
| } |
| cccs := []uint8{} |
| nfd := norm.NFD.String(str) |
| for i := range nfd { |
| cccs = append(cccs, norm.NFD.PropertiesString(nfd[i:]).CCC()) |
| } |
| if len(cccs) < len(elems) { |
| if len(cccs) > 2 { |
| return fmt.Errorf("number of decomposed characters should be greater or equal to the number of collation elements for len(colelems) > 3 (%d < %d)", len(cccs), len(elems)) |
| } |
| p := len(elems) - 1 |
| for ; p > 0 && elems[p].w[0] == 0; p-- { |
| elems[p].ccc = cccs[len(cccs)-1] |
| } |
| for ; p >= 0; p-- { |
| elems[p].ccc = cccs[0] |
| } |
| } else { |
| for i := range elems { |
| elems[i].ccc = cccs[i] |
| } |
| } |
| // doNorm in collate.go assumes that the following conditions hold. |
| if len(elems) > 1 && len(cccs) > 1 && cccs[0] != 0 && cccs[0] != cccs[len(cccs)-1] { |
| return fmt.Errorf("incompatible CCC values for expansion %X (%d)", runes, cccs) |
| } |
| b.root.newEntry(str, elems) |
| return nil |
| } |
| |
| func (t *Tailoring) setAnchor(anchor string) error { |
| anchor = norm.NFC.String(anchor) |
| a := t.index.find(anchor) |
| if a == nil { |
| a = t.index.newEntry(anchor, nil) |
| a.implicit = true |
| a.modified = true |
| for _, r := range []rune(anchor) { |
| e := t.index.find(string(r)) |
| e.lock = true |
| } |
| } |
| t.anchor = a |
| return nil |
| } |
| |
| // SetAnchor sets the point after which elements passed in subsequent calls to |
| // Insert will be inserted. It is equivalent to the reset directive in an LDML |
| // specification. See Insert for an example. |
| // SetAnchor supports the following logical reset positions: |
| // <first_tertiary_ignorable/>, <last_teriary_ignorable/>, <first_primary_ignorable/>, |
| // and <last_non_ignorable/>. |
| func (t *Tailoring) SetAnchor(anchor string) error { |
| if err := t.setAnchor(anchor); err != nil { |
| return err |
| } |
| t.before = false |
| return nil |
| } |
| |
| // SetAnchorBefore is similar to SetAnchor, except that subsequent calls to |
| // Insert will insert entries before the anchor. |
| func (t *Tailoring) SetAnchorBefore(anchor string) error { |
| if err := t.setAnchor(anchor); err != nil { |
| return err |
| } |
| t.before = true |
| return nil |
| } |
| |
| // Insert sets the ordering of str relative to the entry set by the previous |
| // call to SetAnchor or Insert. The argument extend corresponds |
| // to the extend elements as defined in LDML. A non-empty value for extend |
| // will cause the collation elements corresponding to extend to be appended |
| // to the collation elements generated for the entry added by Insert. |
| // This has the same net effect as sorting str after the string anchor+extend. |
| // See https://www.unicode.org/reports/tr10/#Tailoring_Example for details |
| // on parametric tailoring and https://unicode.org/reports/tr35/#Collation_Elements |
| // for full details on LDML. |
| // |
| // Examples: create a tailoring for Swedish, where "ä" is ordered after "z" |
| // at the primary sorting level: |
| // t := b.Tailoring("se") |
| // t.SetAnchor("z") |
| // t.Insert(colltab.Primary, "ä", "") |
| // Order "ü" after "ue" at the secondary sorting level: |
| // t.SetAnchor("ue") |
| // t.Insert(colltab.Secondary, "ü","") |
| // or |
| // t.SetAnchor("u") |
| // t.Insert(colltab.Secondary, "ü", "e") |
| // Order "q" afer "ab" at the secondary level and "Q" after "q" |
| // at the tertiary level: |
| // t.SetAnchor("ab") |
| // t.Insert(colltab.Secondary, "q", "") |
| // t.Insert(colltab.Tertiary, "Q", "") |
| // Order "b" before "a": |
| // t.SetAnchorBefore("a") |
| // t.Insert(colltab.Primary, "b", "") |
| // Order "0" after the last primary ignorable: |
| // t.SetAnchor("<last_primary_ignorable/>") |
| // t.Insert(colltab.Primary, "0", "") |
| func (t *Tailoring) Insert(level colltab.Level, str, extend string) error { |
| if t.anchor == nil { |
| return fmt.Errorf("%s:Insert: no anchor point set for tailoring of %s", t.id, str) |
| } |
| str = norm.NFC.String(str) |
| e := t.index.find(str) |
| if e == nil { |
| e = t.index.newEntry(str, nil) |
| } else if e.logical != noAnchor { |
| return fmt.Errorf("%s:Insert: cannot reinsert logical reset position %q", t.id, e.str) |
| } |
| if e.lock { |
| return fmt.Errorf("%s:Insert: cannot reinsert element %q", t.id, e.str) |
| } |
| a := t.anchor |
| // Find the first element after the anchor which differs at a level smaller or |
| // equal to the given level. Then insert at this position. |
| // See https://unicode.org/reports/tr35/#Collation_Elements, Section 5.14.5 for details. |
| e.before = t.before |
| if t.before { |
| t.before = false |
| if a.prev == nil { |
| a.insertBefore(e) |
| } else { |
| for a = a.prev; a.level > level; a = a.prev { |
| } |
| a.insertAfter(e) |
| } |
| e.level = level |
| } else { |
| for ; a.level > level; a = a.next { |
| } |
| e.level = a.level |
| if a != e { |
| a.insertAfter(e) |
| a.level = level |
| } else { |
| // We don't set a to prev itself. This has the effect of the entry |
| // getting new collation elements that are an increment of itself. |
| // This is intentional. |
| a.prev.level = level |
| } |
| } |
| e.extend = norm.NFD.String(extend) |
| e.exclude = false |
| e.modified = true |
| e.elems = nil |
| t.anchor = e |
| return nil |
| } |
| |
| func (o *ordering) getWeight(e *entry) []rawCE { |
| if len(e.elems) == 0 && e.logical == noAnchor { |
| if e.implicit { |
| for _, r := range e.runes { |
| e.elems = append(e.elems, o.getWeight(o.find(string(r)))...) |
| } |
| } else if e.before { |
| count := [colltab.Identity + 1]int{} |
| a := e |
| for ; a.elems == nil && !a.implicit; a = a.next { |
| count[a.level]++ |
| } |
| e.elems = []rawCE{makeRawCE(a.elems[0].w, a.elems[0].ccc)} |
| for i := colltab.Primary; i < colltab.Quaternary; i++ { |
| if count[i] != 0 { |
| e.elems[0].w[i] -= count[i] |
| break |
| } |
| } |
| if e.prev != nil { |
| o.verifyWeights(e.prev, e, e.prev.level) |
| } |
| } else { |
| prev := e.prev |
| e.elems = nextWeight(prev.level, o.getWeight(prev)) |
| o.verifyWeights(e, e.next, e.level) |
| } |
| } |
| return e.elems |
| } |
| |
| func (o *ordering) addExtension(e *entry) { |
| if ex := o.find(e.extend); ex != nil { |
| e.elems = append(e.elems, ex.elems...) |
| } else { |
| for _, r := range []rune(e.extend) { |
| e.elems = append(e.elems, o.find(string(r)).elems...) |
| } |
| } |
| e.extend = "" |
| } |
| |
| func (o *ordering) verifyWeights(a, b *entry, level colltab.Level) error { |
| if level == colltab.Identity || b == nil || b.elems == nil || a.elems == nil { |
| return nil |
| } |
| for i := colltab.Primary; i < level; i++ { |
| if a.elems[0].w[i] < b.elems[0].w[i] { |
| return nil |
| } |
| } |
| if a.elems[0].w[level] >= b.elems[0].w[level] { |
| err := fmt.Errorf("%s:overflow: collation elements of %q (%X) overflows those of %q (%X) at level %d (%X >= %X)", o.id, a.str, a.runes, b.str, b.runes, level, a.elems, b.elems) |
| log.Println(err) |
| // TODO: return the error instead, or better, fix the conflicting entry by making room. |
| } |
| return nil |
| } |
| |
| func (b *Builder) error(e error) { |
| if e != nil { |
| b.err = e |
| } |
| } |
| |
| func (b *Builder) errorID(locale string, e error) { |
| if e != nil { |
| b.err = fmt.Errorf("%s:%v", locale, e) |
| } |
| } |
| |
| // patchNorm ensures that NFC and NFD counterparts are consistent. |
| func (o *ordering) patchNorm() { |
| // Insert the NFD counterparts, if necessary. |
| for _, e := range o.ordered { |
| nfd := norm.NFD.String(e.str) |
| if nfd != e.str { |
| if e0 := o.find(nfd); e0 != nil && !e0.modified { |
| e0.elems = e.elems |
| } else if e.modified && !equalCEArrays(o.genColElems(nfd), e.elems) { |
| e := o.newEntry(nfd, e.elems) |
| e.modified = true |
| } |
| } |
| } |
| // Update unchanged composed forms if one of their parts changed. |
| for _, e := range o.ordered { |
| nfd := norm.NFD.String(e.str) |
| if e.modified || nfd == e.str { |
| continue |
| } |
| if e0 := o.find(nfd); e0 != nil { |
| e.elems = e0.elems |
| } else { |
| e.elems = o.genColElems(nfd) |
| if norm.NFD.LastBoundary([]byte(nfd)) == 0 { |
| r := []rune(nfd) |
| head := string(r[0]) |
| tail := "" |
| for i := 1; i < len(r); i++ { |
| s := norm.NFC.String(head + string(r[i])) |
| if e0 := o.find(s); e0 != nil && e0.modified { |
| head = s |
| } else { |
| tail += string(r[i]) |
| } |
| } |
| e.elems = append(o.genColElems(head), o.genColElems(tail)...) |
| } |
| } |
| } |
| // Exclude entries for which the individual runes generate the same collation elements. |
| for _, e := range o.ordered { |
| if len(e.runes) > 1 && equalCEArrays(o.genColElems(e.str), e.elems) { |
| e.exclude = true |
| } |
| } |
| } |
| |
| func (b *Builder) buildOrdering(o *ordering) { |
| for _, e := range o.ordered { |
| o.getWeight(e) |
| } |
| for _, e := range o.ordered { |
| o.addExtension(e) |
| } |
| o.patchNorm() |
| o.sort() |
| simplify(o) |
| b.processExpansions(o) // requires simplify |
| b.processContractions(o) // requires simplify |
| |
| t := newNode() |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| if !e.skip() { |
| ce, err := e.encode() |
| b.errorID(o.id, err) |
| t.insert(e.runes[0], ce) |
| } |
| } |
| o.handle = b.index.addTrie(t) |
| } |
| |
| func (b *Builder) build() (*table, error) { |
| if b.built { |
| return b.t, b.err |
| } |
| b.built = true |
| b.t = &table{ |
| Table: colltab.Table{ |
| MaxContractLen: utf8.UTFMax, |
| VariableTop: uint32(b.varTop), |
| }, |
| } |
| |
| b.buildOrdering(&b.root) |
| b.t.root = b.root.handle |
| for _, t := range b.locale { |
| b.buildOrdering(t.index) |
| if b.err != nil { |
| break |
| } |
| } |
| i, err := b.index.generate() |
| b.t.trie = *i |
| b.t.Index = colltab.Trie{ |
| Index: i.index, |
| Values: i.values, |
| Index0: i.index[blockSize*b.t.root.lookupStart:], |
| Values0: i.values[blockSize*b.t.root.valueStart:], |
| } |
| b.error(err) |
| return b.t, b.err |
| } |
| |
| // Build builds the root Collator. |
| func (b *Builder) Build() (colltab.Weighter, error) { |
| table, err := b.build() |
| if err != nil { |
| return nil, err |
| } |
| return table, nil |
| } |
| |
| // Build builds a Collator for Tailoring t. |
| func (t *Tailoring) Build() (colltab.Weighter, error) { |
| // TODO: implement. |
| return nil, nil |
| } |
| |
| // Print prints the tables for b and all its Tailorings as a Go file |
| // that can be included in the Collate package. |
| func (b *Builder) Print(w io.Writer) (n int, err error) { |
| p := func(nn int, e error) { |
| n += nn |
| if err == nil { |
| err = e |
| } |
| } |
| t, err := b.build() |
| if err != nil { |
| return 0, err |
| } |
| p(fmt.Fprintf(w, `var availableLocales = "und`)) |
| for _, loc := range b.locale { |
| if loc.id != "und" { |
| p(fmt.Fprintf(w, ",%s", loc.id)) |
| } |
| } |
| p(fmt.Fprint(w, "\"\n\n")) |
| p(fmt.Fprintf(w, "const varTop = 0x%x\n\n", b.varTop)) |
| p(fmt.Fprintln(w, "var locales = [...]tableIndex{")) |
| for _, loc := range b.locale { |
| if loc.id == "und" { |
| p(t.fprintIndex(w, loc.index.handle, loc.id)) |
| } |
| } |
| for _, loc := range b.locale { |
| if loc.id != "und" { |
| p(t.fprintIndex(w, loc.index.handle, loc.id)) |
| } |
| } |
| p(fmt.Fprint(w, "}\n\n")) |
| n, _, err = t.fprint(w, "main") |
| return |
| } |
| |
| // reproducibleFromNFKD checks whether the given expansion could be generated |
| // from an NFKD expansion. |
| func reproducibleFromNFKD(e *entry, exp, nfkd []rawCE) bool { |
| // Length must be equal. |
| if len(exp) != len(nfkd) { |
| return false |
| } |
| for i, ce := range exp { |
| // Primary and secondary values should be equal. |
| if ce.w[0] != nfkd[i].w[0] || ce.w[1] != nfkd[i].w[1] { |
| return false |
| } |
| // Tertiary values should be equal to maxTertiary for third element onwards. |
| // TODO: there seem to be a lot of cases in CLDR (e.g. ㏭ in zh.xml) that can |
| // simply be dropped. Try this out by dropping the following code. |
| if i >= 2 && ce.w[2] != maxTertiary { |
| return false |
| } |
| if _, err := makeCE(ce); err != nil { |
| // Simply return false. The error will be caught elsewhere. |
| return false |
| } |
| } |
| return true |
| } |
| |
| func simplify(o *ordering) { |
| // Runes that are a starter of a contraction should not be removed. |
| // (To date, there is only Kannada character 0CCA.) |
| keep := make(map[rune]bool) |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| if len(e.runes) > 1 { |
| keep[e.runes[0]] = true |
| } |
| } |
| // Tag entries for which the runes NFKD decompose to identical values. |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| s := e.str |
| nfkd := norm.NFKD.String(s) |
| nfd := norm.NFD.String(s) |
| if e.decompose || len(e.runes) > 1 || len(e.elems) == 1 || keep[e.runes[0]] || nfkd == nfd { |
| continue |
| } |
| if reproducibleFromNFKD(e, e.elems, o.genColElems(nfkd)) { |
| e.decompose = true |
| } |
| } |
| } |
| |
| // appendExpansion converts the given collation sequence to |
| // collation elements and adds them to the expansion table. |
| // It returns an index to the expansion table. |
| func (b *Builder) appendExpansion(e *entry) int { |
| t := b.t |
| i := len(t.ExpandElem) |
| ce := uint32(len(e.elems)) |
| t.ExpandElem = append(t.ExpandElem, ce) |
| for _, w := range e.elems { |
| ce, err := makeCE(w) |
| if err != nil { |
| b.error(err) |
| return -1 |
| } |
| t.ExpandElem = append(t.ExpandElem, ce) |
| } |
| return i |
| } |
| |
| // processExpansions extracts data necessary to generate |
| // the extraction tables. |
| func (b *Builder) processExpansions(o *ordering) { |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| if !e.expansion() { |
| continue |
| } |
| key := fmt.Sprintf("%v", e.elems) |
| i, ok := b.expIndex[key] |
| if !ok { |
| i = b.appendExpansion(e) |
| b.expIndex[key] = i |
| } |
| e.expansionIndex = i |
| } |
| } |
| |
| func (b *Builder) processContractions(o *ordering) { |
| // Collate contractions per starter rune. |
| starters := []rune{} |
| cm := make(map[rune][]*entry) |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| if e.contraction() { |
| if len(e.str) > b.t.MaxContractLen { |
| b.t.MaxContractLen = len(e.str) |
| } |
| r := e.runes[0] |
| if _, ok := cm[r]; !ok { |
| starters = append(starters, r) |
| } |
| cm[r] = append(cm[r], e) |
| } |
| } |
| // Add entries of single runes that are at a start of a contraction. |
| for e := o.front(); e != nil; e, _ = e.nextIndexed() { |
| if !e.contraction() { |
| r := e.runes[0] |
| if _, ok := cm[r]; ok { |
| cm[r] = append(cm[r], e) |
| } |
| } |
| } |
| // Build the tries for the contractions. |
| t := b.t |
| for _, r := range starters { |
| l := cm[r] |
| // Compute suffix strings. There are 31 different contraction suffix |
| // sets for 715 contractions and 82 contraction starter runes as of |
| // version 6.0.0. |
| sufx := []string{} |
| hasSingle := false |
| for _, e := range l { |
| if len(e.runes) > 1 { |
| sufx = append(sufx, string(e.runes[1:])) |
| } else { |
| hasSingle = true |
| } |
| } |
| if !hasSingle { |
| b.error(fmt.Errorf("no single entry for starter rune %U found", r)) |
| continue |
| } |
| // Unique the suffix set. |
| sort.Strings(sufx) |
| key := strings.Join(sufx, "\n") |
| handle, ok := b.ctHandle[key] |
| if !ok { |
| var err error |
| handle, err = appendTrie(&t.ContractTries, sufx) |
| if err != nil { |
| b.error(err) |
| } |
| b.ctHandle[key] = handle |
| } |
| // Bucket sort entries in index order. |
| es := make([]*entry, len(l)) |
| for _, e := range l { |
| var p, sn int |
| if len(e.runes) > 1 { |
| str := []byte(string(e.runes[1:])) |
| p, sn = lookup(&t.ContractTries, handle, str) |
| if sn != len(str) { |
| log.Fatalf("%s: processContractions: unexpected length for '%X'; len=%d; want %d", o.id, e.runes, sn, len(str)) |
| } |
| } |
| if es[p] != nil { |
| log.Fatalf("%s: multiple contractions for position %d for rune %U", o.id, p, e.runes[0]) |
| } |
| es[p] = e |
| } |
| // Create collation elements for contractions. |
| elems := []uint32{} |
| for _, e := range es { |
| ce, err := e.encodeBase() |
| b.errorID(o.id, err) |
| elems = append(elems, ce) |
| } |
| key = fmt.Sprintf("%v", elems) |
| i, ok := b.ctElem[key] |
| if !ok { |
| i = len(t.ContractElem) |
| b.ctElem[key] = i |
| t.ContractElem = append(t.ContractElem, elems...) |
| } |
| // Store info in entry for starter rune. |
| es[0].contractionIndex = i |
| es[0].contractionHandle = handle |
| } |
| } |