package regexp2
import (
"container/list"
"errors"
"log"
"math"
"sort"
"strconv"
"sync"
"time"
"unicode/utf8"
"github.com/dlclark/regexp2/v2/syntax"
)
var (
DefaultMatchTimeout = time .Duration (math .MaxInt64 )
ErrBacktrackingStackLimit = errors .New ("regexp2: maximum backtracking stack size exceeded" )
)
type Regexp struct {
MatchTimeout time .Duration
pattern string
options RegexOptions
debug bool
caps map [int ]int
capnames map [string ]int
capslist []string
capsize int
code *syntax .Code
optimizations OptimizationOptions
runnerPool *sync .Pool
replaceCache *replacerDataCache
findFirstChar func (r *Runner ) bool
execute func (r *Runner ) error
executeQuick func (r *Runner ) error
stringPrefixFilter StringPrefixFilter
quickCode *syntax .Code
}
func Compile (expr string , options ...CompileOption ) (*Regexp , error ) {
c := newCompileConfig (options )
return compile (expr , c )
}
func compile(expr string , c compileConfig ) (*Regexp , error ) {
parseOptions := syntax .ParseOptions {
RegexOptions : syntax .RegexOptions (c .regexOptions ),
MaintainCaptureOrder : c .maintainCaptureOrder ,
CodeGen : c .codeGen ,
}
tree , err := syntax .Parse (expr , parseOptions )
if err != nil {
return nil , err
}
if c .debug {
log .Print (tree .Dump ())
}
code , err := syntax .Write (tree )
if err != nil {
return nil , err
}
if c .debug {
log .Print (code .Dump ())
}
if !c .optimizations .DisableCharClassASCIIBitmap {
code .PrepareCharSetASCIIBitmaps ()
}
re := &Regexp {
pattern : expr ,
options : c .regexOptions ,
debug : c .debug ,
caps : code .Caps ,
capnames : tree .Capnames ,
capslist : tree .Caplist ,
capsize : code .Capsize ,
code : code ,
quickCode : makeQuickCode (code ),
MatchTimeout : DefaultMatchTimeout ,
optimizations : c .optimizations ,
}
re .stringPrefixFilter = newStringPrefixFilter (code )
re .initCaches ()
return re , nil
}
func makeQuickCode(code *syntax .Code ) *syntax .Code {
if code == nil || len (code .QuickCodes ) == 0 {
return nil
}
quick := *code
quick .Codes = code .QuickCodes
quick .Dispatches = code .QuickDispatches
quick .QuickCodes = nil
quick .QuickDispatches = nil
return &quick
}
func MustCompile (str string , options ...CompileOption ) *Regexp {
c := newCompileConfig (options )
regexp := getEngineRegexp (str , c )
if regexp != nil {
return regexp
}
regexp , err := compile (str , c )
if err != nil {
panic (`regexp2: Compile(` + quote (str ) + `): ` + err .Error())
}
return regexp
}
func Escape (input string ) string {
return syntax .Escape (input )
}
func Unescape (input string ) (string , error ) {
return syntax .Unescape (input )
}
func SetTimeoutCheckPeriod (d time .Duration ) {
clockPeriod = d
}
func StopTimeoutClock () {
stopClock ()
}
func (re *Regexp ) String () string {
return re .pattern
}
func quote(s string ) string {
if strconv .CanBackquote (s ) {
return "`" + s + "`"
}
return strconv .Quote (s )
}
func (re *Regexp ) RightToLeft () bool {
return re .options &RightToLeft != 0
}
func (re *Regexp ) Debug () bool {
return re .debug
}
func (re *Regexp ) Replace (input , replacement string , startAt , count int ) (string , error ) {
data , err := re .getReplacerData (replacement )
if err != nil {
return "" , err
}
return replace (re , data , nil , input , startAt , count )
}
func (re *Regexp ) getReplacerData (replacement string ) (*syntax .ReplacerData , error ) {
shouldCache := re .replaceCache != nil && re .optimizations .cacheReplacerData (replacement )
if shouldCache {
if data , ok := re .replaceCache .get (replacement ); ok {
return data , nil
}
}
data , err := syntax .NewReplacerData (replacement , re .caps , re .capsize , re .capnames , syntax .RegexOptions (re .options ))
if err != nil {
return nil , err
}
if shouldCache {
re .replaceCache .add (replacement , data )
}
return data , nil
}
func (re *Regexp ) ReplaceFunc (input string , evaluator MatchEvaluator , startAt , count int ) (string , error ) {
return replace (re , nil , evaluator , input , startAt , count )
}
func (re *Regexp ) FindStringMatch (s string ) (*Match , error ) {
startAt , ok , err := re .findStringMatchStart (s , -1 )
if err != nil {
return nil , err
}
if !ok {
return nil , nil
}
r , runeStart := re .getRunesAndStart (s , startAt )
if runeStart < 0 {
runeStart = 0
}
return re .run (false , runeStart , -1 , r , newStringMatchText (s , r ))
}
func (re *Regexp ) FindRunesMatch (r []rune ) (*Match , error ) {
return re .run (false , -1 , -1 , r , newMatchText (r ))
}
func (re *Regexp ) FindStringMatchStartingAt (s string , startAt int ) (*Match , error ) {
startAt , ok , err := re .findStringMatchStart (s , startAt )
if err != nil {
return nil , err
}
if !ok {
return nil , nil
}
r , startAt := re .getRunesAndStart (s , startAt )
if startAt == -1 {
return nil , errors .New ("startAt must align to the start of a valid rune in the input string" )
}
return re .run (false , startAt , -1 , r , newStringMatchText (s , r ))
}
func (re *Regexp ) FindRunesMatchStartingAt (r []rune , startAt int ) (*Match , error ) {
return re .run (false , startAt , -1 , r , newMatchText (r ))
}
func (re *Regexp ) FindAllStringIndex (s string , n int ) ([][]int , error ) {
if n == 0 {
return nil , nil
}
startAt , ok , err := re .findStringMatchStart (s , -1 )
if err != nil {
return nil , err
}
if !ok {
return nil , nil
}
runner := re .getRunner ()
var input []rune
var pooledInput *[]rune
runeStart := 0
if startAt == 0 {
input , pooledInput = runner .decodeString (s )
} else {
input , runeStart , pooledInput = runner .decodeStringWithStart (s , startAt )
}
defer func () {
re .putRunner (runner )
if pooledInput != nil {
*pooledInput = input
pooledRuneBuffers .put (pooledInput )
}
}()
if runeStart < 0 {
runeStart = 0
}
byteOffsets := newStringByteMapper (s )
if re .quickCode != nil {
runner .code = re .quickCode
}
return re .findAllRunesIndex (runner , input , runeStart , n , func (runeIndex , runeLength int ) (int , int ) {
if byteOffsets == nil {
return runeIndex , runeIndex + runeLength
}
return byteOffsets .byteIndex (runeIndex ), byteOffsets .byteIndex (runeIndex + runeLength )
})
}
func (re *Regexp ) FindAllRunesIndex (r []rune , n int ) ([][]int , error ) {
if n == 0 {
return nil , nil
}
runner := re .getRunner ()
defer re .putRunner (runner )
startAt := 0
if re .RightToLeft () {
startAt = len (r )
}
if re .quickCode != nil {
runner .code = re .quickCode
}
return re .findAllRunesIndex (runner , r , 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 ) {
var out [][]int
var flat []int
if n > 0 {
out = make ([][]int , 0 , n )
flat = make ([]int , 0 , n *2 )
}
prevEnd := -1
previousMatchLength := -1
for n != 0 {
m , err := runner .scan (input , nil , startAt , previousMatchLength , true , re .MatchTimeout )
if err != nil {
return nil , err
}
if m == nil {
break
}
if m .RuneLength != 0 || m .RuneIndex != prevEnd {
start , end := makeIndex (m .RuneIndex , m .RuneLength )
flat = append (flat , start , end )
out = append (out , flat [len (flat )-2 :len (flat ):len (flat )])
prevEnd = m .RuneIndex + m .RuneLength
if n > 0 {
n --
}
}
startAt = m .textpos
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
}
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
}
return runeIndex + m .deltas [i ]
}
func (re *Regexp ) FindNextMatch (m *Match ) (*Match , error ) {
if m == nil {
return nil , nil
}
return re .run (false , m .textpos , m .RuneLength , m .text .runes , m .text )
}
func (re *Regexp ) MatchString (s string ) (bool , error ) {
if re .stringPrefixFilter != nil && !re .RightToLeft () {
candidateByteIndex , ok := re .stringPrefixFilter (s , 0 )
if !ok {
return false , nil
}
return re .matchStringAt (s , candidateByteIndex )
}
return re .matchString (s )
}
func (re *Regexp ) matchString (s string ) (bool , error ) {
return re .matchStringAt (s , -1 )
}
func (re *Regexp ) matchStringAt (s string , startAt int ) (bool , error ) {
runner := re .getRunner ()
var input []rune
var pooledInput *[]rune
runeStart := 0
if startAt <= 0 {
input , pooledInput = runner .decodeString (s )
if re .RightToLeft () {
runeStart = len (input )
}
} else {
input , runeStart , pooledInput = runner .decodeStringWithStart (s , startAt )
if runeStart < 0 {
runeStart = 0
}
}
defer func () {
re .putRunner (runner )
if pooledInput != nil {
*pooledInput = input
pooledRuneBuffers .put (pooledInput )
}
}()
if re .quickCode != nil {
runner .code = re .quickCode
}
m , err := runner .scan (input , nil , runeStart , -1 , true , re .MatchTimeout )
if err != nil {
return false , err
}
return m != nil , nil
}
func (re *Regexp ) getRunesAndStart (s string , startAt int ) ([]rune , int ) {
if startAt < 0 {
if re .RightToLeft () {
r := getRunes (s )
return r , len (r )
}
return getRunes (s ), 0
}
ret := make ([]rune , len (s ))
i := 0
runeIdx := -1
for strIdx , r := range s {
if strIdx == startAt {
runeIdx = i
}
ret [i ] = r
i ++
}
if startAt == len (s ) {
runeIdx = i
}
return ret [:i ], runeIdx
}
func getRunes(s string ) []rune {
return []rune (s )
}
func (re *Regexp ) MatchRunes (r []rune ) (bool , error ) {
m , err := re .run (true , -1 , -1 , r , nil )
if err != nil {
return false , err
}
return m != nil , nil
}
func (re *Regexp ) GetGroupNames () []string {
var result []string
if re .capslist == nil {
result = make ([]string , re .capsize )
for i := 0 ; i < len (result ); i ++ {
result [i ] = strconv .Itoa (i )
}
} else {
result = make ([]string , len (re .capslist ))
copy (result , re .capslist )
}
return result
}
func (re *Regexp ) GetGroupNumbers () []int {
var result []int
if re .caps == nil {
result = make ([]int , re .capsize )
for i := 0 ; i < len (result ); i ++ {
result [i ] = i
}
} else {
result = make ([]int , len (re .caps ))
for k , v := range re .caps {
result [v ] = k
}
}
return result
}
func (re *Regexp ) GroupNameFromNumber (i int ) string {
if re .capslist == nil {
if i >= 0 && i < re .capsize {
return strconv .Itoa (i )
}
return ""
}
if re .caps != nil {
var ok bool
if i , ok = re .caps [i ]; !ok {
return ""
}
}
if i >= 0 && i < len (re .capslist ) {
return re .capslist [i ]
}
return ""
}
func (re *Regexp ) GroupNumberFromName (name string ) int {
if re .capnames != nil {
if k , ok := re .capnames [name ]; ok {
return k
}
return -1
}
result := 0
for i := 0 ; i < len (name ); i ++ {
ch := name [i ]
if ch > '9' || ch < '0' {
return -1
}
result *= 10
result += int (ch - '0' )
}
if result >= 0 && result < re .capsize {
return result
}
return -1
}
func (re *Regexp ) MarshalText () ([]byte , error ) {
return []byte (re .String ()), nil
}
func (re *Regexp ) UnmarshalText (text []byte ) error {
newRE , err := Compile (string (text ), DefaultUnmarshalOptions )
if err != nil {
return err
}
*re = *newRE
return nil
}
func (re *Regexp ) initCaches () {
re .runnerPool = &sync .Pool {
New : func () any {
return &Runner {
re : re ,
code : re .code ,
}
},
}
if re .optimizations .MaxCachedReplacerDataEntries > 0 {
re .replaceCache = newReplacerDataCache (re .optimizations .MaxCachedReplacerDataEntries )
}
}
type replacerDataCache struct {
mu sync .Mutex
maxSize int
ll *list .List
cache map [string ]*list .Element
}
type replacerDataCacheEntry struct {
key string
data *syntax .ReplacerData
}
func newReplacerDataCache(maxSize int ) *replacerDataCache {
return &replacerDataCache {
maxSize : maxSize ,
ll : list .New (),
cache : make (map [string ]*list .Element ),
}
}
func (c *replacerDataCache ) get (key string ) (*syntax .ReplacerData , bool ) {
c .mu .Lock ()
defer c .mu .Unlock ()
if ele , ok := c .cache [key ]; ok {
c .ll .MoveToFront (ele )
return ele .Value .(*replacerDataCacheEntry ).data , true
}
return nil , false
}
func (c *replacerDataCache ) add (key string , data *syntax .ReplacerData ) {
c .mu .Lock ()
defer c .mu .Unlock ()
if ele , ok := c .cache [key ]; ok {
ele .Value .(*replacerDataCacheEntry ).data = data
c .ll .MoveToFront (ele )
return
}
ele := c .ll .PushFront (&replacerDataCacheEntry {key : key , data : data })
c .cache [key ] = ele
if c .maxSize > 0 && c .ll .Len () > c .maxSize {
oldest := c .ll .Back ()
if oldest != nil {
c .ll .Remove (oldest )
delete (c .cache , oldest .Value .(*replacerDataCacheEntry ).key )
}
}
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .