package syntax
import (
"fmt"
"math"
"os"
"sort"
"strconv"
"unicode"
)
type RegexOptions int32
const (
IgnoreCase RegexOptions = 0x0001
Multiline = 0x0002
ExplicitCapture = 0x0004
Compiled = 0x0008
Singleline = 0x0010
IgnorePatternWhitespace = 0x0020
RightToLeft = 0x0040
Debug = 0x0080
ECMAScript = 0x0100
RE2 = 0x0200
Unicode = 0x0400
)
func optionFromCode(ch rune ) RegexOptions {
switch ch {
case 'i' , 'I' :
return IgnoreCase
case 'r' , 'R' :
return RightToLeft
case 'm' , 'M' :
return Multiline
case 'n' , 'N' :
return ExplicitCapture
case 's' , 'S' :
return Singleline
case 'x' , 'X' :
return IgnorePatternWhitespace
case 'd' , 'D' :
return Debug
case 'e' , 'E' :
return ECMAScript
case 'u' , 'U' :
return Unicode
default :
return 0
}
}
type Error struct {
Code ErrorCode
Expr string
Args []interface {}
}
func (e *Error ) Error () string {
if len (e .Args ) == 0 {
return "error parsing regexp: " + e .Code .String () + " in `" + e .Expr + "`"
}
return "error parsing regexp: " + fmt .Sprintf (e .Code .String (), e .Args ...) + " in `" + e .Expr + "`"
}
type ErrorCode string
const (
ErrInternalError ErrorCode = "regexp/syntax: internal error"
ErrUnterminatedComment = "unterminated comment"
ErrInvalidCharRange = "invalid character class range"
ErrInvalidRepeatSize = "invalid repeat count"
ErrInvalidUTF8 = "invalid UTF-8"
ErrCaptureGroupOutOfRange = "capture group number out of range"
ErrUnexpectedParen = "unexpected )"
ErrMissingParen = "missing closing )"
ErrMissingBrace = "missing closing }"
ErrInvalidRepeatOp = "invalid nested repetition operator"
ErrMissingRepeatArgument = "missing argument to repetition operator"
ErrConditionalExpression = "illegal conditional (?(...)) expression"
ErrTooManyAlternates = "too many | in (?()|)"
ErrUnrecognizedGrouping = "unrecognized grouping construct: (%v"
ErrInvalidGroupName = "invalid group name: group names must begin with a word character and have a matching terminator"
ErrCapNumNotZero = "capture number cannot be zero"
ErrUndefinedBackRef = "reference to undefined group number %v"
ErrUndefinedNameRef = "reference to undefined group name %v"
ErrAlternationCantCapture = "alternation conditions do not capture and cannot be named"
ErrAlternationCantHaveComment = "alternation conditions cannot be comments"
ErrMalformedReference = "(?(%v) ) malformed"
ErrUndefinedReference = "(?(%v) ) reference to undefined group"
ErrIllegalEndEscape = "illegal \\ at end of pattern"
ErrMalformedSlashP = "malformed \\p{X} character escape"
ErrIncompleteSlashP = "incomplete \\p{X} character escape"
ErrUnknownSlashP = "unknown unicode category, script, or property '%v'"
ErrUnrecognizedEscape = "unrecognized escape sequence \\%v"
ErrMissingControl = "missing control character"
ErrUnrecognizedControl = "unrecognized control character"
ErrTooFewHex = "insufficient hexadecimal digits"
ErrInvalidHex = "hex values may not be larger than 0x10FFFF"
ErrMalformedNameRef = "malformed \\k<...> named back reference"
ErrBadClassInCharRange = "cannot include class \\%v in character range"
ErrUnterminatedBracket = "unterminated [] set"
ErrSubtractionMustBeLast = "a subtraction must be the last element in a character class"
ErrReversedCharRange = "[%c-%c] range in reverse order"
)
func (e ErrorCode ) String () string {
return string (e )
}
type parser struct {
stack *regexNode
group *regexNode
alternation *regexNode
concatenation *regexNode
unit *regexNode
patternRaw string
pattern []rune
currentPos int
specialCase *unicode .SpecialCase
autocap int
capcount int
captop int
capsize int
caps map [int ]int
capnames map [string ]int
capnumlist []int
capnamelist []string
options RegexOptions
optionsStack []RegexOptions
ignoreNextParen bool
}
const (
maxValueDiv10 int = math .MaxInt32 / 10
maxValueMod10 = math .MaxInt32 % 10
)
func Parse (re string , op RegexOptions ) (*RegexTree , error ) {
p := parser {
options : op ,
caps : make (map [int ]int ),
}
p .setPattern (re )
if err := p .countCaptures (); err != nil {
return nil , err
}
p .reset (op )
root , err := p .scanRegex ()
if err != nil {
return nil , err
}
tree := &RegexTree {
root : root ,
caps : p .caps ,
capnumlist : p .capnumlist ,
captop : p .captop ,
Capnames : p .capnames ,
Caplist : p .capnamelist ,
options : op ,
}
if tree .options &Debug > 0 {
os .Stdout .WriteString (tree .Dump ())
}
return tree , nil
}
func (p *parser ) setPattern (pattern string ) {
p .patternRaw = pattern
p .pattern = make ([]rune , 0 , len (pattern ))
for _ , r := range pattern {
p .pattern = append (p .pattern , r )
}
}
func (p *parser ) getErr (code ErrorCode , args ...interface {}) error {
return &Error {Code : code , Expr : p .patternRaw , Args : args }
}
func (p *parser ) noteCaptureSlot (i , pos int ) {
if _ , ok := p .caps [i ]; !ok {
p .caps [i ] = pos
p .capcount ++
if p .captop <= i {
if i == math .MaxInt32 {
p .captop = i
} else {
p .captop = i + 1
}
}
}
}
func (p *parser ) noteCaptureName (name string , pos int ) {
if p .capnames == nil {
p .capnames = make (map [string ]int )
}
if _ , ok := p .capnames [name ]; !ok {
p .capnames [name ] = pos
p .capnamelist = append (p .capnamelist , name )
}
}
func (p *parser ) assignNameSlots () {
if p .capnames != nil {
for _ , name := range p .capnamelist {
for p .isCaptureSlot (p .autocap ) {
p .autocap ++
}
pos := p .capnames [name ]
p .capnames [name ] = p .autocap
p .noteCaptureSlot (p .autocap , pos )
p .autocap ++
}
}
if p .capcount < p .captop {
p .capnumlist = make ([]int , p .capcount )
i := 0
for k := range p .caps {
p .capnumlist [i ] = k
i ++
}
sort .Ints (p .capnumlist )
}
if p .capnames != nil || p .capnumlist != nil {
var oldcapnamelist []string
var next int
var k int
if p .capnames == nil {
oldcapnamelist = nil
p .capnames = make (map [string ]int )
p .capnamelist = []string {}
next = -1
} else {
oldcapnamelist = p .capnamelist
p .capnamelist = []string {}
next = p .capnames [oldcapnamelist [0 ]]
}
for i := 0 ; i < p .capcount ; i ++ {
j := i
if p .capnumlist != nil {
j = p .capnumlist [i ]
}
if next == j {
p .capnamelist = append (p .capnamelist , oldcapnamelist [k ])
k ++
if k == len (oldcapnamelist ) {
next = -1
} else {
next = p .capnames [oldcapnamelist [k ]]
}
} else {
str := strconv .Itoa (j )
p .capnamelist = append (p .capnamelist , str )
p .capnames [str ] = j
}
}
}
}
func (p *parser ) consumeAutocap () int {
r := p .autocap
p .autocap ++
return r
}
func (p *parser ) countCaptures () error {
var ch rune
p .noteCaptureSlot (0 , 0 )
p .autocap = 1
for p .charsRight () > 0 {
pos := p .textpos ()
ch = p .moveRightGetChar ()
switch ch {
case '\\' :
if p .charsRight () > 0 {
p .scanBackslash (true )
}
case '#' :
if p .useOptionX () {
p .moveLeft ()
p .scanBlank ()
}
case '[' :
p .scanCharSet (false , true )
case ')' :
if !p .emptyOptionsStack () {
p .popOptions ()
}
case '(' :
if p .charsRight () >= 2 && p .rightChar (1 ) == '#' && p .rightChar (0 ) == '?' {
p .moveLeft ()
p .scanBlank ()
} else {
p .pushOptions ()
if p .charsRight () > 0 && p .rightChar (0 ) == '?' {
p .moveRight (1 )
if p .charsRight () > 1 && (p .rightChar (0 ) == '<' || p .rightChar (0 ) == '\'' ) {
p .moveRight (1 )
ch = p .rightChar (0 )
if ch != '0' && IsWordChar (ch ) {
if ch >= '1' && ch <= '9' {
dec , err := p .scanDecimal ()
if err != nil {
return err
}
p .noteCaptureSlot (dec , pos )
} else {
p .noteCaptureName (p .scanCapname (), pos )
}
}
} else if p .useRE2 () && p .charsRight () > 2 && (p .rightChar (0 ) == 'P' && p .rightChar (1 ) == '<' ) {
p .moveRight (2 )
ch = p .rightChar (0 )
if IsWordChar (ch ) {
p .noteCaptureName (p .scanCapname (), pos )
}
} else {
p .scanOptions ()
if p .charsRight () > 0 {
if p .rightChar (0 ) == ')' {
p .moveRight (1 )
p .popKeepOptions ()
} else if p .rightChar (0 ) == '(' {
p .ignoreNextParen = true
continue
}
}
}
} else {
if !p .useOptionN () && !p .ignoreNextParen {
p .noteCaptureSlot (p .consumeAutocap (), pos )
}
}
}
p .ignoreNextParen = false
}
}
p .assignNameSlots ()
return nil
}
func (p *parser ) reset (topopts RegexOptions ) {
p .currentPos = 0
p .autocap = 1
p .ignoreNextParen = false
if len (p .optionsStack ) > 0 {
p .optionsStack = p .optionsStack [:0 ]
}
p .options = topopts
p .stack = nil
}
func (p *parser ) scanRegex () (*regexNode , error ) {
ch := '@'
isQuant := false
p .startGroup (newRegexNodeMN (ntCapture , p .options , 0 , -1 ))
for p .charsRight () > 0 {
wasPrevQuantifier := isQuant
isQuant = false
if err := p .scanBlank (); err != nil {
return nil , err
}
startpos := p .textpos ()
if p .useOptionX () {
for p .charsRight () > 0 {
ch = p .rightChar (0 )
if !(!isStopperX (ch ) || (ch == '{' && !p .isTrueQuantifier ())) {
break
}
p .moveRight (1 )
}
} else {
for p .charsRight () > 0 {
ch = p .rightChar (0 )
if !(!isSpecial (ch ) || ch == '{' && !p .isTrueQuantifier ()) {
break
}
p .moveRight (1 )
}
}
endpos := p .textpos ()
p .scanBlank ()
if p .charsRight () == 0 {
ch = '!'
} else if ch = p .rightChar (0 ); isSpecial (ch ) {
isQuant = isQuantifier (ch )
p .moveRight (1 )
} else {
ch = ' '
}
if startpos < endpos {
cchUnquantified := endpos - startpos
if isQuant {
cchUnquantified --
}
wasPrevQuantifier = false
if cchUnquantified > 0 {
p .addToConcatenate (startpos , cchUnquantified , false )
}
if isQuant {
p .addUnitOne (p .charAt (endpos - 1 ))
}
}
switch ch {
case '!' :
goto BreakOuterScan
case ' ' :
goto ContinueOuterScan
case '[' :
cc , err := p .scanCharSet (p .useOptionI (), false )
if err != nil {
return nil , err
}
p .addUnitSet (cc )
case '(' :
p .pushOptions ()
if grouper , err := p .scanGroupOpen (); err != nil {
return nil , err
} else if grouper == nil {
p .popKeepOptions ()
} else {
p .pushGroup ()
p .startGroup (grouper )
}
continue
case '|' :
p .addAlternate ()
goto ContinueOuterScan
case ')' :
if p .emptyStack () {
return nil , p .getErr (ErrUnexpectedParen )
}
if err := p .addGroup (); err != nil {
return nil , err
}
if err := p .popGroup (); err != nil {
return nil , err
}
p .popOptions ()
if p .unit == nil {
goto ContinueOuterScan
}
case '\\' :
n , err := p .scanBackslash (false )
if err != nil {
return nil , err
}
p .addUnitNode (n )
case '^' :
if p .useOptionM () {
p .addUnitType (ntBol )
} else {
p .addUnitType (ntBeginning )
}
case '$' :
if p .useOptionM () {
p .addUnitType (ntEol )
} else {
p .addUnitType (ntEndZ )
}
case '.' :
if p .useOptionS () {
p .addUnitSet (AnyClass ())
} else if p .useOptionE () {
p .addUnitSet (ECMAAnyClass ())
} else {
p .addUnitNotone ('\n' )
}
case '{' , '*' , '+' , '?' :
if p .unit == nil {
if wasPrevQuantifier {
return nil , p .getErr (ErrInvalidRepeatOp )
} else {
return nil , p .getErr (ErrMissingRepeatArgument )
}
}
p .moveLeft ()
default :
return nil , p .getErr (ErrInternalError )
}
if err := p .scanBlank (); err != nil {
return nil , err
}
if p .charsRight () > 0 {
isQuant = p .isTrueQuantifier ()
}
if p .charsRight () == 0 || !isQuant {
p .addConcatenate ()
goto ContinueOuterScan
}
ch = p .moveRightGetChar ()
for p .unit != nil {
var min , max int
var lazy bool
switch ch {
case '*' :
min = 0
max = math .MaxInt32
case '?' :
min = 0
max = 1
case '+' :
min = 1
max = math .MaxInt32
case '{' :
{
var err error
startpos = p .textpos ()
if min , err = p .scanDecimal (); err != nil {
return nil , err
}
max = min
if startpos < p .textpos () {
if p .charsRight () > 0 && p .rightChar (0 ) == ',' {
p .moveRight (1 )
if p .charsRight () == 0 || p .rightChar (0 ) == '}' {
max = math .MaxInt32
} else {
if max , err = p .scanDecimal (); err != nil {
return nil , err
}
}
}
}
if startpos == p .textpos () || p .charsRight () == 0 || p .moveRightGetChar () != '}' {
p .addConcatenate ()
p .textto (startpos - 1 )
goto ContinueOuterScan
}
}
default :
return nil , p .getErr (ErrInternalError )
}
if err := p .scanBlank (); err != nil {
return nil , err
}
if p .charsRight () == 0 || p .rightChar (0 ) != '?' {
lazy = false
} else {
p .moveRight (1 )
lazy = true
}
if min > max {
return nil , p .getErr (ErrInvalidRepeatSize )
}
p .addConcatenate3 (lazy , min , max )
}
ContinueOuterScan :
}
BreakOuterScan :
;
if !p .emptyStack () {
return nil , p .getErr (ErrMissingParen )
}
if err := p .addGroup (); err != nil {
return nil , err
}
return p .unit , nil
}
func (p *parser ) scanReplacement () (*regexNode , error ) {
var c , startpos int
p .concatenation = newRegexNode (ntConcatenate , p .options )
for {
c = p .charsRight ()
if c == 0 {
break
}
startpos = p .textpos ()
for c > 0 && p .rightChar (0 ) != '$' {
p .moveRight (1 )
c --
}
p .addToConcatenate (startpos , p .textpos ()-startpos , true )
if c > 0 {
if p .moveRightGetChar () == '$' {
n , err := p .scanDollar ()
if err != nil {
return nil , err
}
p .addUnitNode (n )
}
p .addConcatenate ()
}
}
return p .concatenation , nil
}
func (p *parser ) scanDollar () (*regexNode , error ) {
if p .charsRight () == 0 {
return newRegexNodeCh (ntOne , p .options , '$' ), nil
}
ch := p .rightChar (0 )
angled := false
backpos := p .textpos ()
lastEndPos := backpos
if ch == '{' && p .charsRight () > 1 {
angled = true
p .moveRight (1 )
ch = p .rightChar (0 )
}
if ch >= '0' && ch <= '9' {
if !angled && p .useOptionE () {
capnum := -1
newcapnum := int (ch - '0' )
p .moveRight (1 )
if p .isCaptureSlot (newcapnum ) {
capnum = newcapnum
lastEndPos = p .textpos ()
}
for p .charsRight () > 0 {
ch = p .rightChar (0 )
if ch < '0' || ch > '9' {
break
}
digit := int (ch - '0' )
if newcapnum > maxValueDiv10 || (newcapnum == maxValueDiv10 && digit > maxValueMod10 ) {
return nil , p .getErr (ErrCaptureGroupOutOfRange )
}
newcapnum = newcapnum *10 + digit
p .moveRight (1 )
if p .isCaptureSlot (newcapnum ) {
capnum = newcapnum
lastEndPos = p .textpos ()
}
}
p .textto (lastEndPos )
if capnum >= 0 {
return newRegexNodeM (ntRef , p .options , capnum ), nil
}
} else {
capnum , err := p .scanDecimal ()
if err != nil {
return nil , err
}
if !angled || p .charsRight () > 0 && p .moveRightGetChar () == '}' {
if p .isCaptureSlot (capnum ) {
return newRegexNodeM (ntRef , p .options , capnum ), nil
}
}
}
} else if angled && IsWordChar (ch ) {
capname := p .scanCapname ()
if p .charsRight () > 0 && p .moveRightGetChar () == '}' {
if p .isCaptureName (capname ) {
return newRegexNodeM (ntRef , p .options , p .captureSlotFromName (capname )), nil
}
}
} else if !angled {
capnum := 1
switch ch {
case '$' :
p .moveRight (1 )
return newRegexNodeCh (ntOne , p .options , '$' ), nil
case '&' :
capnum = 0
case '`' :
capnum = replaceLeftPortion
case '\'' :
capnum = replaceRightPortion
case '+' :
capnum = replaceLastGroup
case '_' :
capnum = replaceWholeString
}
if capnum != 1 {
p .moveRight (1 )
return newRegexNodeM (ntRef , p .options , capnum ), nil
}
}
p .textto (backpos )
return newRegexNodeCh (ntOne , p .options , '$' ), nil
}
func (p *parser ) scanGroupOpen () (*regexNode , error ) {
var ch rune
var nt nodeType
var err error
close := '>'
start := p .textpos ()
if p .charsRight () == 0 || p .rightChar (0 ) != '?' || (p .rightChar (0 ) == '?' && (p .charsRight () > 1 && p .rightChar (1 ) == ')' )) {
if p .useOptionN () || p .ignoreNextParen {
p .ignoreNextParen = false
return newRegexNode (ntGroup , p .options ), nil
}
return newRegexNodeMN (ntCapture , p .options , p .consumeAutocap (), -1 ), nil
}
p .moveRight (1 )
for {
if p .charsRight () == 0 {
break
}
switch ch = p .moveRightGetChar (); ch {
case ':' :
nt = ntGroup
case '=' :
p .options &= ^RightToLeft
nt = ntRequire
case '!' :
p .options &= ^RightToLeft
nt = ntPrevent
case '>' :
nt = ntGreedy
case '\'' :
close = '\''
fallthrough
case '<' :
if p .charsRight () == 0 {
goto BreakRecognize
}
switch ch = p .moveRightGetChar (); ch {
case '=' :
if close == '\'' {
goto BreakRecognize
}
p .options |= RightToLeft
nt = ntRequire
case '!' :
if close == '\'' {
goto BreakRecognize
}
p .options |= RightToLeft
nt = ntPrevent
default :
p .moveLeft ()
capnum := -1
uncapnum := -1
proceed := false
if ch >= '0' && ch <= '9' {
if capnum , err = p .scanDecimal (); err != nil {
return nil , err
}
if !p .isCaptureSlot (capnum ) {
capnum = -1
}
if p .charsRight () > 0 && !(p .rightChar (0 ) == close || p .rightChar (0 ) == '-' ) {
return nil , p .getErr (ErrInvalidGroupName )
}
if capnum == 0 {
return nil , p .getErr (ErrCapNumNotZero )
}
} else if IsWordChar (ch ) {
capname := p .scanCapname ()
if p .isCaptureName (capname ) {
capnum = p .captureSlotFromName (capname )
}
if p .charsRight () > 0 && !(p .rightChar (0 ) == close || p .rightChar (0 ) == '-' ) {
return nil , p .getErr (ErrInvalidGroupName )
}
} else if ch == '-' {
proceed = true
} else {
return nil , p .getErr (ErrInvalidGroupName )
}
if (capnum != -1 || proceed == true ) && p .charsRight () > 0 && p .rightChar (0 ) == '-' {
p .moveRight (1 )
if p .charsRight () == 0 {
return nil , p .getErr (ErrInvalidGroupName )
}
ch = p .rightChar (0 )
if ch >= '0' && ch <= '9' {
if uncapnum , err = p .scanDecimal (); err != nil {
return nil , err
}
if !p .isCaptureSlot (uncapnum ) {
return nil , p .getErr (ErrUndefinedBackRef , uncapnum )
}
if p .charsRight () > 0 && p .rightChar (0 ) != close {
return nil , p .getErr (ErrInvalidGroupName )
}
} else if IsWordChar (ch ) {
uncapname := p .scanCapname ()
if !p .isCaptureName (uncapname ) {
return nil , p .getErr (ErrUndefinedNameRef , uncapname )
}
uncapnum = p .captureSlotFromName (uncapname )
if p .charsRight () > 0 && p .rightChar (0 ) != close {
return nil , p .getErr (ErrInvalidGroupName )
}
} else {
return nil , p .getErr (ErrInvalidGroupName )
}
}
if (capnum != -1 || uncapnum != -1 ) && p .charsRight () > 0 && p .moveRightGetChar () == close {
return newRegexNodeMN (ntCapture , p .options , capnum , uncapnum ), nil
}
goto BreakRecognize
}
case '(' :
parenPos := p .textpos ()
if p .charsRight () > 0 {
ch = p .rightChar (0 )
if ch >= '0' && ch <= '9' {
var capnum int
if capnum , err = p .scanDecimal (); err != nil {
return nil , err
}
if p .charsRight () > 0 && p .moveRightGetChar () == ')' {
if p .isCaptureSlot (capnum ) {
return newRegexNodeM (ntTestref , p .options , capnum ), nil
}
return nil , p .getErr (ErrUndefinedReference , capnum )
}
return nil , p .getErr (ErrMalformedReference , capnum )
} else if IsWordChar (ch ) {
capname := p .scanCapname ()
if p .isCaptureName (capname ) && p .charsRight () > 0 && p .moveRightGetChar () == ')' {
return newRegexNodeM (ntTestref , p .options , p .captureSlotFromName (capname )), nil
}
}
}
nt = ntTestgroup
p .textto (parenPos - 1 )
p .ignoreNextParen = true
charsRight := p .charsRight ()
if charsRight >= 3 && p .rightChar (1 ) == '?' {
rightchar2 := p .rightChar (2 )
if rightchar2 == '#' {
return nil , p .getErr (ErrAlternationCantHaveComment )
}
if rightchar2 == '\'' {
return nil , p .getErr (ErrAlternationCantCapture )
}
if charsRight >= 4 && (rightchar2 == '<' && p .rightChar (3 ) != '!' && p .rightChar (3 ) != '=' ) {
return nil , p .getErr (ErrAlternationCantCapture )
}
}
case 'P' :
if p .useRE2 () {
if p .charsRight () < 3 {
goto BreakRecognize
}
ch = p .moveRightGetChar ()
if ch != '<' {
goto BreakRecognize
}
ch = p .moveRightGetChar ()
p .moveLeft ()
if IsWordChar (ch ) {
capnum := -1
capname := p .scanCapname ()
if p .isCaptureName (capname ) {
capnum = p .captureSlotFromName (capname )
}
if p .charsRight () > 0 && p .rightChar (0 ) != '>' {
return nil , p .getErr (ErrInvalidGroupName )
}
if capnum != -1 && p .charsRight () > 0 && p .moveRightGetChar () == '>' {
return newRegexNodeMN (ntCapture , p .options , capnum , -1 ), nil
}
goto BreakRecognize
} else {
return nil , p .getErr (ErrInvalidGroupName )
}
}
fallthrough
default :
p .moveLeft ()
nt = ntGroup
if p .group .t != ntTestgroup {
p .scanOptions ()
}
if p .charsRight () == 0 {
goto BreakRecognize
}
if ch = p .moveRightGetChar (); ch == ')' {
return nil , nil
}
if ch != ':' {
goto BreakRecognize
}
}
return newRegexNode (nt , p .options ), nil
}
BreakRecognize :
return nil , p .getErr (ErrUnrecognizedGrouping , string (p .pattern [start :p .textpos ()]))
}
func (p *parser ) scanBackslash (scanOnly bool ) (*regexNode , error ) {
if p .charsRight () == 0 {
return nil , p .getErr (ErrIllegalEndEscape )
}
switch ch := p .rightChar (0 ); ch {
case 'b' , 'B' , 'A' , 'G' , 'Z' , 'z' :
p .moveRight (1 )
return newRegexNode (p .typeFromCode (ch ), p .options ), nil
case 'w' :
p .moveRight (1 )
if p .useOptionE () || p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , ECMAWordClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , WordClass ()), nil
case 'W' :
p .moveRight (1 )
if p .useOptionE () || p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , NotECMAWordClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , NotWordClass ()), nil
case 's' :
p .moveRight (1 )
if p .useOptionE () {
return newRegexNodeSet (ntSet , p .options , ECMASpaceClass ()), nil
} else if p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , RE2SpaceClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , SpaceClass ()), nil
case 'S' :
p .moveRight (1 )
if p .useOptionE () {
return newRegexNodeSet (ntSet , p .options , NotECMASpaceClass ()), nil
} else if p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , NotRE2SpaceClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , NotSpaceClass ()), nil
case 'd' :
p .moveRight (1 )
if p .useOptionE () || p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , ECMADigitClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , DigitClass ()), nil
case 'D' :
p .moveRight (1 )
if p .useOptionE () || p .useRE2 () {
return newRegexNodeSet (ntSet , p .options , NotECMADigitClass ()), nil
}
return newRegexNodeSet (ntSet , p .options , NotDigitClass ()), nil
case 'p' , 'P' :
p .moveRight (1 )
prop , err := p .parseProperty ()
if err != nil {
return nil , err
}
cc := &CharSet {}
cc .addCategory (prop , (ch != 'p' ), p .useOptionI (), p .patternRaw )
if p .useOptionI () {
cc .addLowercase ()
}
return newRegexNodeSet (ntSet , p .options , cc ), nil
default :
return p .scanBasicBackslash (scanOnly )
}
}
func (p *parser ) scanBasicBackslash (scanOnly bool ) (*regexNode , error ) {
if p .charsRight () == 0 {
return nil , p .getErr (ErrIllegalEndEscape )
}
angled := false
k := false
close := '\x00'
backpos := p .textpos ()
ch := p .rightChar (0 )
if ch == 'k' && (!p .useOptionE () || len (p .capnames ) > 0 ) {
if p .charsRight () >= 2 {
p .moveRight (1 )
ch = p .moveRightGetChar ()
if ch == '<' || (!p .useOptionE () && ch == '\'' ) {
angled = true
if ch == '\'' {
close = '\''
} else {
close = '>'
}
}
}
if !angled || p .charsRight () <= 0 {
return nil , p .getErr (ErrMalformedNameRef )
}
ch = p .rightChar (0 )
k = true
} else if !p .useOptionE () && (ch == '<' || ch == '\'' ) && p .charsRight () > 1 {
angled = true
if ch == '\'' {
close = '\''
} else {
close = '>'
}
p .moveRight (1 )
ch = p .rightChar (0 )
}
if angled && ch >= '0' && ch <= '9' {
capnum , err := p .scanDecimal ()
if err != nil {
return nil , err
}
if p .charsRight () > 0 && p .moveRightGetChar () == close {
if p .isCaptureSlot (capnum ) {
return newRegexNodeM (ntRef , p .options , capnum ), nil
}
return nil , p .getErr (ErrUndefinedBackRef , capnum )
}
} else if !angled && ch >= '1' && ch <= '9' {
capnum , err := p .scanDecimal ()
if err != nil {
return nil , err
}
if scanOnly {
return nil , nil
}
if p .isCaptureSlot (capnum ) {
return newRegexNodeM (ntRef , p .options , capnum ), nil
}
if capnum <= 9 && !p .useOptionE () {
return nil , p .getErr (ErrUndefinedBackRef , capnum )
}
} else if angled {
capname := p .scanCapname ()
if capname != "" && p .charsRight () > 0 && p .moveRightGetChar () == close {
if scanOnly {
return nil , nil
}
if p .isCaptureName (capname ) {
return newRegexNodeM (ntRef , p .options , p .captureSlotFromName (capname )), nil
}
return nil , p .getErr (ErrUndefinedNameRef , capname )
} else {
if k {
return nil , p .getErr (ErrMalformedNameRef )
}
}
}
p .textto (backpos )
ch , err := p .scanCharEscape ()
if err != nil {
return nil , err
}
if scanOnly {
return nil , nil
}
if p .useOptionI () {
ch = unicode .ToLower (ch )
}
return newRegexNodeCh (ntOne , p .options , ch ), nil
}
func (p *parser ) parseProperty () (string , error ) {
if p .charsRight () >= 1 && p .rightChar (0 ) != '{' {
ch := string (p .moveRightGetChar ())
if !isValidUnicodeCat (ch ) {
return "" , p .getErr (ErrUnknownSlashP , ch )
}
return ch , nil
}
if p .charsRight () < 3 {
return "" , p .getErr (ErrIncompleteSlashP )
}
ch := p .moveRightGetChar ()
if ch != '{' {
return "" , p .getErr (ErrMalformedSlashP )
}
startpos := p .textpos ()
for p .charsRight () > 0 {
ch = p .moveRightGetChar ()
if !(IsWordChar (ch ) || ch == '-' ) {
p .moveLeft ()
break
}
}
capname := string (p .pattern [startpos :p .textpos ()])
if p .charsRight () == 0 || p .moveRightGetChar () != '}' {
return "" , p .getErr (ErrIncompleteSlashP )
}
if !isValidUnicodeCat (capname ) {
return "" , p .getErr (ErrUnknownSlashP , capname )
}
return capname , nil
}
func (p *parser ) typeFromCode (ch rune ) nodeType {
switch ch {
case 'b' :
if p .useOptionE () {
return ntECMABoundary
}
return ntBoundary
case 'B' :
if p .useOptionE () {
return ntNonECMABoundary
}
return ntNonboundary
case 'A' :
return ntBeginning
case 'G' :
return ntStart
case 'Z' :
return ntEndZ
case 'z' :
return ntEnd
default :
return ntNothing
}
}
func (p *parser ) scanBlank () error {
if p .useOptionX () {
for {
for p .charsRight () > 0 && isSpace (p .rightChar (0 )) {
p .moveRight (1 )
}
if p .charsRight () == 0 {
break
}
if p .rightChar (0 ) == '#' {
for p .charsRight () > 0 && p .rightChar (0 ) != '\n' {
p .moveRight (1 )
}
} else if p .charsRight () >= 3 && p .rightChar (2 ) == '#' &&
p .rightChar (1 ) == '?' && p .rightChar (0 ) == '(' {
for p .charsRight () > 0 && p .rightChar (0 ) != ')' {
p .moveRight (1 )
}
if p .charsRight () == 0 {
return p .getErr (ErrUnterminatedComment )
}
p .moveRight (1 )
} else {
break
}
}
} else {
for {
if p .charsRight () < 3 || p .rightChar (2 ) != '#' ||
p .rightChar (1 ) != '?' || p .rightChar (0 ) != '(' {
return nil
}
for p .charsRight () > 0 && p .rightChar (0 ) != ')' {
p .moveRight (1 )
}
if p .charsRight () == 0 {
return p .getErr (ErrUnterminatedComment )
}
p .moveRight (1 )
}
}
return nil
}
func (p *parser ) scanCapname () string {
startpos := p .textpos ()
for p .charsRight () > 0 {
if !IsWordChar (p .moveRightGetChar ()) {
p .moveLeft ()
break
}
}
return string (p .pattern [startpos :p .textpos ()])
}
func (p *parser ) scanCharSet (caseInsensitive , scanOnly bool ) (*CharSet , error ) {
ch := '\x00'
chPrev := '\x00'
inRange := false
firstChar := true
closed := false
var cc *CharSet
if !scanOnly {
cc = &CharSet {}
}
if p .charsRight () > 0 && p .rightChar (0 ) == '^' {
p .moveRight (1 )
if !scanOnly {
cc .negate = true
}
}
for ; p .charsRight () > 0 ; firstChar = false {
fTranslatedChar := false
ch = p .moveRightGetChar ()
if ch == ']' {
if !firstChar {
closed = true
break
} else if p .useOptionE () {
if !scanOnly {
cc .addRanges (NoneClass ().ranges )
}
closed = true
break
}
} else if ch == '\\' && p .charsRight () > 0 {
switch ch = p .moveRightGetChar (); ch {
case 'D' , 'd' :
if !scanOnly {
if inRange {
return nil , p .getErr (ErrBadClassInCharRange , ch )
}
cc .addDigit (p .useOptionE () || p .useRE2 (), ch == 'D' , p .patternRaw )
}
continue
case 'S' , 's' :
if !scanOnly {
if inRange {
return nil , p .getErr (ErrBadClassInCharRange , ch )
}
cc .addSpace (p .useOptionE (), p .useRE2 (), ch == 'S' )
}
continue
case 'W' , 'w' :
if !scanOnly {
if inRange {
return nil , p .getErr (ErrBadClassInCharRange , ch )
}
cc .addWord (p .useOptionE () || p .useRE2 (), ch == 'W' )
}
continue
case 'p' , 'P' :
if !scanOnly {
if inRange {
return nil , p .getErr (ErrBadClassInCharRange , ch )
}
prop , err := p .parseProperty ()
if err != nil {
return nil , err
}
cc .addCategory (prop , (ch != 'p' ), caseInsensitive , p .patternRaw )
} else {
p .parseProperty ()
}
continue
case '-' :
if !scanOnly {
cc .addRange (ch , ch )
}
continue
default :
p .moveLeft ()
var err error
ch , err = p .scanCharEscape ()
if err != nil {
return nil , err
}
fTranslatedChar = true
break
}
} else if ch == '[' {
if p .charsRight () > 0 && p .rightChar (0 ) == ':' && !inRange {
savePos := p .textpos ()
p .moveRight (1 )
negate := false
if p .charsRight () > 1 && p .rightChar (0 ) == '^' {
negate = true
p .moveRight (1 )
}
nm := p .scanCapname ()
if !scanOnly && p .useRE2 () {
if ok := cc .addNamedASCII (nm , negate ); !ok {
return nil , p .getErr (ErrInvalidCharRange )
}
}
if p .charsRight () < 2 || p .moveRightGetChar () != ':' || p .moveRightGetChar () != ']' {
p .textto (savePos )
} else if p .useRE2 () {
continue
}
}
}
if inRange {
inRange = false
if !scanOnly {
if ch == '[' && !fTranslatedChar && !firstChar {
cc .addChar (chPrev )
sub , err := p .scanCharSet (caseInsensitive , false )
if err != nil {
return nil , err
}
cc .addSubtraction (sub )
if p .charsRight () > 0 && p .rightChar (0 ) != ']' {
return nil , p .getErr (ErrSubtractionMustBeLast )
}
} else {
if chPrev > ch {
return nil , p .getErr (ErrReversedCharRange , chPrev , ch )
}
cc .addRange (chPrev , ch )
}
}
} else if p .charsRight () >= 2 && p .rightChar (0 ) == '-' && p .rightChar (1 ) != ']' {
chPrev = ch
inRange = true
p .moveRight (1 )
} else if p .charsRight () >= 1 && ch == '-' && !fTranslatedChar && p .rightChar (0 ) == '[' && !firstChar {
if !scanOnly {
p .moveRight (1 )
sub , err := p .scanCharSet (caseInsensitive , false )
if err != nil {
return nil , err
}
cc .addSubtraction (sub )
if p .charsRight () > 0 && p .rightChar (0 ) != ']' {
return nil , p .getErr (ErrSubtractionMustBeLast )
}
} else {
p .moveRight (1 )
p .scanCharSet (caseInsensitive , true )
}
} else {
if !scanOnly {
cc .addRange (ch , ch )
}
}
}
if !closed {
return nil , p .getErr (ErrUnterminatedBracket )
}
if !scanOnly && caseInsensitive {
cc .addLowercase ()
}
return cc , nil
}
func (p *parser ) scanDecimal () (int , error ) {
i := 0
var d int
for p .charsRight () > 0 {
d = int (p .rightChar (0 ) - '0' )
if d < 0 || d > 9 {
break
}
p .moveRight (1 )
if i > maxValueDiv10 || (i == maxValueDiv10 && d > maxValueMod10 ) {
return 0 , p .getErr (ErrCaptureGroupOutOfRange )
}
i *= 10
i += d
}
return int (i ), nil
}
func isOnlyTopOption(option RegexOptions ) bool {
return option == RightToLeft || option == ECMAScript || option == RE2
}
func (p *parser ) scanOptions () {
for off := false ; p .charsRight () > 0 ; p .moveRight (1 ) {
ch := p .rightChar (0 )
if ch == '-' {
off = true
} else if ch == '+' {
off = false
} else {
option := optionFromCode (ch )
if option == 0 || isOnlyTopOption (option ) {
return
}
if off {
p .options &= ^option
} else {
p .options |= option
}
}
}
}
func (p *parser ) scanCharEscape () (r rune , err error ) {
ch := p .moveRightGetChar ()
if ch >= '0' && ch <= '7' {
p .moveLeft ()
return p .scanOctal (), nil
}
pos := p .textpos ()
switch ch {
case 'x' :
if p .charsRight () > 0 && p .rightChar (0 ) == '{' {
if p .useOptionE () {
return ch , nil
}
p .moveRight (1 )
return p .scanHexUntilBrace ()
} else {
r , err = p .scanHex (2 )
}
case 'u' :
if p .useOptionE () && p .useOptionU () && p .charsRight () > 0 && p .rightChar (0 ) == '{' {
p .moveRight (1 )
return p .scanHexUntilBrace ()
} else {
r , err = p .scanHex (4 )
}
case 'a' :
return '\u0007' , nil
case 'b' :
return '\b' , nil
case 'e' :
return '\u001B' , nil
case 'f' :
return '\f' , nil
case 'n' :
return '\n' , nil
case 'r' :
return '\r' , nil
case 't' :
return '\t' , nil
case 'v' :
return '\u000B' , nil
case 'c' :
r , err = p .scanControl ()
default :
if !p .useOptionE () && !p .useRE2 () && IsWordChar (ch ) {
return 0 , p .getErr (ErrUnrecognizedEscape , string (ch ))
}
return ch , nil
}
if err != nil && p .useOptionE () {
p .textto (pos )
return ch , nil
}
return
}
func (p *parser ) scanControl () (rune , error ) {
if p .charsRight () <= 0 {
return 0 , p .getErr (ErrMissingControl )
}
ch := p .moveRightGetChar ()
if ch >= 'a' && ch <= 'z' {
ch = (ch - ('a' - 'A' ))
}
ch = (ch - '@' )
if ch >= 0 && ch < ' ' {
return ch , nil
}
return 0 , p .getErr (ErrUnrecognizedControl )
}
func (p *parser ) scanHexUntilBrace () (rune , error ) {
i := 0
hasContent := false
for p .charsRight () > 0 {
ch := p .moveRightGetChar ()
if ch == '}' {
if !hasContent {
return 0 , p .getErr (ErrTooFewHex )
}
return rune (i ), nil
}
hasContent = true
d := hexDigit (ch )
if d < 0 {
return 0 , p .getErr (ErrMissingBrace )
}
i *= 0x10
i += d
if i > unicode .MaxRune {
return 0 , p .getErr (ErrInvalidHex )
}
}
return 0 , p .getErr (ErrMissingBrace )
}
func (p *parser ) scanHex (c int ) (rune , error ) {
i := 0
if p .charsRight () >= c {
for c > 0 {
d := hexDigit (p .moveRightGetChar ())
if d < 0 {
break
}
i *= 0x10
i += d
c --
}
}
if c > 0 {
return 0 , p .getErr (ErrTooFewHex )
}
return rune (i ), nil
}
func hexDigit(ch rune ) int {
if d := uint (ch - '0' ); d <= 9 {
return int (d )
}
if d := uint (ch - 'a' ); d <= 5 {
return int (d + 0xa )
}
if d := uint (ch - 'A' ); d <= 5 {
return int (d + 0xa )
}
return -1
}
func (p *parser ) scanOctal () rune {
c := 3
if c > p .charsRight () {
c = p .charsRight ()
}
i := 0
d := int (p .rightChar (0 ) - '0' )
for c > 0 && d <= 7 && d >= 0 {
if i >= 0x20 && p .useOptionE () {
break
}
i *= 8
i += d
c --
p .moveRight (1 )
if !p .rightMost () {
d = int (p .rightChar (0 ) - '0' )
}
}
i &= 0xFF
return rune (i )
}
func (p *parser ) textpos () int {
return p .currentPos
}
func (p *parser ) textto (pos int ) {
p .currentPos = pos
}
func (p *parser ) moveRightGetChar () rune {
ch := p .pattern [p .currentPos ]
p .currentPos ++
return ch
}
func (p *parser ) moveRight (i int ) {
p .currentPos += i
}
func (p *parser ) moveLeft () {
p .currentPos --
}
func (p *parser ) charAt (i int ) rune {
return p .pattern [i ]
}
func (p *parser ) rightChar (i int ) rune {
return p .pattern [p .currentPos +i ]
}
func (p *parser ) charsRight () int {
return len (p .pattern ) - p .currentPos
}
func (p *parser ) rightMost () bool {
return p .currentPos == len (p .pattern )
}
func (p *parser ) captureSlotFromName (capname string ) int {
return p .capnames [capname ]
}
func (p *parser ) isCaptureSlot (i int ) bool {
if p .caps != nil {
_ , ok := p .caps [i ]
return ok
}
return (i >= 0 && i < p .capsize )
}
func (p *parser ) isCaptureName (capname string ) bool {
if p .capnames == nil {
return false
}
_ , ok := p .capnames [capname ]
return ok
}
func (p *parser ) useOptionN () bool {
return (p .options & ExplicitCapture ) != 0
}
func (p *parser ) useOptionI () bool {
return (p .options & IgnoreCase ) != 0
}
func (p *parser ) useOptionM () bool {
return (p .options & Multiline ) != 0
}
func (p *parser ) useOptionS () bool {
return (p .options & Singleline ) != 0
}
func (p *parser ) useOptionX () bool {
return (p .options & IgnorePatternWhitespace ) != 0
}
func (p *parser ) useOptionE () bool {
return (p .options & ECMAScript ) != 0
}
func (p *parser ) useRE2 () bool {
return (p .options & RE2 ) != 0
}
func (p *parser ) useOptionU () bool {
return (p .options & Unicode ) != 0
}
func (p *parser ) emptyOptionsStack () bool {
return len (p .optionsStack ) == 0
}
func (p *parser ) addConcatenate () {
p .concatenation .addChild (p .unit )
p .unit = nil
}
func (p *parser ) addConcatenate3 (lazy bool , min , max int ) {
p .concatenation .addChild (p .unit .makeQuantifier (lazy , min , max ))
p .unit = nil
}
func (p *parser ) addUnitOne (ch rune ) {
if p .useOptionI () {
ch = unicode .ToLower (ch )
}
p .unit = newRegexNodeCh (ntOne , p .options , ch )
}
func (p *parser ) addUnitNotone (ch rune ) {
if p .useOptionI () {
ch = unicode .ToLower (ch )
}
p .unit = newRegexNodeCh (ntNotone , p .options , ch )
}
func (p *parser ) addUnitSet (set *CharSet ) {
p .unit = newRegexNodeSet (ntSet , p .options , set )
}
func (p *parser ) addUnitNode (node *regexNode ) {
p .unit = node
}
func (p *parser ) addUnitType (t nodeType ) {
p .unit = newRegexNode (t , p .options )
}
func (p *parser ) addGroup () error {
if p .group .t == ntTestgroup || p .group .t == ntTestref {
p .group .addChild (p .concatenation .reverseLeft ())
if (p .group .t == ntTestref && len (p .group .children ) > 2 ) || len (p .group .children ) > 3 {
return p .getErr (ErrTooManyAlternates )
}
} else {
p .alternation .addChild (p .concatenation .reverseLeft ())
p .group .addChild (p .alternation )
}
p .unit = p .group
return nil
}
func (p *parser ) popKeepOptions () {
lastIdx := len (p .optionsStack ) - 1
p .optionsStack = p .optionsStack [:lastIdx ]
}
func (p *parser ) popOptions () {
lastIdx := len (p .optionsStack ) - 1
p .options = p .optionsStack [lastIdx ]
p .optionsStack = p .optionsStack [:lastIdx ]
}
func (p *parser ) pushOptions () {
p .optionsStack = append (p .optionsStack , p .options )
}
func (p *parser ) addToConcatenate (pos , cch int , isReplacement bool ) {
var node *regexNode
if cch == 0 {
return
}
if cch > 1 {
str := make ([]rune , cch )
copy (str , p .pattern [pos :pos +cch ])
if p .useOptionI () && !isReplacement {
for i := 0 ; i < len (str ); i ++ {
str [i ] = unicode .ToLower (str [i ])
}
}
node = newRegexNodeStr (ntMulti , p .options , str )
} else {
ch := p .charAt (pos )
if p .useOptionI () && !isReplacement {
ch = unicode .ToLower (ch )
}
node = newRegexNodeCh (ntOne , p .options , ch )
}
p .concatenation .addChild (node )
}
func (p *parser ) pushGroup () {
p .group .next = p .stack
p .alternation .next = p .group
p .concatenation .next = p .alternation
p .stack = p .concatenation
}
func (p *parser ) popGroup () error {
p .concatenation = p .stack
p .alternation = p .concatenation .next
p .group = p .alternation .next
p .stack = p .group .next
if p .group .t == ntTestgroup && len (p .group .children ) == 0 {
if p .unit == nil {
return p .getErr (ErrConditionalExpression )
}
p .group .addChild (p .unit )
p .unit = nil
}
return nil
}
func (p *parser ) emptyStack () bool {
return p .stack == nil
}
func (p *parser ) startGroup (openGroup *regexNode ) {
p .group = openGroup
p .alternation = newRegexNode (ntAlternate , p .options )
p .concatenation = newRegexNode (ntConcatenate , p .options )
}
func (p *parser ) addAlternate () {
if p .group .t == ntTestgroup || p .group .t == ntTestref {
p .group .addChild (p .concatenation .reverseLeft ())
} else {
p .alternation .addChild (p .concatenation .reverseLeft ())
}
p .concatenation = newRegexNode (ntConcatenate , p .options )
}
const (
Q byte = 5
S = 4
Z = 3
X = 2
E = 1
)
var _category = []byte {
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , X , X , X , X , X , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
X , 0 , 0 , Z , S , 0 , 0 , 0 , S , S , Q , Q , 0 , 0 , S , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , Q ,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , S , S , 0 , S , 0 ,
0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , Q , S , 0 , 0 , 0 ,
}
func isSpace(ch rune ) bool {
return (ch <= ' ' && _category [ch ] == X )
}
func isSpecial(ch rune ) bool {
return (ch <= '|' && _category [ch ] >= S )
}
func isStopperX(ch rune ) bool {
return (ch <= '|' && _category [ch ] >= X )
}
func isQuantifier(ch rune ) bool {
return (ch <= '{' && _category [ch ] >= Q )
}
func (p *parser ) isTrueQuantifier () bool {
nChars := p .charsRight ()
if nChars == 0 {
return false
}
startpos := p .textpos ()
ch := p .charAt (startpos )
if ch != '{' {
return ch <= '{' && _category [ch ] >= Q
}
pos := startpos
for {
nChars --
if nChars <= 0 {
break
}
pos ++
ch = p .charAt (pos )
if ch < '0' || ch > '9' {
break
}
}
if nChars == 0 || pos -startpos == 1 {
return false
}
if ch == '}' {
return true
}
if ch != ',' {
return false
}
for {
nChars --
if nChars <= 0 {
break
}
pos ++
ch = p .charAt (pos )
if ch < '0' || ch > '9' {
break
}
}
return nChars > 0 && ch == '}'
}
The pages are generated with Golds v0.8.2 . (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 .