diff --git a/go.mod b/go.mod index d0043fe9..4d696054 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/ansi v0.11.8 github.com/charmbracelet/x/term v0.2.2 - github.com/dlclark/regexp2/v2 v2.7.1 + github.com/dlclark/regexp2/v2 v2.7.2 github.com/jessevdk/go-flags v1.6.1 github.com/mattn/go-runewidth v0.0.29 github.com/muesli/termenv v0.16.0 diff --git a/go.sum b/go.sum index e99dc42d..c6d18788 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/dlclark/regexp2/v2 v2.7.1 h1:yqDtwI1ptXXvEUNpYTk2lad4jLtAcKqkzepn4savSk4= -github.com/dlclark/regexp2/v2 v2.7.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dlclark/regexp2/v2 v2.7.2 h1:nBhTfGMNPCDloto6XZRFJ5wpi+lym2eUMHRBY0diX30= +github.com/dlclark/regexp2/v2 v2.7.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= diff --git a/vendor/github.com/dlclark/regexp2/v2/decode.go b/vendor/github.com/dlclark/regexp2/v2/decode.go index 6427e548..b0b834f7 100644 --- a/vendor/github.com/dlclark/regexp2/v2/decode.go +++ b/vendor/github.com/dlclark/regexp2/v2/decode.go @@ -12,9 +12,10 @@ import ( type decodedInput struct { runes []rune pooled *[]rune - runeStart int // index in runes of the requested startAt; -1 if not a rune boundary - runeOffset int // original-string rune index of runes[0] - byteOffset int // original-string byte index of runes[0] + runeStart int // index in runes of the requested startAt; -1 if not a rune boundary + runeOffset int // original-string rune index of runes[0] + byteOffset int // original-string byte index of runes[0] + ascii bool // every decoded rune is ASCII; invalid UTF-8 is not ASCII } // decodeFrom is the first original-string byte that must be decoded. 0 means @@ -40,6 +41,19 @@ func (re *Regexp) decodeLeftContextRunes() int { return re.leftContextRunes } +// stringSearchOrigin preserves the caller's origin when \G may observe it. +// Such patterns retain the whole input, so no decode offset is needed. +// Otherwise the origin is unobservable and the candidate is sufficient. +func (re *Regexp) stringSearchOrigin(input string, startAt, candidate int) int { + if !re.RightToLeft() && re.decodeLeftContextRunes() < 0 { + if startAt <= 0 { + return 0 + } + return utf8.RuneCountInString(input[:startAt]) + } + return candidate +} + // decodeString converts s to []rune for the MatchString startAt<=0 path. // Keep this as close as possible to a single UTF-8 walk so 4–30 byte // matches stay cheap. @@ -85,6 +99,7 @@ func decodeInput(s string, startAt, decodeFrom, maxCachedLength int, needOffsets runes: runes, pooled: pooledBuffer, byteOffset: decodeFrom, + ascii: ascii, runeStart: startRuneIndex(s, startAt, decodeFrom, n, ascii), } diff --git a/vendor/github.com/dlclark/regexp2/v2/helpers/indexof.go b/vendor/github.com/dlclark/regexp2/v2/helpers/indexof.go index 14dee3c2..5cd13e07 100644 --- a/vendor/github.com/dlclark/regexp2/v2/helpers/indexof.go +++ b/vendor/github.com/dlclark/regexp2/v2/helpers/indexof.go @@ -147,7 +147,7 @@ func IndexFunc(in []rune, f func(ch rune) bool) int { func IndexOfAnyExceptInSet(in []rune, set syntax.CharSet) int { for i, c := range in { - if !set.CharIn(c) { + if !set.Contains(c) { return i } } diff --git a/vendor/github.com/dlclark/regexp2/v2/match.go b/vendor/github.com/dlclark/regexp2/v2/match.go index ba8a0105..91d83d08 100644 --- a/vendor/github.com/dlclark/regexp2/v2/match.go +++ b/vendor/github.com/dlclark/regexp2/v2/match.go @@ -118,6 +118,9 @@ func newStringMatchTextAt(input string, r []rune, runeOffset, byteOffset int) *m hasStringInput: true, runeOffset: runeOffset, byteOffset: byteOffset, + // Every decoded rune consumes at least one byte, even invalid UTF-8. + // Equal lengths therefore mean byte and rune offsets are identical. + byteOffsetsReady: len(input)-byteOffset == len(r), } } @@ -141,36 +144,25 @@ func (t *matchText) byteRange(runeIndex, runeLength int) (int, int) { func (t *matchText) buildByteOffsets() []int { if t.hasStringInput { - return stringByteOffsets(t.input[t.byteOffset:]) + return stringByteOffsets(t.input[t.byteOffset:], len(t.runes)) } return runeByteOffsets(t.runes) } -func stringByteOffsets(s string) []int { - var byteOffsets []int - runeIndex := 0 - for strIdx, ch := range s { - if byteOffsets != nil { - byteOffsets[runeIndex] = strIdx - } - runeLen := utf8.RuneLen(ch) - if ch == utf8.RuneError { - _, runeLen = utf8.DecodeRuneInString(s[strIdx:]) - } - if byteOffsets == nil && (strIdx != runeIndex || runeLen != 1) { - byteOffsets = make([]int, len(s)+1) - for i := 0; i < runeIndex; i++ { - byteOffsets[i] = i - } - byteOffsets[runeIndex] = strIdx - } - runeIndex++ +func stringByteOffsets(s string, runeCount int) []int { + if len(s) == runeCount { + return nil } - if byteOffsets != nil { - byteOffsets[runeIndex] = len(s) - return byteOffsets[:runeIndex+1] + // Decoding already established the rune count. A range walk gives the + // original byte positions, including one-byte invalid UTF-8 sequences. + offsets := make([]int, runeCount+1) + i := 0 + for byteIndex := range s { + offsets[i] = byteIndex + i++ } - return nil + offsets[runeCount] = len(s) + return offsets } func runeByteOffsets(runes []rune) []int { @@ -382,9 +374,11 @@ func (m *Match) GroupByName(name string) *Group { func (m *Match) GroupByNumber(num int) *Group { // check our sparse map if m.sparseCaps != nil { - if newNum, ok := m.sparseCaps[num]; ok { - num = newNum + newNum, ok := m.sparseCaps[num] + if !ok { + return nil } + num = newNum } if num >= len(m.matchcount) || num < 0 { return nil diff --git a/vendor/github.com/dlclark/regexp2/v2/regexp.go b/vendor/github.com/dlclark/regexp2/v2/regexp.go index c544a572..63220707 100644 --- a/vendor/github.com/dlclark/regexp2/v2/regexp.go +++ b/vendor/github.com/dlclark/regexp2/v2/regexp.go @@ -13,7 +13,6 @@ import ( "errors" "log" "math" - "sort" "strconv" "sync" "time" @@ -245,7 +244,7 @@ func (re *Regexp) FindStringMatch(s string) (*Match, error) { if !ok { return nil, nil } - return re.findDecodedStringMatch(s, startAt) + return re.findDecodedStringMatch(s, startAt, -1) } // FindRunesMatch searches the input rune slice for a Regexp match @@ -255,24 +254,25 @@ func (re *Regexp) FindRunesMatch(r []rune) (*Match, error) { // FindStringMatchStartingAt searches the input string for a Regexp match starting at the startAt index func (re *Regexp) FindStringMatchStartingAt(s string, startAt int) (*Match, error) { - startAt, ok, err := re.findStringMatchStart(s, startAt) + candidate, ok, err := re.findStringMatchStart(s, startAt) if err != nil { return nil, err } if !ok { return nil, nil } - return re.findDecodedStringMatch(s, startAt) + return re.findDecodedStringMatch(s, candidate, startAt) } -func (re *Regexp) findDecodedStringMatch(s string, startAt int) (*Match, error) { +func (re *Regexp) findDecodedStringMatch(s string, candidate, startAt int) (*Match, error) { // Returned matches retain their rune data, so this path must not consume a // pooled buffer that can never be returned. - d := re.decodeStringInput(s, startAt, false) + d := re.decodeStringInput(s, candidate, false) runner := re.getRunner() defer re.putRunner(runner) text := newStringMatchTextAt(s, d.runes, d.runeOffset, d.byteOffset) - return runner.scan(d.runes, text, d.runeStart, -1, false, re.MatchTimeout) + origin := re.stringSearchOrigin(s, startAt, d.runeStart) + return runner.scan(d.runes, text, origin, d.runeStart, -1, false, re.MatchTimeout) } // FindRunesMatchStartingAt searches the input rune slice for a Regexp match starting at the startAt index @@ -295,22 +295,28 @@ func (re *Regexp) FindAllStringIndex(s string, n int) ([][]int, error) { return nil, nil } - d := re.decodeStringInput(s, startAt, true) + // Index results only need byte offsets. Keep the mapper relative to the + // decoded suffix so neither decoding nor mapping has to count prefix runes. + d := decodeInput(s, startAt, re.decodeFrom(s, startAt), re.optimizations.MaxCachedRuneBufferLength, false) runner := re.getRunner() defer func() { re.putRunner(runner) d.release() }() - byteOffsets := newStringByteMapper(s) + byteOffsets := stringByteMapper{input: s[d.byteOffset:]} + if re.RightToLeft() { + byteOffsets.runePos = len(d.runes) + byteOffsets.bytePos = len(byteOffsets.input) + } if re.quickCode != nil { runner.code = re.quickCode } - return re.findAllRunesIndex(runner, d.runes, d.runeStart, n, func(runeIndex, runeLength int) (int, int) { - if byteOffsets == nil { + origin := re.stringSearchOrigin(s, -1, d.runeStart) + return re.findAllRunesIndex(runner, d.runes, origin, d.runeStart, n, func(runeIndex, runeLength int) (int, int) { + if len(d.runes) == len(byteOffsets.input) { return d.byteOffset + runeIndex, d.byteOffset + runeIndex + runeLength } - start := runeIndex + d.runeOffset - return byteOffsets.byteIndex(start), byteOffsets.byteIndex(start + runeLength) + return d.byteOffset + byteOffsets.byteIndex(runeIndex), d.byteOffset + byteOffsets.byteIndex(runeIndex+runeLength) }) } @@ -331,12 +337,12 @@ func (re *Regexp) FindAllRunesIndex(r []rune, n int) ([][]int, error) { if re.quickCode != nil { runner.code = re.quickCode } - return re.findAllRunesIndex(runner, r, startAt, n, func(runeIndex, runeLength int) (int, int) { + return re.findAllRunesIndex(runner, r, startAt, startAt, n, func(runeIndex, runeLength int) (int, int) { return runeIndex, runeIndex + runeLength }) } -func (re *Regexp) findAllRunesIndex(runner *Runner, input []rune, startAt, n int, makeIndex func(runeIndex, runeLength int) (int, int)) ([][]int, error) { +func (re *Regexp) findAllRunesIndex(runner *Runner, input []rune, origin, startAt, n int, makeIndex func(runeIndex, runeLength int) (int, int)) ([][]int, error) { var out [][]int var flat []int if n > 0 { @@ -347,7 +353,7 @@ func (re *Regexp) findAllRunesIndex(runner *Runner, input []rune, startAt, n int prevEnd := -1 previousMatchLength := -1 for n != 0 { - m, err := runner.scan(input, nil, startAt, previousMatchLength, true, re.MatchTimeout) + m, err := runner.scan(input, nil, origin, startAt, previousMatchLength, true, re.MatchTimeout) if err != nil { return nil, err } @@ -367,46 +373,37 @@ func (re *Regexp) findAllRunesIndex(runner *Runner, input []rune, startAt, n int } startAt = m.textpos + origin = startAt previousMatchLength = m.RuneLength } return out, nil } type stringByteMapper struct { - runeIndexes []int - deltas []int -} - -func newStringByteMapper(s string) *stringByteMapper { - var mapper *stringByteMapper - runeIndex := 0 - delta := 0 - for strIdx, ch := range s { - runeLen := utf8.RuneLen(ch) - if ch == utf8.RuneError { - _, runeLen = utf8.DecodeRuneInString(s[strIdx:]) - } - if runeLen != 1 { - if mapper == nil { - mapper = &stringByteMapper{} - } - delta += runeLen - 1 - mapper.runeIndexes = append(mapper.runeIndexes, runeIndex+1) - mapper.deltas = append(mapper.deltas, delta) - } - runeIndex++ - } - return mapper + input string + runePos int + bytePos int } +// runeIndex is relative to input; each invalid UTF-8 byte counts as one rune. func (m *stringByteMapper) byteIndex(runeIndex int) int { - i := sort.Search(len(m.runeIndexes), func(i int) bool { - return m.runeIndexes[i] > runeIndex - }) - 1 - if i < 0 { - return runeIndex + for m.runePos < runeIndex { + size := 1 + if m.input[m.bytePos] >= utf8.RuneSelf { + _, size = utf8.DecodeRuneInString(m.input[m.bytePos:]) + } + m.bytePos += size + m.runePos++ + } + for m.runePos > runeIndex { + size := 1 + if m.input[m.bytePos-1] >= utf8.RuneSelf { + _, size = utf8.DecodeLastRuneInString(m.input[:m.bytePos]) + } + m.bytePos -= size + m.runePos-- } - return runeIndex + m.deltas[i] + return m.bytePos } // FindNextMatch returns the next match in the same input string as the match parameter. @@ -430,10 +427,6 @@ func (re *Regexp) MatchString(s string) (bool, error) { return re.matchStringAt(s, candidateByteIndex) } - return re.matchString(s) -} - -func (re *Regexp) matchString(s string) (bool, error) { return re.matchStringAt(s, -1) } @@ -468,7 +461,8 @@ func (re *Regexp) matchStringAt(s string, startAt int) (bool, error) { runner.code = re.quickCode } - m, err := runner.scan(input, nil, runeStart, -1, true, re.MatchTimeout) + origin := re.stringSearchOrigin(s, -1, runeStart) + m, err := runner.scan(input, nil, origin, runeStart, -1, true, re.MatchTimeout) if err != nil { return false, err } diff --git a/vendor/github.com/dlclark/regexp2/v2/replace.go b/vendor/github.com/dlclark/regexp2/v2/replace.go index 48739ab7..acf0c66a 100644 --- a/vendor/github.com/dlclark/regexp2/v2/replace.go +++ b/vendor/github.com/dlclark/regexp2/v2/replace.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" + "github.com/dlclark/regexp2/v2/helpers" "github.com/dlclark/regexp2/v2/syntax" ) @@ -27,6 +28,15 @@ func writeRunes(buf *bytes.Buffer, text []rune, start, end int) { } } +func writeUnmatched(buf *bytes.Buffer, input string, d *decodedInput, start, end int) { + // Equal byte/rune lengths can include invalid UTF-8, which must be re-encoded. + if d.ascii { + buf.WriteString(input[start:end]) + } else { + writeRunes(buf, d.runes, start, end) + } +} + func compactBalancedMatches(m *Match) { for cap := 0; cap < len(m.matchcount); cap++ { limit := m.matchcount[cap] * 2 @@ -158,6 +168,16 @@ func replaceRunnerLTR(regex *Regexp, data *syntax.ReplacerData, input string, st if startAt > len(input) { return "", errors.New("startAt must be less than the length of the input string") } + // Short inputs are cheaper to decode directly. For longer inputs, validate + // startAt before rejecting a miss. Keep the original scan start: anchors + // and replacement rules may need context before the candidate. + if len(input) >= helpers.ASCIISearchMin { + if _, ok, err := regex.findStringMatchStart(input, startAt); err != nil { + return "", err + } else if !ok { + return input, nil + } + } runner := regex.getRunner() d := decodeInput(input, startAt, 0, regex.optimizations.MaxCachedRuneBufferLength, false) @@ -175,7 +195,7 @@ func replaceRunnerLTR(regex *Regexp, data *syntax.ReplacerData, input string, st runeStart = 0 } - m, err := runner.scan(text, textInfo, runeStart, -1, true, regex.MatchTimeout) + m, err := runner.scan(text, textInfo, runeStart, runeStart, -1, true, regex.MatchTimeout) if err != nil { return "", err } @@ -196,7 +216,7 @@ func replaceRunnerLTR(regex *Regexp, data *syntax.ReplacerData, input string, st local := m.runeSliceIndex() if local != prevat { - writeRunes(buf, text, prevat, local) + writeUnmatched(buf, input, &d, prevat, local) } prevat = local + m.RuneLength replacementImpl(data, buf, m) @@ -206,14 +226,14 @@ func replaceRunnerLTR(regex *Regexp, data *syntax.ReplacerData, input string, st break } - m, err = runner.scan(text, textInfo, m.textpos, m.RuneLength, true, regex.MatchTimeout) + m, err = runner.scan(text, textInfo, m.textpos, m.textpos, m.RuneLength, true, regex.MatchTimeout) if err != nil { return "", err } } if prevat < len(text) { - writeRunes(buf, text, prevat, len(text)) + writeUnmatched(buf, input, &d, prevat, len(text)) } return buf.String(), nil } @@ -239,7 +259,7 @@ func replaceRunnerRTL(regex *Regexp, data *syntax.ReplacerData, input string, st runeStart = len(text) } - m, err := runner.scan(text, textInfo, runeStart, -1, true, regex.MatchTimeout) + m, err := runner.scan(text, textInfo, runeStart, runeStart, -1, true, regex.MatchTimeout) if err != nil { return "", err } @@ -262,7 +282,12 @@ func replaceRunnerRTL(regex *Regexp, data *syntax.ReplacerData, input string, st local := m.runeSliceIndex() if local+m.RuneLength != prevat { - al = append(al, string(text[local+m.RuneLength:prevat])) + // As in writeUnmatched, only ASCII permits copying original bytes. + if d.ascii { + al = append(al, input[local+m.RuneLength:prevat]) + } else { + al = append(al, string(text[local+m.RuneLength:prevat])) + } } prevat = local replacementImplRTL(data, &al, m) @@ -272,14 +297,14 @@ func replaceRunnerRTL(regex *Regexp, data *syntax.ReplacerData, input string, st break } - m, err = runner.scan(text, textInfo, m.textpos, m.RuneLength, true, regex.MatchTimeout) + m, err = runner.scan(text, textInfo, m.textpos, m.textpos, m.RuneLength, true, regex.MatchTimeout) if err != nil { return "", err } } if prevat > 0 { - writeRunes(buf, text, 0, prevat) + writeUnmatched(buf, input, &d, 0, prevat) } for i := len(al) - 1; i >= 0; i-- { buf.WriteString(al[i]) @@ -322,7 +347,10 @@ func replacementImplRTL(data *syntax.ReplacerData, al *[]string, m *Match) { l := *al buf := &bytes.Buffer{} - for _, r := range data.Rules { + // The caller reverses the complete segment list, so emit each + // replacement's fragments in reverse order as well. + for i := len(data.Rules) - 1; i >= 0; i-- { + r := data.Rules[i] buf.Reset() if r >= 0 { // string lookup l = append(l, data.Strings[r]) diff --git a/vendor/github.com/dlclark/regexp2/v2/runner.go b/vendor/github.com/dlclark/regexp2/v2/runner.go index 6362cebf..58463564 100644 --- a/vendor/github.com/dlclark/regexp2/v2/runner.go +++ b/vendor/github.com/dlclark/regexp2/v2/runner.go @@ -94,26 +94,12 @@ func (re *Regexp) run(quick bool, textstart, previousMatchLength int, input []ru runner.code = re.quickCode } - return runner.scan(input, textInfo, textstart, previousMatchLength, quick, re.MatchTimeout) + return runner.scan(input, textInfo, textstart, textstart, previousMatchLength, quick, re.MatchTimeout) } -// Scans the string to find the first match. Uses the Match object -// both to feed text in and as a place to store matches that come out. -// -// All the action is in the Go() method. Our -// responsibility is to load up the class members before -// calling Go. -// -// The optimizer can compute a set of candidate starting characters, -// and we could use a separate method Skip() that will quickly scan past -// any characters that we know can't match. -// -// The input slice is passed separately from matchText so quick scans can avoid -// allocating match metadata. When textInfo is nil, successful matches are only -// used as a boolean result and capture text is intentionally unavailable. If -// we collapsed down to just textInfo it would "escape" and hit the GC for fast -// scans without captures. -func (r *Runner) scan(rt []rune, textInfo *matchText, textstart, previousMatchLength int, quick bool, timeout time.Duration) (*Match, error) { +// scan starts at candidate while preserving textstart for \G. Both are rune +// indexes in rt. A nil textInfo allows quick scans to omit capture metadata. +func (r *Runner) scan(rt []rune, textInfo *matchText, textstart, candidate, previousMatchLength int, quick bool, timeout time.Duration) (*Match, error) { r.timeout = timeout r.ignoreTimeout = (time.Duration(math.MaxInt64) == timeout) r.debug = r.re.Debug() @@ -132,8 +118,7 @@ func (r *Runner) scan(rt []rune, textInfo *matchText, textstart, previousMatchLe stoppos = 0 } - r.Runtextpos = textstart - //initted := false + r.Runtextpos = candidate // setup our scanner functions findFirstChar := r.re.findFirstChar @@ -290,7 +275,7 @@ func executeDefault(r *Runner) error { } else { // Non-ASCII runes fall back to the complete character sets. for i, setIndex := range table.Sets { - if r.code.Sets[setIndex].CharIn(ch) { + if r.code.Sets[setIndex].Contains(ch) { branch = i break } @@ -719,7 +704,7 @@ func executeDefault(r *Runner) error { case syntax.Set: - if r.forwardchars() < 1 || !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) { + if r.forwardchars() < 1 || !r.code.Sets[r.operand(0)].Contains(r.forwardcharnext()) { break } @@ -808,7 +793,7 @@ func executeDefault(r *Runner) error { set := r.code.Sets[r.operand(0)] for c > 0 { - if !set.CharIn(r.forwardcharnext()) { + if !set.Contains(r.forwardcharnext()) { goto BreakBackward } c-- @@ -879,7 +864,7 @@ func executeDefault(r *Runner) error { i := c for ; i > 0; i-- { - if !set.CharIn(r.forwardcharnext()) { + if !set.Contains(r.forwardcharnext()) { r.backwardnext() break } @@ -996,7 +981,7 @@ func executeDefault(r *Runner) error { pos := r.trackPeekN(1) r.textto(pos) - if !r.code.Sets[r.operand(0)].CharIn(r.forwardcharnext()) { + if !r.code.Sets[r.operand(0)].Contains(r.forwardcharnext()) { break } @@ -1519,8 +1504,8 @@ func findFirstCharDefault(r *Runner) bool { } else { for i := r.forwardchars(); i > 0; i-- { n := r.forwardcharnext() - //fmt.Printf("%v in %v: %v\n", string(n), set.String(), set.CharIn(n)) - if set.CharIn(n) { + //fmt.Printf("%v in %v: %v\n", string(n), set.String(), set.Contains(n)) + if set.Contains(n) { r.backwardnext() return true } @@ -1792,7 +1777,7 @@ func findLiteralAfterLoopLeftToRight(r *Runner, literal *syntax.LiteralAfterLoop } start := literalIndex - for start > r.Runtextpos && literal.LoopNode.Set.CharIn(r.Runtext[start-1]) { + for start > r.Runtextpos && literal.LoopNode.Set.Contains(r.Runtext[start-1]) { start-- } if hasRequiredLengthAt(r, start) { @@ -1832,7 +1817,7 @@ func findRequiredLandmarkChainLeftToRight(r *Runner, chain *syntax.RequiredLandm if candidate < r.Runtextpos { candidate = r.Runtextpos } - for candidate > r.Runtextpos && chain.LeadingLoopSet.CharIn(r.Runtext[candidate-1]) { + for candidate > r.Runtextpos && chain.LeadingLoopSet.Contains(r.Runtext[candidate-1]) { candidate-- } if hasRequiredLengthAt(r, candidate) { @@ -1866,7 +1851,7 @@ func findNextRequiredLandmarkRunes(input []rune, startAt, endAt int, landmark sy func requiredLandmarkAlternativeMatch(input []rune, start, endAt int, alt syntax.RequiredLandmarkAlternative) (requiredLandmarkMatch, bool) { if alt.RequireWhitespaceBefore && - (start == 0 || alt.LeadingWhitespaceSet == nil || !alt.LeadingWhitespaceSet.CharIn(input[start-1])) { + (start == 0 || alt.LeadingWhitespaceSet == nil || !alt.LeadingWhitespaceSet.Contains(input[start-1])) { return requiredLandmarkMatch{}, false } @@ -1882,7 +1867,7 @@ func requiredLandmarkAlternativeMatch(input []rune, start, endAt int, alt syntax if maxRepeat <= 0 { maxRepeat = alt.MinRepeat } - for end < endAt && end-start < maxRepeat && alt.Set.CharIn(input[end]) { + for end < endAt && end-start < maxRepeat && alt.Set.Contains(input[end]) { end++ } if end-start < alt.MinRepeat { @@ -1893,12 +1878,12 @@ func requiredLandmarkAlternativeMatch(input []rune, start, endAt int, alt syntax } if alt.RequireWhitespaceAfter && - (end >= endAt || alt.TrailingWhitespaceSet == nil || !alt.TrailingWhitespaceSet.CharIn(input[end])) { + (end >= endAt || alt.TrailingWhitespaceSet == nil || !alt.TrailingWhitespaceSet.Contains(input[end])) { return requiredLandmarkMatch{}, false } matchStart := start - for matchStart > 0 && alt.LeadingWhitespaceSet != nil && alt.LeadingWhitespaceSet.CharIn(input[matchStart-1]) { + for matchStart > 0 && alt.LeadingWhitespaceSet != nil && alt.LeadingWhitespaceSet.Contains(input[matchStart-1]) { matchStart-- } return requiredLandmarkMatch{Start: matchStart, CoreStart: start, End: end}, true @@ -1985,7 +1970,7 @@ func charInFixedDistanceSet(set syntax.FixedDistanceSet, ch rune) bool { } return found } - return set.Set != nil && set.Set.CharIn(ch) + return set.Set != nil && set.Set.Contains(ch) } func latestPossibleStart(r *Runner) int { diff --git a/vendor/github.com/dlclark/regexp2/v2/split.go b/vendor/github.com/dlclark/regexp2/v2/split.go index 3c161988..3c123354 100644 --- a/vendor/github.com/dlclark/regexp2/v2/split.go +++ b/vendor/github.com/dlclark/regexp2/v2/split.go @@ -3,6 +3,7 @@ package regexp2 import ( "errors" "math" + "slices" ) // Split splits the given input string using the pattern and returns @@ -30,23 +31,58 @@ func (re *Regexp) Split(input string, count int) ([]string, error) { count = math.MaxInt } - // iterate through the matches + startAt, ok, err := re.findStringMatchStart(input, -1) + if err != nil { + return nil, err + } + if !ok { + return []string{input}, nil + } + d := decodeInput(input, startAt, re.decodeFrom(input, startAt), re.optimizations.MaxCachedRuneBufferLength, false) + runner := re.getRunner() + defer func() { + re.putRunner(runner) + d.release() + }() + text := newStringMatchTextAt(input, d.runes, 0, d.byteOffset) + + // Keep captures in the reusable match, but only materialize output strings. + // Passing text also ensures registered engines use their capturing program. priorIndex := 0 + if re.RightToLeft() { + priorIndex = len(input) + } var retVal []string matched := false - m, err := re.FindStringMatch(input) + origin := re.stringSearchOrigin(input, -1, d.runeStart) + m, err := runner.scan(d.runes, text, origin, d.runeStart, -1, true, re.MatchTimeout) - for ; m != nil && count > 0; m, err = re.FindNextMatch(m) { + for ; m != nil && count > 0; m, err = runner.scan(d.runes, text, m.textpos, m.textpos, m.RuneLength, true, re.MatchTimeout) { + if m.balancing { + compactBalancedMatches(m) + } matched = true start, end := matchInputSpan(m) - retVal = append(retVal, input[priorIndex:start]) - // append any capture groups, skipping group 0 - gs := m.Groups() - for i := 1; i < len(gs); i++ { - retVal = append(retVal, gs[i].String()) + if re.RightToLeft() { + retVal = append(retVal, input[end:priorIndex]) + } else { + retVal = append(retVal, input[priorIndex:start]) + } + // Preserve group order and empty strings for unmatched groups without + // allocating Group objects or their capture histories. + for group := 1; group < len(m.matchcount); group++ { + value := "" + if m.matchcount[group] > 0 { + capture := newCapture(text, m.matchIndex(group), m.matchLength(group)) + value = capture.String() + } + retVal = append(retVal, value) } priorIndex = end + if re.RightToLeft() { + priorIndex = start + } count-- } @@ -58,6 +94,11 @@ func (re *Regexp) Split(input string, count int) ([]string, error) { return []string{input}, nil } - retVal = append(retVal, input[priorIndex:]) + if re.RightToLeft() { + retVal = append(retVal, input[:priorIndex]) + slices.Reverse(retVal) + } else { + retVal = append(retVal, input[priorIndex:]) + } return retVal, nil } diff --git a/vendor/github.com/dlclark/regexp2/v2/stringprefixfilter.go b/vendor/github.com/dlclark/regexp2/v2/stringprefixfilter.go index 080b8c29..a041ad23 100644 --- a/vendor/github.com/dlclark/regexp2/v2/stringprefixfilter.go +++ b/vendor/github.com/dlclark/regexp2/v2/stringprefixfilter.go @@ -140,6 +140,26 @@ func (s asciiSetStringScanner) index(input string) int { return -1 } +// A literal U+FFFD also matches invalid UTF-8 bytes once decoded. Keep the +// byte filter for valid strings, and let the rune engine handle invalid input. +// Ordinary literals return the original filter without a per-search check. +func withRuneErrorFallback(filter StringPrefixFilter, literals ...string) StringPrefixFilter { + for _, literal := range literals { + if strings.ContainsRune(literal, utf8.RuneError) { + return func(input string, startAt int) (int, bool) { + if startAt < 0 || startAt > len(input) { + return 0, false + } + if !utf8.ValidString(input[startAt:]) { + return startAt, true + } + return filter(input, startAt) + } + } + } + return filter +} + func stringIndexPrefixFilter(prefix string, ignoreCase bool, minRequiredLength int) StringPrefixFilter { if prefix == "" { return nil @@ -148,7 +168,7 @@ func stringIndexPrefixFilter(prefix string, ignoreCase bool, minRequiredLength i return nil } - return func(input string, startAt int) (candidateByteIndex int, ok bool) { + return withRuneErrorFallback(func(input string, startAt int) (candidateByteIndex int, ok bool) { if !hasMinRequiredBytes(input, startAt, minRequiredLength) { return 0, false } @@ -163,7 +183,7 @@ func stringIndexPrefixFilter(prefix string, ignoreCase bool, minRequiredLength i return 0, false } return startAt + offset, true - } + }, prefix) } func stringIndexPrefixesFilter(prefixes []string, ignoreCase bool, minRequiredLength int) StringPrefixFilter { @@ -182,9 +202,9 @@ func stringIndexPrefixesFilter(prefixes []string, ignoreCase bool, minRequiredLe return filter.index } - return func(input string, startAt int) (candidateByteIndex int, ok bool) { + return withRuneErrorFallback(func(input string, startAt int) (candidateByteIndex int, ok bool) { return indexAnyPrefixFallback(input, startAt, prefixes, ignoreCase, minRequiredLength) - } + }, prefixes...) } func indexAnyPrefixFallback(input string, startAt int, prefixes []string, ignoreCase bool, minRequiredLength int) (candidateByteIndex int, ok bool) { @@ -322,7 +342,7 @@ func stringFixedDistanceStringFilter(literal string, distance, minRequiredLength return nil } - return func(input string, startAt int) (candidateByteIndex int, ok bool) { + return withRuneErrorFallback(func(input string, startAt int) (candidateByteIndex int, ok bool) { if !hasMinRequiredBytes(input, startAt, minRequiredLength) { return 0, false } @@ -344,7 +364,7 @@ func stringFixedDistanceStringFilter(literal string, distance, minRequiredLength searchAt = literalIndex + 1 } return 0, false - } + }, literal) } func stringLiteralAfterLoopFilter(literal *syntax.LiteralAfterLoop, minRequiredLength int) StringPrefixFilter { @@ -355,7 +375,7 @@ func stringLiteralAfterLoopFilter(literal *syntax.LiteralAfterLoop, minRequiredL return nil } - return func(input string, startAt int) (candidateByteIndex int, ok bool) { + return withRuneErrorFallback(func(input string, startAt int) (candidateByteIndex int, ok bool) { if !hasMinRequiredBytes(input, startAt, minRequiredLength) { return 0, false } @@ -363,7 +383,7 @@ func stringLiteralAfterLoopFilter(literal *syntax.LiteralAfterLoop, minRequiredL return 0, false } return startAt, true - } + }, literal.String) } func stringHasLiteralAfterLoop(input string, searchAt int, literal *syntax.LiteralAfterLoop) bool { diff --git a/vendor/github.com/dlclark/regexp2/v2/syntax/charclass.go b/vendor/github.com/dlclark/regexp2/v2/syntax/charclass.go index b00fc9ad..30f61fa1 100644 --- a/vendor/github.com/dlclark/regexp2/v2/syntax/charclass.go +++ b/vendor/github.com/dlclark/regexp2/v2/syntax/charclass.go @@ -295,13 +295,20 @@ func NewCharSetRuntime(buf string) CharSet { // CharIn returns true if the rune is in our character set (either ranges or categories). // It handles negations and subtracted sub-charsets. func (c CharSet) CharIn(ch rune) bool { + return c.Contains(ch) +} + +// Contains reports whether ch is in the character set, including negation and +// subtraction. It is equivalent to CharIn but avoids copying the set on each +// call when testing many runes. +func (c *CharSet) Contains(ch rune) bool { if ch >= 0 && ch < 128 && c.ascii != nil { return (c.ascii.bits[ch/64] & (1 << (uint(ch) % 64))) != 0 } return c.charInSlow(ch) } -func (c CharSet) charInSlow(ch rune) bool { +func (c *CharSet) charInSlow(ch rune) bool { val := false // in s && !s.subtracted @@ -346,7 +353,7 @@ func (c CharSet) charInSlow(ch rune) bool { // get subtracted recurse if val && c.sub != nil { - val = !c.sub.CharIn(ch) + val = !c.sub.Contains(ch) } //log.Printf("Char '%v' in %v == %v", string(ch), c.String(), val) diff --git a/vendor/modules.txt b/vendor/modules.txt index 18d14b83..628f23c0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -41,7 +41,7 @@ github.com/clipperhouse/displaywidth # github.com/clipperhouse/uax29/v2 v2.7.0 ## explicit; go 1.18 github.com/clipperhouse/uax29/v2/graphemes -# github.com/dlclark/regexp2/v2 v2.7.1 +# github.com/dlclark/regexp2/v2 v2.7.2 ## explicit; go 1.25 github.com/dlclark/regexp2/v2 github.com/dlclark/regexp2/v2/helpers