mirror of
https://github.com/mudler/luet.git
synced 2025-06-28 16:28:06 +00:00
Instead of using gox on one side and an action to release, we can merge them together with goreleaser which will build for extra targets (arm, mips if needed in the future) and it also takes care of creating checksums, a source archive, and a changelog and creating a release with all the artifacts. All binaries should respect the old naming convention, so any scripts out there should still work. Signed-off-by: Itxaka <igarcia@suse.com>
547 lines
12 KiB
Go
547 lines
12 KiB
Go
// Copyright 2015 Huan Du. All rights reserved.
|
|
// Licensed under the MIT license that can be found in the LICENSE file.
|
|
|
|
package xstrings
|
|
|
|
import (
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type runeRangeMap struct {
|
|
FromLo rune // Lower bound of range map.
|
|
FromHi rune // An inclusive higher bound of range map.
|
|
ToLo rune
|
|
ToHi rune
|
|
}
|
|
|
|
type runeDict struct {
|
|
Dict [unicode.MaxASCII + 1]rune
|
|
}
|
|
|
|
type runeMap map[rune]rune
|
|
|
|
// Translator can translate string with pre-compiled from and to patterns.
|
|
// If a from/to pattern pair needs to be used more than once, it's recommended
|
|
// to create a Translator and reuse it.
|
|
type Translator struct {
|
|
quickDict *runeDict // A quick dictionary to look up rune by index. Only available for latin runes.
|
|
runeMap runeMap // Rune map for translation.
|
|
ranges []*runeRangeMap // Ranges of runes.
|
|
mappedRune rune // If mappedRune >= 0, all matched runes are translated to the mappedRune.
|
|
reverted bool // If to pattern is empty, all matched characters will be deleted.
|
|
hasPattern bool
|
|
}
|
|
|
|
// NewTranslator creates new Translator through a from/to pattern pair.
|
|
func NewTranslator(from, to string) *Translator {
|
|
tr := &Translator{}
|
|
|
|
if from == "" {
|
|
return tr
|
|
}
|
|
|
|
reverted := from[0] == '^'
|
|
deletion := len(to) == 0
|
|
|
|
if reverted {
|
|
from = from[1:]
|
|
}
|
|
|
|
var fromStart, fromEnd, fromRangeStep rune
|
|
var toStart, toEnd, toRangeStep rune
|
|
var fromRangeSize, toRangeSize rune
|
|
var singleRunes []rune
|
|
|
|
// Update the to rune range.
|
|
updateRange := func() {
|
|
// No more rune to read in the to rune pattern.
|
|
if toEnd == utf8.RuneError {
|
|
return
|
|
}
|
|
|
|
if toRangeStep == 0 {
|
|
to, toStart, toEnd, toRangeStep = nextRuneRange(to, toEnd)
|
|
return
|
|
}
|
|
|
|
// Current range is not empty. Consume 1 rune from start.
|
|
if toStart != toEnd {
|
|
toStart += toRangeStep
|
|
return
|
|
}
|
|
|
|
// No more rune. Repeat the last rune.
|
|
if to == "" {
|
|
toEnd = utf8.RuneError
|
|
return
|
|
}
|
|
|
|
// Both start and end are used. Read two more runes from the to pattern.
|
|
to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError)
|
|
}
|
|
|
|
if deletion {
|
|
toStart = utf8.RuneError
|
|
toEnd = utf8.RuneError
|
|
} else {
|
|
// If from pattern is reverted, only the last rune in the to pattern will be used.
|
|
if reverted {
|
|
var size int
|
|
|
|
for len(to) > 0 {
|
|
toStart, size = utf8.DecodeRuneInString(to)
|
|
to = to[size:]
|
|
}
|
|
|
|
toEnd = utf8.RuneError
|
|
} else {
|
|
to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError)
|
|
}
|
|
}
|
|
|
|
fromEnd = utf8.RuneError
|
|
|
|
for len(from) > 0 {
|
|
from, fromStart, fromEnd, fromRangeStep = nextRuneRange(from, fromEnd)
|
|
|
|
// fromStart is a single character. Just map it with a rune in the to pattern.
|
|
if fromRangeStep == 0 {
|
|
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
|
|
updateRange()
|
|
continue
|
|
}
|
|
|
|
for toEnd != utf8.RuneError && fromStart != fromEnd {
|
|
// If mapped rune is a single character instead of a range, simply shift first
|
|
// rune in the range.
|
|
if toRangeStep == 0 {
|
|
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
|
|
updateRange()
|
|
fromStart += fromRangeStep
|
|
continue
|
|
}
|
|
|
|
fromRangeSize = (fromEnd - fromStart) * fromRangeStep
|
|
toRangeSize = (toEnd - toStart) * toRangeStep
|
|
|
|
// Not enough runes in the to pattern. Need to read more.
|
|
if fromRangeSize > toRangeSize {
|
|
fromStart, toStart = tr.addRuneRange(fromStart, fromStart+toRangeSize*fromRangeStep, toStart, toEnd, singleRunes)
|
|
fromStart += fromRangeStep
|
|
updateRange()
|
|
|
|
// Edge case: If fromRangeSize == toRangeSize + 1, the last fromStart value needs be considered
|
|
// as a single rune.
|
|
if fromStart == fromEnd {
|
|
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
|
|
updateRange()
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
fromStart, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart+fromRangeSize*toRangeStep, singleRunes)
|
|
updateRange()
|
|
break
|
|
}
|
|
|
|
if fromStart == fromEnd {
|
|
fromEnd = utf8.RuneError
|
|
continue
|
|
}
|
|
|
|
_, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart, singleRunes)
|
|
fromEnd = utf8.RuneError
|
|
}
|
|
|
|
if fromEnd != utf8.RuneError {
|
|
tr.addRune(fromEnd, toStart, singleRunes)
|
|
}
|
|
|
|
tr.reverted = reverted
|
|
tr.mappedRune = -1
|
|
tr.hasPattern = true
|
|
|
|
// Translate RuneError only if in deletion or reverted mode.
|
|
if deletion || reverted {
|
|
tr.mappedRune = toStart
|
|
}
|
|
|
|
return tr
|
|
}
|
|
|
|
func (tr *Translator) addRune(from, to rune, singleRunes []rune) []rune {
|
|
if from <= unicode.MaxASCII {
|
|
if tr.quickDict == nil {
|
|
tr.quickDict = &runeDict{}
|
|
}
|
|
|
|
tr.quickDict.Dict[from] = to
|
|
} else {
|
|
if tr.runeMap == nil {
|
|
tr.runeMap = make(runeMap)
|
|
}
|
|
|
|
tr.runeMap[from] = to
|
|
}
|
|
|
|
singleRunes = append(singleRunes, from)
|
|
return singleRunes
|
|
}
|
|
|
|
func (tr *Translator) addRuneRange(fromLo, fromHi, toLo, toHi rune, singleRunes []rune) (rune, rune) {
|
|
var r rune
|
|
var rrm *runeRangeMap
|
|
|
|
if fromLo < fromHi {
|
|
rrm = &runeRangeMap{
|
|
FromLo: fromLo,
|
|
FromHi: fromHi,
|
|
ToLo: toLo,
|
|
ToHi: toHi,
|
|
}
|
|
} else {
|
|
rrm = &runeRangeMap{
|
|
FromLo: fromHi,
|
|
FromHi: fromLo,
|
|
ToLo: toHi,
|
|
ToHi: toLo,
|
|
}
|
|
}
|
|
|
|
// If there is any single rune conflicts with this rune range, clear single rune record.
|
|
for _, r = range singleRunes {
|
|
if rrm.FromLo <= r && r <= rrm.FromHi {
|
|
if r <= unicode.MaxASCII {
|
|
tr.quickDict.Dict[r] = 0
|
|
} else {
|
|
delete(tr.runeMap, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
tr.ranges = append(tr.ranges, rrm)
|
|
return fromHi, toHi
|
|
}
|
|
|
|
func nextRuneRange(str string, last rune) (remaining string, start, end rune, rangeStep rune) {
|
|
var r rune
|
|
var size int
|
|
|
|
remaining = str
|
|
escaping := false
|
|
isRange := false
|
|
|
|
for len(remaining) > 0 {
|
|
r, size = utf8.DecodeRuneInString(remaining)
|
|
remaining = remaining[size:]
|
|
|
|
// Parse special characters.
|
|
if !escaping {
|
|
if r == '\\' {
|
|
escaping = true
|
|
continue
|
|
}
|
|
|
|
if r == '-' {
|
|
// Ignore slash at beginning of string.
|
|
if last == utf8.RuneError {
|
|
continue
|
|
}
|
|
|
|
start = last
|
|
isRange = true
|
|
continue
|
|
}
|
|
}
|
|
|
|
escaping = false
|
|
|
|
if last != utf8.RuneError {
|
|
// This is a range which start and end are the same.
|
|
// Considier it as a normal character.
|
|
if isRange && last == r {
|
|
isRange = false
|
|
continue
|
|
}
|
|
|
|
start = last
|
|
end = r
|
|
|
|
if isRange {
|
|
if start < end {
|
|
rangeStep = 1
|
|
} else {
|
|
rangeStep = -1
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
last = r
|
|
}
|
|
|
|
start = last
|
|
end = utf8.RuneError
|
|
return
|
|
}
|
|
|
|
// Translate str with a from/to pattern pair.
|
|
//
|
|
// See comment in Translate function for usage and samples.
|
|
func (tr *Translator) Translate(str string) string {
|
|
if !tr.hasPattern || str == "" {
|
|
return str
|
|
}
|
|
|
|
var r rune
|
|
var size int
|
|
var needTr bool
|
|
|
|
orig := str
|
|
|
|
var output *stringBuilder
|
|
|
|
for len(str) > 0 {
|
|
r, size = utf8.DecodeRuneInString(str)
|
|
r, needTr = tr.TranslateRune(r)
|
|
|
|
if needTr && output == nil {
|
|
output = allocBuffer(orig, str)
|
|
}
|
|
|
|
if r != utf8.RuneError && output != nil {
|
|
output.WriteRune(r)
|
|
}
|
|
|
|
str = str[size:]
|
|
}
|
|
|
|
// No character is translated.
|
|
if output == nil {
|
|
return orig
|
|
}
|
|
|
|
return output.String()
|
|
}
|
|
|
|
// TranslateRune return translated rune and true if r matches the from pattern.
|
|
// If r doesn't match the pattern, original r is returned and translated is false.
|
|
func (tr *Translator) TranslateRune(r rune) (result rune, translated bool) {
|
|
switch {
|
|
case tr.quickDict != nil:
|
|
if r <= unicode.MaxASCII {
|
|
result = tr.quickDict.Dict[r]
|
|
|
|
if result != 0 {
|
|
translated = true
|
|
|
|
if tr.mappedRune >= 0 {
|
|
result = tr.mappedRune
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
fallthrough
|
|
|
|
case tr.runeMap != nil:
|
|
var ok bool
|
|
|
|
if result, ok = tr.runeMap[r]; ok {
|
|
translated = true
|
|
|
|
if tr.mappedRune >= 0 {
|
|
result = tr.mappedRune
|
|
}
|
|
|
|
break
|
|
}
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
var rrm *runeRangeMap
|
|
ranges := tr.ranges
|
|
|
|
for i := len(ranges) - 1; i >= 0; i-- {
|
|
rrm = ranges[i]
|
|
|
|
if rrm.FromLo <= r && r <= rrm.FromHi {
|
|
translated = true
|
|
|
|
if tr.mappedRune >= 0 {
|
|
result = tr.mappedRune
|
|
break
|
|
}
|
|
|
|
if rrm.ToLo < rrm.ToHi {
|
|
result = rrm.ToLo + r - rrm.FromLo
|
|
} else if rrm.ToLo > rrm.ToHi {
|
|
// ToHi can be smaller than ToLo if range is from higher to lower.
|
|
result = rrm.ToLo - r + rrm.FromLo
|
|
} else {
|
|
result = rrm.ToLo
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if tr.reverted {
|
|
if !translated {
|
|
result = tr.mappedRune
|
|
}
|
|
|
|
translated = !translated
|
|
}
|
|
|
|
if !translated {
|
|
result = r
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// HasPattern returns true if Translator has one pattern at least.
|
|
func (tr *Translator) HasPattern() bool {
|
|
return tr.hasPattern
|
|
}
|
|
|
|
// Translate str with the characters defined in from replaced by characters defined in to.
|
|
//
|
|
// From and to are patterns representing a set of characters. Pattern is defined as following.
|
|
//
|
|
// * Special characters
|
|
// * '-' means a range of runes, e.g.
|
|
// * "a-z" means all characters from 'a' to 'z' inclusive;
|
|
// * "z-a" means all characters from 'z' to 'a' inclusive.
|
|
// * '^' as first character means a set of all runes excepted listed, e.g.
|
|
// * "^a-z" means all characters except 'a' to 'z' inclusive.
|
|
// * '\' escapes special characters.
|
|
// * Normal character represents itself, e.g. "abc" is a set including 'a', 'b' and 'c'.
|
|
//
|
|
// Translate will try to find a 1:1 mapping from from to to.
|
|
// If to is smaller than from, last rune in to will be used to map "out of range" characters in from.
|
|
//
|
|
// Note that '^' only works in the from pattern. It will be considered as a normal character in the to pattern.
|
|
//
|
|
// If the to pattern is an empty string, Translate works exactly the same as Delete.
|
|
//
|
|
// Samples:
|
|
// Translate("hello", "aeiou", "12345") => "h2ll4"
|
|
// Translate("hello", "a-z", "A-Z") => "HELLO"
|
|
// Translate("hello", "z-a", "a-z") => "svool"
|
|
// Translate("hello", "aeiou", "*") => "h*ll*"
|
|
// Translate("hello", "^l", "*") => "**ll*"
|
|
// Translate("hello ^ world", `\^lo`, "*") => "he*** * w*r*d"
|
|
func Translate(str, from, to string) string {
|
|
tr := NewTranslator(from, to)
|
|
return tr.Translate(str)
|
|
}
|
|
|
|
// Delete runes in str matching the pattern.
|
|
// Pattern is defined in Translate function.
|
|
//
|
|
// Samples:
|
|
// Delete("hello", "aeiou") => "hll"
|
|
// Delete("hello", "a-k") => "llo"
|
|
// Delete("hello", "^a-k") => "he"
|
|
func Delete(str, pattern string) string {
|
|
tr := NewTranslator(pattern, "")
|
|
return tr.Translate(str)
|
|
}
|
|
|
|
// Count how many runes in str match the pattern.
|
|
// Pattern is defined in Translate function.
|
|
//
|
|
// Samples:
|
|
// Count("hello", "aeiou") => 3
|
|
// Count("hello", "a-k") => 3
|
|
// Count("hello", "^a-k") => 2
|
|
func Count(str, pattern string) int {
|
|
if pattern == "" || str == "" {
|
|
return 0
|
|
}
|
|
|
|
var r rune
|
|
var size int
|
|
var matched bool
|
|
|
|
tr := NewTranslator(pattern, "")
|
|
cnt := 0
|
|
|
|
for len(str) > 0 {
|
|
r, size = utf8.DecodeRuneInString(str)
|
|
str = str[size:]
|
|
|
|
if _, matched = tr.TranslateRune(r); matched {
|
|
cnt++
|
|
}
|
|
}
|
|
|
|
return cnt
|
|
}
|
|
|
|
// Squeeze deletes adjacent repeated runes in str.
|
|
// If pattern is not empty, only runes matching the pattern will be squeezed.
|
|
//
|
|
// Samples:
|
|
// Squeeze("hello", "") => "helo"
|
|
// Squeeze("hello", "m-z") => "hello"
|
|
// Squeeze("hello world", " ") => "hello world"
|
|
func Squeeze(str, pattern string) string {
|
|
var last, r rune
|
|
var size int
|
|
var skipSqueeze, matched bool
|
|
var tr *Translator
|
|
var output *stringBuilder
|
|
|
|
orig := str
|
|
last = -1
|
|
|
|
if len(pattern) > 0 {
|
|
tr = NewTranslator(pattern, "")
|
|
}
|
|
|
|
for len(str) > 0 {
|
|
r, size = utf8.DecodeRuneInString(str)
|
|
|
|
// Need to squeeze the str.
|
|
if last == r && !skipSqueeze {
|
|
if tr != nil {
|
|
if _, matched = tr.TranslateRune(r); !matched {
|
|
skipSqueeze = true
|
|
}
|
|
}
|
|
|
|
if output == nil {
|
|
output = allocBuffer(orig, str)
|
|
}
|
|
|
|
if skipSqueeze {
|
|
output.WriteRune(r)
|
|
}
|
|
} else {
|
|
if output != nil {
|
|
output.WriteRune(r)
|
|
}
|
|
|
|
last = r
|
|
skipSqueeze = false
|
|
}
|
|
|
|
str = str[size:]
|
|
}
|
|
|
|
if output == nil {
|
|
return orig
|
|
}
|
|
|
|
return output.String()
|
|
}
|