Files
Dmitry VerkhoturovandUmputun 09110c792f Bump backend Go modules to latest
Updates every backend dependency with a newer release available, and
tidies the example module alongside as any change to backend/go.mod
requires.
2026-08-19 03:39:11 -05:00

356 lines
8.1 KiB
Go

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
}