package regexp2 import ( "bytes" "errors" "github.com/dlclark/regexp2/v2/syntax" ) const ( replaceSpecials = 4 replaceLeftPortion = -1 replaceRightPortion = -2 replaceLastGroup = -3 replaceWholeString = -4 ) // MatchEvaluator is a function that takes a match and returns a replacement string to be used type MatchEvaluator func(Match) string // Three very similar algorithms appear below: replace (pattern), // replace (evaluator), and split. func writeRunes(buf *bytes.Buffer, text []rune, start, end int) { for i := start; i < end; i++ { buf.WriteRune(text[i]) } } func compactBalancedMatches(m *Match) { for cap := 0; cap < len(m.matchcount); cap++ { limit := m.matchcount[cap] * 2 matcharray := m.matches[cap] var i, j int for i = 0; i < limit; i++ { if matcharray[i] < 0 { break } } for j = i; i < limit; i++ { if matcharray[i] < 0 { j-- } else { if i != j { matcharray[j] = matcharray[i] } j++ } } m.matchcount[cap] = j / 2 } m.balancing = false } // Replace Replaces all occurrences of the regex in the string with the // replacement pattern. // // Note that the special case of no matches is handled on its own: // with no matches, the input string is returned unchanged. // The right-to-left case is split out because StringBuilder // doesn't handle right-to-left string building directly very well. func replace(regex *Regexp, data *syntax.ReplacerData, evaluator MatchEvaluator, input string, startAt, count int) (string, error) { if count < -1 { return "", errors.New("count too small") } if count == 0 { return "", nil } if evaluator == nil { if !regex.RightToLeft() { return replaceRunnerLTR(regex, data, input, startAt, count) } return replaceRunnerRTL(regex, data, input, startAt, count) } m, err := regex.FindStringMatchStartingAt(input, startAt) if err != nil { return "", err } if m == nil { return input, nil } buf := &bytes.Buffer{} if !regex.RightToLeft() { prevat := 0 for m != nil { start, end := matchInputSpan(m) if start > prevat { buf.WriteString(input[prevat:start]) } prevat = end buf.WriteString(evaluator(*m)) count-- if count == 0 { break } m, err = regex.FindNextMatch(m) if err != nil { return "", err } } if prevat < len(input) { buf.WriteString(input[prevat:]) } } else { prevat := len(input) var al []string for m != nil { start, end := matchInputSpan(m) if end < prevat { al = append(al, input[end:prevat]) } prevat = start al = append(al, evaluator(*m)) count-- if count == 0 { break } m, err = regex.FindNextMatch(m) if err != nil { return "", err } } if prevat > 0 { buf.WriteString(input[:prevat]) } for i := len(al) - 1; i >= 0; i-- { buf.WriteString(al[i]) } } return buf.String(), nil } // matchInputSpan returns the UTF-8 byte range of m in its original string input. func matchInputSpan(m *Match) (start, end int) { if m.text != nil && m.text.hasStringInput { start, length := m.ByteRange() return start, start + length } return m.RuneIndex, m.RuneIndex + m.RuneLength } func replaceRunnerLTR(regex *Regexp, data *syntax.ReplacerData, input string, startAt, count int) (string, error) { if startAt > len(input) { return "", errors.New("startAt must be less than the length of the input string") } runner := regex.getRunner() d := decodeInput(input, startAt, 0, regex.optimizations.MaxCachedRuneBufferLength, false) text := d.runes textInfo := newStringMatchText(input, text) defer func() { regex.putRunner(runner) d.release() }() runeStart := d.runeStart if startAt >= 0 && runeStart < 0 { return "", errors.New("startAt must align to the start of a valid rune in the input string") } if runeStart < 0 { runeStart = 0 } m, err := runner.scan(text, textInfo, runeStart, -1, true, regex.MatchTimeout) if err != nil { return "", err } if m == nil { return input, nil } buf, pooledBuf := getPooledReplaceBuffer(len(input), regex.optimizations.MaxCachedReplaceBufferLength) if pooledBuf != nil { defer putPooledReplaceBuffer(buf, pooledBuf) } prevat := 0 for m != nil { if m.balancing { compactBalancedMatches(m) } local := m.runeSliceIndex() if local != prevat { writeRunes(buf, text, prevat, local) } prevat = local + m.RuneLength replacementImpl(data, buf, m) count-- if count == 0 { break } m, err = runner.scan(text, textInfo, m.textpos, m.RuneLength, true, regex.MatchTimeout) if err != nil { return "", err } } if prevat < len(text) { writeRunes(buf, text, prevat, len(text)) } return buf.String(), nil } func replaceRunnerRTL(regex *Regexp, data *syntax.ReplacerData, input string, startAt, count int) (string, error) { if startAt > len(input) { return "", errors.New("startAt must be less than the length of the input string") } runner := regex.getRunner() d := decodeInput(input, startAt, 0, regex.optimizations.MaxCachedRuneBufferLength, false) text := d.runes textInfo := newStringMatchText(input, text) defer func() { regex.putRunner(runner) d.release() }() runeStart := d.runeStart if startAt >= 0 && runeStart < 0 { return "", errors.New("startAt must align to the start of a valid rune in the input string") } if runeStart < 0 { runeStart = len(text) } m, err := runner.scan(text, textInfo, runeStart, -1, true, regex.MatchTimeout) if err != nil { return "", err } if m == nil { return input, nil } buf, pooledBuf := getPooledReplaceBuffer(len(input), regex.optimizations.MaxCachedReplaceBufferLength) if pooledBuf != nil { defer putPooledReplaceBuffer(buf, pooledBuf) } prevat := len(text) var al []string for m != nil { if m.balancing { compactBalancedMatches(m) } local := m.runeSliceIndex() if local+m.RuneLength != prevat { al = append(al, string(text[local+m.RuneLength:prevat])) } prevat = local replacementImplRTL(data, &al, m) count-- if count == 0 { break } m, err = runner.scan(text, textInfo, m.textpos, m.RuneLength, true, regex.MatchTimeout) if err != nil { return "", err } } if prevat > 0 { writeRunes(buf, text, 0, prevat) } for i := len(al) - 1; i >= 0; i-- { buf.WriteString(al[i]) } return buf.String(), nil } // Given a Match, emits into the StringBuilder the evaluated // substitution pattern. func replacementImpl(data *syntax.ReplacerData, buf *bytes.Buffer, m *Match) { for _, r := range data.Rules { if r >= 0 { // string lookup buf.WriteString(data.Strings[r]) } else if r < -replaceSpecials { // group lookup m.groupValueAppendToBuf(-replaceSpecials-1-r, buf) } else { switch -replaceSpecials - 1 - r { // special insertion patterns case replaceLeftPortion: end := m.runeSliceIndex() for i := 0; i < end; i++ { buf.WriteRune(m.text.runes[i]) } case replaceRightPortion: for i := m.runeSliceIndex() + m.RuneLength; i < len(m.text.runes); i++ { buf.WriteRune(m.text.runes[i]) } case replaceLastGroup: m.groupValueAppendToBuf(m.GroupCount()-1, buf) case replaceWholeString: for i := 0; i < len(m.text.runes); i++ { buf.WriteRune(m.text.runes[i]) } } } } } func replacementImplRTL(data *syntax.ReplacerData, al *[]string, m *Match) { l := *al buf := &bytes.Buffer{} for _, r := range data.Rules { buf.Reset() if r >= 0 { // string lookup l = append(l, data.Strings[r]) } else if r < -replaceSpecials { // group lookup m.groupValueAppendToBuf(-replaceSpecials-1-r, buf) l = append(l, buf.String()) } else { switch -replaceSpecials - 1 - r { // special insertion patterns case replaceLeftPortion: end := m.runeSliceIndex() for i := 0; i < end; i++ { buf.WriteRune(m.text.runes[i]) } case replaceRightPortion: for i := m.runeSliceIndex() + m.RuneLength; i < len(m.text.runes); i++ { buf.WriteRune(m.text.runes[i]) } case replaceLastGroup: m.groupValueAppendToBuf(m.GroupCount()-1, buf) case replaceWholeString: for i := 0; i < len(m.text.runes); i++ { buf.WriteRune(m.text.runes[i]) } } l = append(l, buf.String()) } } *al = l }