Merge pull request #2088 from umputun/deps/update-backend
Update backend dependencies to latest
This commit is contained in:
@@ -13,11 +13,11 @@ require (
|
||||
require (
|
||||
github.com/Depado/bfchroma/v2 v2.0.0 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.12.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 // indirect
|
||||
github.com/go-pkgz/rest v1.21.0 // indirect
|
||||
github.com/go-pkgz/routegroup v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
@@ -29,11 +29,11 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/image v0.40.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
go.etcd.io/bbolt v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.53.0 // indirect
|
||||
golang.org/x/image v0.43.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@ github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO
|
||||
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
|
||||
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
|
||||
github.com/go-pkgz/lgr v0.12.3 h1:QDug7kRkEsuQtruT9fNF5PVT2kZUqCDPc4GmsgS3fP8=
|
||||
@@ -25,7 +25,6 @@ github.com/go-pkgz/rest v1.21.0 h1:Y/C4d/TpclJJDxqnH1RAcS6Hmox0RIReAlkwMcUWXK4=
|
||||
github.com/go-pkgz/rest v1.21.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
|
||||
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
|
||||
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -53,82 +52,18 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+13
-13
@@ -5,7 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
github.com/Depado/bfchroma/v2 v2.0.0
|
||||
github.com/PuerkitoBio/goquery v1.12.0
|
||||
github.com/alecthomas/chroma/v2 v2.24.1
|
||||
github.com/alecthomas/chroma/v2 v2.27.0
|
||||
github.com/didip/tollbooth/v8 v8.0.1
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/cors v1.2.2
|
||||
@@ -28,22 +28,22 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go.etcd.io/bbolt v1.5.0
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/image v0.40.0
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/image v0.43.0
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.4 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dghubble/oauth1 v0.7.3 // indirect
|
||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 // indirect
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.4 // indirect
|
||||
github.com/go-pkgz/email v0.6.0 // indirect
|
||||
github.com/go-pkgz/expirable-cache/v3 v3.1.0 // indirect
|
||||
@@ -54,20 +54,20 @@ require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.19.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.21.0 // indirect
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect
|
||||
github.com/slack-go/slack v0.23.1 // indirect
|
||||
github.com/slack-go/slack v0.27.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+26
-68
@@ -8,8 +8,8 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
|
||||
@@ -18,8 +18,8 @@ github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6
|
||||
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/andybalholm/cascadia v1.3.4 h1:vM2lgh0Vru9Vwyfm4cQqWP2HHMW0u0+2PAW7Q38Qufg=
|
||||
github.com/andybalholm/cascadia v1.3.4/go.mod h1:BLRmbRjpEtNKieZOCCvYj4RqN+KRA41GBe/5O+G93kM=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -34,8 +34,8 @@ github.com/dghubble/oauth1 v0.7.3 h1:EkEM/zMDMp3zOsX2DC/ZQ2vnEX3ELK0/l9kb+vs4ptE
|
||||
github.com/dghubble/oauth1 v0.7.3/go.mod h1:oxTe+az9NSMIucDPDCCtzJGsPhciJV33xocHfcR2sVY=
|
||||
github.com/didip/tollbooth/v8 v8.0.1 h1:VAAapTo1t4Bn6bbpcHjuovwoa9u3JH++wgjbpWv+rB8=
|
||||
github.com/didip/tollbooth/v8 v8.0.1/go.mod h1:oEd9l+ep373d7DmvKLc0a5gasPOev2mTewi6KPQBGJ4=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 h1:MYWvNYw8okuqNhwTYO587EZMiDruVa2vhV6fsGpfya0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.2/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||
@@ -76,7 +76,6 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
@@ -106,8 +105,8 @@ github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bB
|
||||
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -124,8 +123,8 @@ github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
|
||||
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
|
||||
@@ -138,8 +137,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/slack-go/slack v0.23.1 h1:ZS5B96wxxYQRwvJ3/vJFtqtUZi3tXhsZCyT44Nv7M80=
|
||||
github.com/slack-go/slack v0.23.1/go.mod h1:H0yR/YBuRJ39RkE+JpV/d/oEsbanzTRowR82bCN0cEs=
|
||||
github.com/slack-go/slack v0.27.0 h1:VWOpUzOK6UAPCCQlFxl79jhv8a/b+GOSJMnWziDJ8B8=
|
||||
github.com/slack-go/slack v0.27.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
|
||||
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
@@ -191,8 +190,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
@@ -201,81 +200,40 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/image v0.43.0 h1:FLxcP4ec2350nTfOC8ysKtqYSIFbk/QGjw1ZHNP4tsY=
|
||||
golang.org/x/image v0.43.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
+111
-78
@@ -1,89 +1,122 @@
|
||||
version: "2"
|
||||
run:
|
||||
tests: true
|
||||
|
||||
output:
|
||||
print-issued-lines: false
|
||||
|
||||
show-stats: false
|
||||
formats:
|
||||
text:
|
||||
print-issued-lines: false
|
||||
colors: true
|
||||
linters:
|
||||
enable-all: true
|
||||
default: all
|
||||
disable:
|
||||
- lll
|
||||
- gocyclo
|
||||
- dupl
|
||||
- gochecknoglobals
|
||||
- funlen
|
||||
- godox
|
||||
- wsl
|
||||
- gocognit
|
||||
- nolintlint
|
||||
- testpackage
|
||||
- godot
|
||||
- nestif
|
||||
- paralleltest
|
||||
- nlreturn
|
||||
- cyclop
|
||||
- gci
|
||||
- gofumpt
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- wrapcheck
|
||||
- stylecheck
|
||||
- thelper
|
||||
- nonamedreturns
|
||||
- revive
|
||||
- dupword
|
||||
- exhaustruct
|
||||
- varnamelen
|
||||
- forcetypeassert
|
||||
- ireturn
|
||||
- maintidx
|
||||
- govet
|
||||
- testableexamples
|
||||
- musttag
|
||||
- prealloc
|
||||
- dupl
|
||||
- godoclint
|
||||
- cyclop
|
||||
- depguard
|
||||
- goconst
|
||||
- perfsprint
|
||||
- dupword
|
||||
- err113
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustruct
|
||||
- forbidigo
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gocognit
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- godox
|
||||
- gomoddirectives
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- mnd
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- predeclared
|
||||
- recvcheck
|
||||
- tenv
|
||||
- err113
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
dupl:
|
||||
threshold: 100
|
||||
goconst:
|
||||
min-len: 8
|
||||
min-occurrences: 3
|
||||
forbidigo:
|
||||
#forbid:
|
||||
# - (Must)?NewLexer$
|
||||
exclude_godoc_examples: false
|
||||
|
||||
|
||||
- revive
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wastedassign
|
||||
- whitespace
|
||||
- wsl
|
||||
- wsl_v5
|
||||
- funcorder
|
||||
- noinlineerr
|
||||
- tagalign
|
||||
- goconst
|
||||
- gochecknoinits
|
||||
- durationcheck
|
||||
- embeddedstructfieldcheck
|
||||
- wrapcheck
|
||||
- gomodguard
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 100
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
goconst:
|
||||
min-len: 8
|
||||
min-occurrences: 3
|
||||
gocyclo:
|
||||
min-complexity: 10
|
||||
wrapcheck:
|
||||
report-internal-errors: false
|
||||
ignore-package-globs:
|
||||
- github.com/alecthomas/errors
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- path: (.+)\.go$
|
||||
text: "^(G104|G204|G307|G304):"
|
||||
- path: (.+)\.go$
|
||||
text: Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked
|
||||
- path: (.+)\.go$
|
||||
text: exported method `(.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos)` should have comment or be unexported
|
||||
- path: (.+)\.go$
|
||||
text: uses unkeyed fields
|
||||
- path: (.+)\.go$
|
||||
text: declaration of "err" shadows declaration
|
||||
- path: (.+)\.go$
|
||||
text: bad syntax for struct tag key
|
||||
- path: (.+)\.go$
|
||||
text: bad syntax for struct tag pair
|
||||
- path: (.+)\.go$
|
||||
text: ^ST1012
|
||||
- path: (.+)\.go$
|
||||
text: log/slog.Logger.*must not be called
|
||||
- path: (.+)_test\.go$
|
||||
text: error returned from external package is unwrapped
|
||||
- linters: [staticcheck]
|
||||
text: QF1008
|
||||
- text: "Error return value of `.*.Write` is not checked"
|
||||
linters: [errcheck]
|
||||
path: (.+)_test\.go$
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
exclude-dirs:
|
||||
- _examples
|
||||
max-per-linter: 0
|
||||
max-same: 0
|
||||
exclude-use-default: false
|
||||
exclude:
|
||||
# Captured by errcheck.
|
||||
- '^(G104|G204):'
|
||||
# Very commonly not checked.
|
||||
- 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
|
||||
- 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos) should have comment or be unexported'
|
||||
- 'composite literal uses unkeyed fields'
|
||||
- 'declaration of "err" shadows declaration'
|
||||
- 'should not use dot imports'
|
||||
- 'Potential file inclusion via variable'
|
||||
- 'should have comment or be unexported'
|
||||
- 'comment on exported var .* should be of the form'
|
||||
- 'at least one file in a package should have a package comment'
|
||||
- 'string literal contains the Unicode'
|
||||
- 'methods on the same type should have the same receiver name'
|
||||
- '_TokenType_name should be _TokenTypeName'
|
||||
- '`_TokenType_map` should be `_TokenTypeMap`'
|
||||
- 'rewrite if-else to switch statement'
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Chroma is a syntax highlighting library, tool and web playground for Go. It is based on Pygments and includes importers for it, so most of the same concepts from Pygments apply to Chroma.
|
||||
|
||||
This project is written in Go, uses Hermit to manage tooling, and Just for helper commands. Helper scripts are in ./scripts.
|
||||
This project is written in Go, uses Hermit to manage tooling, and Just for helper commands. Helper tooling is primarily in ./_tools.
|
||||
|
||||
Language definitions are XML files defined in ./lexers/embedded/*.xml.
|
||||
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
let version = exec("git describe --tags --dirty --always") | trim
|
||||
# TinyGo's installation root; used to source `wasm_exec.js`.
|
||||
let tinygoroot = exec("tinygo env TINYGOROOT") | trim
|
||||
|
||||
|
||||
# Generate tokentype_enumer.go from types.go via `//go:generate`.
|
||||
tokentype = go.generate {
|
||||
package = "."
|
||||
inputs = ["types.go"]
|
||||
outputs = ["tokentype_enumer.go"]
|
||||
}
|
||||
|
||||
# Regenerate the lexer table in README.md by invoking the host `chroma` binary.
|
||||
# GOOS/GOARCH are cleared so cross-compile env vars don't break the local run.
|
||||
protected readme = exec {
|
||||
command = "./table.py"
|
||||
inputs = ["table.py", "lexers/**/*.go", "lexers/**/*.xml"]
|
||||
output = "README.md"
|
||||
}
|
||||
|
||||
# Format frontend JS sources in place. Runs as a sub-step of `index-min-js`,
|
||||
# so bundling always sees formatted sources.
|
||||
format-js = exec {
|
||||
command = "biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js"
|
||||
inputs = ["biome.js", "cmd/chromad/static/index.js", "cmd/chromad/static/chroma.js"]
|
||||
}
|
||||
|
||||
# Copy TinyGo's wasm_exec.js into the chromad static assets.
|
||||
wasm-exec = exec {
|
||||
command = "install -m644 '#{tinygoroot}/targets/wasm_exec.js' cmd/chromad/static/wasm_exec.js"
|
||||
resolve = "sha256 '#{tinygoroot}/targets/wasm_exec.js'"
|
||||
output = "cmd/chromad/static/wasm_exec.js"
|
||||
}
|
||||
|
||||
# Build the chroma WASM module via tinygo (installed via hermit) for the
|
||||
# smaller output binary.
|
||||
chroma-wasm = exec {
|
||||
command = "tinygo build -no-debug -target wasm -o cmd/chromad/static/chroma.wasm cmd/libchromawasm/main.go"
|
||||
inputs = ["cmd/libchromawasm/**/*.go", "*.go", "lexers/**/*.go", "lexers/**/*.xml", "formatters/**/*.go", "styles/**/*.go"]
|
||||
output = "cmd/chromad/static/chroma.wasm"
|
||||
}
|
||||
|
||||
# Bundle and minify the frontend JS. Depends on `format-js` so the bundle
|
||||
# always reflects formatted sources.
|
||||
index-min-js = exec {
|
||||
command = "esbuild --platform=browser --format=esm --bundle cmd/chromad/static/index.js --minify --external:./wasm_exec.js --outfile=cmd/chromad/static/index.min.js"
|
||||
inputs = ["cmd/chromad/static/index.js", "cmd/chromad/static/chroma.js"]
|
||||
output = "cmd/chromad/static/index.min.js"
|
||||
depends_on = [format-js]
|
||||
}
|
||||
|
||||
# Bundle and minify the frontend CSS.
|
||||
index-min-css = exec {
|
||||
command = "esbuild --bundle cmd/chromad/static/index.css --minify --outfile=cmd/chromad/static/index.min.css"
|
||||
inputs = ["cmd/chromad/static/index.css", "cmd/chromad/static/bulma.css"]
|
||||
output = "cmd/chromad/static/index.min.css"
|
||||
}
|
||||
|
||||
# Build the chromad server binary. cmd/chromad is a separate Go module, so
|
||||
# `dir` puts the build in there and `package = "."` resolves against that
|
||||
# module. `output` stays project-root-relative; bit absolutises it before
|
||||
# passing to `go build -o`. Defaults to linux/amd64 to match the deploy
|
||||
# target; override with GOOS/GOARCH env vars for local builds.
|
||||
chromad = go.exe {
|
||||
dir = "cmd/chromad"
|
||||
package = "."
|
||||
output = "build/chromad"
|
||||
flags = ["-ldflags", "-X 'main.version=#{version}'"]
|
||||
goos = env("GOOS", "linux")
|
||||
goarch = env("GOARCH", "amd64")
|
||||
cgo = false
|
||||
depends_on = [wasm-exec, chroma-wasm, index-min-js, index-min-css, test]
|
||||
}
|
||||
|
||||
pre format-go = go.fmt {
|
||||
package = "./..."
|
||||
}
|
||||
|
||||
# Run Go tests.
|
||||
test = go.test {
|
||||
package = "./..."
|
||||
}
|
||||
|
||||
# Deploy chromad to swapoff.org. Must be explicitly selected.
|
||||
explicit upload = exec {
|
||||
command = <<-EOF
|
||||
scp #{chromad.path} root@swapoff.org:
|
||||
ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
EOF
|
||||
depends_on = [chromad]
|
||||
}
|
||||
|
||||
target default = [test, chromad, readme, tokentype]
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
VERSION = %(git describe --tags --dirty --always)%
|
||||
export CGOENABLED = 0
|
||||
|
||||
tokentype_enumer.go: types.go
|
||||
build: go generate
|
||||
|
||||
# Regenerate the list of lexers in the README
|
||||
README.md: lexers/*.go lexers/*/*.xml table.py
|
||||
build: ./table.py
|
||||
-clean
|
||||
|
||||
implicit %{1}%{2}.min.%{3}: **/*.{css,js}
|
||||
build: esbuild --bundle %{IN} --minify --outfile=%{OUT}
|
||||
|
||||
implicit build/%{1}: cmd/*
|
||||
cd cmd/%{1}
|
||||
inputs: cmd/%{1}/**/* **/*.go
|
||||
build: go build -ldflags="-X 'main.version=%{VERSION}'" -o ../../build/%{1} .
|
||||
|
||||
#upload: chromad
|
||||
# build:
|
||||
# scp chromad root@swapoff.org:
|
||||
# ssh root@swapoff.org 'install -m755 ./chromad /srv/http/swapoff.org/bin && service chromad restart'
|
||||
# touch upload
|
||||
+1
-1
@@ -28,7 +28,7 @@ ENV GOARCH=amd64
|
||||
RUN just chromad
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:3.23 AS runtime
|
||||
FROM alpine:3.24 AS runtime
|
||||
|
||||
# Install ca-certificates for HTTPS requests
|
||||
RUN apk --no-cache add ca-certificates curl
|
||||
|
||||
+5
-1
@@ -1,4 +1,4 @@
|
||||
set positional-arguments := true
|
||||
set positional-arguments
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
version := `git describe --tags --dirty --always`
|
||||
@@ -21,6 +21,10 @@ tokentype-string:
|
||||
format-js:
|
||||
biome format --write cmd/chromad/static/index.js cmd/chromad/static/chroma.js
|
||||
|
||||
# Tidy Go modules
|
||||
tidy:
|
||||
find . -name 'go.mod' -execdir go mod tidy \;
|
||||
|
||||
# Build chromad binary
|
||||
chromad: wasm-exec chroma-wasm
|
||||
#!/usr/bin/env bash
|
||||
|
||||
+9
-7
@@ -36,25 +36,25 @@ translators for Pygments lexers and styles.
|
||||
|
||||
| Prefix | Language
|
||||
| :----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, AMPL, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, ATL, AutoHotkey, AutoIt, Awk
|
||||
| A | ABAP, ABNF, ActionScript, ActionScript 3, Ada, Agda, AL, Alloy, AMPL, Angular2, ANTLR, ApacheConf, APL, AppleScript, ArangoDB AQL, Arduino, ArmAsm, Arturo, ATL, AutoHotkey, AutoIt, Awk
|
||||
| B | Ballerina, Bash, Bash Session, Batchfile, Beef, BibTeX, Bicep, BlitzBasic, BNF, BQN, Brainfuck
|
||||
| C | C, C#, C++, C3, Caddyfile, Caddyfile Directives, Cap'n Proto, Cassandra CQL, Ceylon, CFEngine3, cfstatement, ChaiScript, Chapel, Cheetah, Clojure, CMake, COBOL, CoffeeScript, Common Lisp, Coq, Core, Crystal, CSS, CSV, CUE, Cython
|
||||
| D | D, Dart, Dax, Desktop file, Diff, Django/Jinja, dns, Docker, DTD, Dylan
|
||||
| E | EBNF, Elixir, Elm, EmacsLisp, Erlang
|
||||
| D | D, Dart, Dax, Desktop file, Devicetree, Diff, Django/Jinja, dns, Docker, DTD, Dylan
|
||||
| E | EBNF, Elixir, Elm, EmacsLisp, ERB, Erlang
|
||||
| F | Factor, Fennel, Fish, Forth, Fortran, FortranFixed, FSharp
|
||||
| G | GAS, GDScript, GDScript3, Gemtext, Genshi, Genshi HTML, Genshi Text, Gettext, Gherkin, Gleam, GLSL, Gnuplot, Go, Go HTML Template, Go Template, Go Text Template, GraphQL, Groff, Groovy
|
||||
| H | Handlebars, Hare, Haskell, Haxe, HCL, Hexdump, HLB, HLSL, HolyC, HTML, HTTP, Hy
|
||||
| I | Idris, Igor, INI, Io, ISCdhcpd
|
||||
| J | J, Janet, Java, JavaScript, JSON, JSONata, Jsonnet, Julia, Jungle
|
||||
| K | Kakoune, Kotlin
|
||||
| L | Lean4, Lighttpd configuration file, LLVM, lox, Lua, Luau
|
||||
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonScript, MorrowindScript, Myghty, MySQL
|
||||
| K | Kakoune, KDL, Kotlin
|
||||
| L | Lateralus, Lean4, Lighttpd configuration file, LilyPond, LLVM, lox, Lua, Luau
|
||||
| M | Makefile, Mako, markdown, Markless, Mason, Materialize SQL dialect, Mathematica, Matlab, MCFunction, Meson, Metal, microcad, MiniZinc, MLIR, Modelica, Modula-2, Mojo, MonkeyC, MoonBit, MoonScript, MorrowindScript, Myghty, MySQL
|
||||
| N | NASM, Natural, NDISASM, Newspeak, Nginx configuration file, Nim, Nix, NSIS, Nu
|
||||
| O | Objective-C, ObjectPascal, OCaml, Octave, Odin, OnesEnterprise, OpenEdge ABL, OpenSCAD, Org Mode
|
||||
| P | PacmanConf, Perl, PHP, PHTML, Pig, PkgConfig, PL/pgSQL, plaintext, Plutus Core, Pony, PostgreSQL SQL dialect, PostScript, POVRay, PowerQuery, PowerShell, Prolog, Promela, PromQL, properties, Protocol Buffer, Protocol Buffer Text Format, PRQL, PSL, Puppet, Python, Python 2
|
||||
| Q | QBasic, QML
|
||||
| R | R, Racket, Ragel, Raku, react, ReasonML, reg, Rego, reStructuredText, Rexx, RGBDS Assembly, Ring, RPGLE, RPMSpec, Ruby, Rust
|
||||
| S | SAS, Sass, Scala, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, Spade, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
|
||||
| S | SAS, Sass, Scala, scdoc, Scheme, Scilab, SCSS, Sed, Sieve, Smali, Smalltalk, Smarty, SNBT, Snobol, Solidity, SourcePawn, Spade, SPARQL, SQL, SquidConf, Standard ML, stas, Stylus, Svelte, Swift, SYSTEMD, systemverilog
|
||||
| T | TableGen, Tal, TASM, Tcl, Tcsh, Termcap, Terminfo, Terraform, TeX, Thrift, TOML, TradingView, Transact-SQL, Turing, Turtle, Twig, TypeScript, TypoScript, TypoScriptCssData, TypoScriptHtmlData, Typst
|
||||
| U | ucode
|
||||
| V | V, V shell, Vala, VB.net, verilog, VHDL, VHS, VimL, vue
|
||||
@@ -276,6 +276,8 @@ for that setup the `chroma` executable can be just symlinked to `~/.lessfilter`.
|
||||
its input using Chroma
|
||||
* [Hugo](https://gohugo.io/) is a static site generator that [uses Chroma for syntax
|
||||
highlighting code examples](https://gohugo.io/content-management/syntax-highlighting/)
|
||||
* [f4](https://github.com/unxed/f4) is asynchronious cross platform Far Manager clone in Go
|
||||
that uses Chroma for syntax highlighting in built-in editor
|
||||
|
||||
## Testing lexers
|
||||
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ type Colour int32
|
||||
|
||||
// NewColour creates a Colour directly from RGB values.
|
||||
func NewColour(r, g, b uint8) Colour {
|
||||
return ParseColour(fmt.Sprintf("%02x%02x%02x", r, g, b))
|
||||
return Colour(int32(r)<<16|int32(g)<<8|int32(b)) + 1
|
||||
}
|
||||
|
||||
// Distance between this colour and another.
|
||||
|
||||
+57
-13
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -83,6 +84,11 @@ func WithPreWrapper(wrapper PreWrapper) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithModeClasses adds the style's mode (eg. "light" or "dark") as a CSS
|
||||
// class on wrapper elements and scopes WriteCSS rules by mode. This enables
|
||||
// combining light and dark stylesheets and switching themes at runtime.
|
||||
func WithModeClasses(b bool) Option { return func(f *Formatter) { f.modeClasses = b } }
|
||||
|
||||
// WrapLongLines wraps long lines.
|
||||
func WrapLongLines(b bool) Option {
|
||||
return func(f *Formatter) {
|
||||
@@ -206,6 +212,7 @@ type Formatter struct {
|
||||
inlineCode bool
|
||||
preventSurroundingPre bool
|
||||
tabWidth int
|
||||
modeClasses bool
|
||||
wrapLongLines bool
|
||||
lineNumbers bool
|
||||
lineNumbersInTable bool
|
||||
@@ -241,7 +248,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
|
||||
fmt.Fprint(w, "</style>")
|
||||
}
|
||||
fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
|
||||
fmt.Fprintf(w, "<body%s>\n", f.styleAttrWithMode(css, chroma.Background, style))
|
||||
}
|
||||
|
||||
wrapInTable := f.lineNumbers && f.lineNumbersInTable
|
||||
@@ -252,10 +259,10 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
|
||||
if wrapInTable {
|
||||
// List line numbers in its own <td>
|
||||
fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
|
||||
fmt.Fprintf(w, "<div%s>\n", f.styleAttrWithMode(css, chroma.PreWrapper, style))
|
||||
fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttrWithMode(css, chroma.PreWrapper, style)))
|
||||
for index := range lines {
|
||||
line := f.baseLineNumber + index
|
||||
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||
@@ -277,7 +284,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
|
||||
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttrWithMode(css, chroma.PreWrapper, style)))
|
||||
|
||||
highlightIndex = 0
|
||||
for index, tokens := range lines {
|
||||
@@ -288,7 +295,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
highlightIndex++
|
||||
}
|
||||
|
||||
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||
if !f.preventSurroundingPre && !f.inlineCode {
|
||||
// Start of Line
|
||||
fmt.Fprint(w, `<span`)
|
||||
|
||||
@@ -321,7 +328,7 @@ func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.
|
||||
fmt.Fprint(w, html)
|
||||
}
|
||||
|
||||
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||
if !f.preventSurroundingPre && !f.inlineCode {
|
||||
fmt.Fprint(w, `</span>`) // End of CodeLine
|
||||
|
||||
fmt.Fprint(w, `</span>`) // End of Line
|
||||
@@ -414,6 +421,26 @@ func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.Toke
|
||||
return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
|
||||
}
|
||||
|
||||
// modeClass returns the CSS class corresponding to the style's mode (eg.
|
||||
// "light" or "dark"), with the formatter's class prefix applied.
|
||||
func (f *Formatter) modeClass(style *chroma.Style) string {
|
||||
return f.prefix + style.Mode().String()
|
||||
}
|
||||
|
||||
// styleAttrWithMode is like styleAttr but, in classes mode, appends the
|
||||
// style's mode class alongside the existing class. Used for the outer
|
||||
// wrapper and standalone <body> so external CSS can target the mode.
|
||||
func (f *Formatter) styleAttrWithMode(styles map[chroma.TokenType]string, tt chroma.TokenType, style *chroma.Style) string {
|
||||
if !f.Classes || !f.modeClasses {
|
||||
return f.styleAttr(styles, tt)
|
||||
}
|
||||
cls := f.class(tt)
|
||||
if cls == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(` class="%s %s"`, cls, f.modeClass(style))
|
||||
}
|
||||
|
||||
func (f *Formatter) tabWidthStyle() string {
|
||||
if f.tabWidth != 0 && f.tabWidth != 8 {
|
||||
return fmt.Sprintf("-moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d;", f.tabWidth)
|
||||
@@ -437,20 +464,38 @@ func (f *Formatter) writeCSSRule(w io.Writer, comment string, selector string, s
|
||||
}
|
||||
|
||||
// WriteCSS writes CSS style definitions (without any surrounding HTML).
|
||||
//
|
||||
// Rules are scoped by the style's mode (eg. ".chroma.dark") so that CSS
|
||||
// generated from a light and dark style can be combined without conflict.
|
||||
// To support dynamic theme switching, call WriteCSS with both styles,
|
||||
// concatenate the output, and toggle the wrapper's mode class (added
|
||||
// automatically by Format) at runtime. Tokens that one theme leaves
|
||||
// unstyled fall back to that theme's ".chroma.<mode>" text/background
|
||||
// via the CSS cascade; pass WithAllClasses(true) if you need every
|
||||
// token's rule materialised explicitly for both themes.
|
||||
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
css := f.styleCache.get(style, false)
|
||||
var chromaSel, bgSel string
|
||||
if f.modeClasses {
|
||||
modeCls := f.modeClass(style)
|
||||
chromaSel = fmt.Sprintf(".%schroma.%s", f.prefix, modeCls)
|
||||
bgSel = fmt.Sprintf(".%sbg.%s", f.prefix, modeCls)
|
||||
} else {
|
||||
chromaSel = fmt.Sprintf(".%schroma", f.prefix)
|
||||
bgSel = fmt.Sprintf(".%sbg", f.prefix)
|
||||
}
|
||||
|
||||
// Special-case background as it is mapped to the outer ".chroma" class.
|
||||
if err := f.writeCSSRule(w, chroma.Background.String(), fmt.Sprintf(".%sbg", f.prefix), css[chroma.Background]); err != nil {
|
||||
if err := f.writeCSSRule(w, chroma.Background.String(), bgSel, css[chroma.Background]); err != nil {
|
||||
return err
|
||||
}
|
||||
// Special-case PreWrapper as it is the ".chroma" class.
|
||||
if err := f.writeCSSRule(w, chroma.PreWrapper.String(), fmt.Sprintf(".%schroma", f.prefix), css[chroma.PreWrapper]); err != nil {
|
||||
if err := f.writeCSSRule(w, chroma.PreWrapper.String(), chromaSel, css[chroma.PreWrapper]); err != nil {
|
||||
return err
|
||||
}
|
||||
// Special-case code column of table to expand width.
|
||||
if f.lineNumbers && f.lineNumbersInTable {
|
||||
selector := fmt.Sprintf(".%schroma .%s:last-child", f.prefix, f.class(chroma.LineTableTD))
|
||||
selector := fmt.Sprintf("%s .%s:last-child", chromaSel, f.class(chroma.LineTableTD))
|
||||
if err := f.writeCSSRule(w, chroma.LineTableTD.String(), selector, "width: 100%;"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -460,7 +505,7 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
|
||||
for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
|
||||
comment := fmt.Sprintf("%s targeted by URL anchor", tt)
|
||||
selector := fmt.Sprintf(".%schroma .%s:target", f.prefix, f.class(tt))
|
||||
selector := fmt.Sprintf("%s .%s:target", chromaSel, f.class(tt))
|
||||
if err := f.writeCSSRule(w, comment, selector, targetedLineCSS); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -481,7 +526,7 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
if class == "" {
|
||||
continue
|
||||
}
|
||||
if err := f.writeCSSRule(w, tt.String(), fmt.Sprintf(".%schroma .%s", f.prefix, class), css[tt]); err != nil {
|
||||
if err := f.writeCSSRule(w, tt.String(), fmt.Sprintf("%s .%s", chromaSel, class), css[tt]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -613,8 +658,7 @@ func (l *styleCache) get(style *chroma.Style, compress bool) map[chroma.TokenTyp
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// Look for an existing entry.
|
||||
for i := len(l.cache) - 1; i >= 0; i-- {
|
||||
entry := l.cache[i]
|
||||
for i, entry := range slices.Backward(l.cache) {
|
||||
if entry.style == style && entry.compressed == compress {
|
||||
// Top of the cache, no need to adjust the order.
|
||||
if i == len(l.cache)-1 {
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Arturo</name>
|
||||
<alias>arturo</alias>
|
||||
<alias>art</alias>
|
||||
<filename>*.art</filename>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern=";.*?$"><token type="CommentSingle"/></rule>
|
||||
<rule pattern="^((\s#!)|(#!)).*?$"><token type="CommentHashbang"/></rule>
|
||||
<rule pattern="(false|true|maybe)\b"><token type="NameConstant"/></rule>
|
||||
<rule pattern="\b(this|init)\b\??:?"><token type="NameBuiltinPseudo"/></rule>
|
||||
<rule pattern="`.`"><token type="LiteralStringChar"/></rule>
|
||||
<rule pattern="\\\w+\b\??:?"><token type="NameProperty"/></rule>
|
||||
<rule pattern="#\w+"><token type="NameConstant"/></rule>
|
||||
<rule pattern="\b[0-9]+\.[0-9]+"><token type="LiteralNumberFloat"/></rule>
|
||||
<rule pattern="\b[0-9]+"><token type="LiteralNumberInteger"/></rule>
|
||||
<rule pattern="\w+\b\??:"><token type="NameLabel"/></rule>
|
||||
<rule pattern="\'(?:\w+\b\??:?)"><token type="KeywordDeclaration"/></rule>
|
||||
<rule pattern="\:\w+"><token type="KeywordType"/></rule>
|
||||
<rule pattern="\.\w+\??:?"><token type="NameAttribute"/></rule>
|
||||
<rule pattern="(\()(.*?)(\)\?)"><bygroups><token type="Punctuation"/><usingself state="root"/><token type="Punctuation"/></bygroups></rule>
|
||||
<rule pattern="""><token type="LiteralStringDouble"/><push state="inside-simple-string"/></rule>
|
||||
<rule pattern="»"><token type="LiteralStringSingle"/><push state="inside-smart-string"/></rule>
|
||||
<rule pattern="«««"><token type="LiteralStringDouble"/><push state="inside-safe-string"/></rule>
|
||||
<rule pattern="\{\/"><token type="LiteralStringSingle"/><push state="inside-regex-string"/></rule>
|
||||
<rule pattern="\{\:"><token type="LiteralStringDouble"/><push state="inside-curly-verb-string"/></rule>
|
||||
<rule pattern="(\{)(\!)(\w+)(\s|\n)([\w\W]*?)(^\})">
|
||||
<usingbygroup>
|
||||
<sublexer_name_group>3</sublexer_name_group>
|
||||
<code_group>5</code_group>
|
||||
<emitters>
|
||||
<token type="LiteralStringDouble"/>
|
||||
<token type="LiteralStringInterpol"/>
|
||||
<token type="LiteralStringInterpol"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="LiteralString"/>
|
||||
<token type="LiteralStringDouble"/>
|
||||
</emitters>
|
||||
</usingbygroup>
|
||||
</rule>
|
||||
<rule pattern="\{"><token type="LiteralStringSingle"/><push state="inside-curly-string"/></rule>
|
||||
<rule pattern="\-{3,}"><token type="LiteralStringSingle"/><push state="inside-eof-string"/></rule>
|
||||
<rule><include state="builtin-functions"/></rule>
|
||||
<rule pattern="[()[\],]"><token type="Punctuation"/></rule>
|
||||
<rule pattern="(\->|==>|\||::|@|\#|\$|\&|!|!!|\./)"><token type="NameDecorator"/></rule>
|
||||
<rule pattern="(<:|:>|:<|>:|<\\|<>|<|>|ø|∞|\+|\-|\*|\~|=|\^|%|/|//|==>|<=>|<==>|=>>|<<=>>|<<==>>|\-\->|<\->|<\-\->|=\||\|=|\-:|:\-|_|\.|\.\.|\\)"><token type="Operator"/></rule>
|
||||
<rule pattern="\b\w+"><token type="Name"/></rule>
|
||||
<rule pattern="\s+"><token type="TextWhitespace"/></rule>
|
||||
<rule pattern=".+$"><token type="Error"/></rule>
|
||||
</state>
|
||||
<state name="inside-interpol">
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|]+"><usingself state="root"/></rule>
|
||||
</state>
|
||||
<state name="inside-template">
|
||||
<rule pattern="\|\|\>"><token type="LiteralStringInterpol"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|]+"><usingself state="root"/></rule>
|
||||
</state>
|
||||
<state name="string-escape">
|
||||
<rule pattern="(\\\\|\\n|\\t|\\")"><token type="LiteralStringEscape"/></rule>
|
||||
</state>
|
||||
<state name="inside-simple-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="""><token type="LiteralStringDouble"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|"]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="inside-smart-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="\n"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|\n]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="inside-safe-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="»»»"><token type="LiteralStringDouble"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|»]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="inside-regex-string">
|
||||
<rule pattern="\\[sSwWdDbBZApPxucItnvfr0]+"><token type="LiteralStringEscape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="\/\}"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|\/]+"><token type="LiteralStringRegex"/></rule>
|
||||
</state>
|
||||
<state name="inside-curly-verb-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="\:\}"><token type="LiteralStringDouble"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|<:]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="inside-curly-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="\}"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|<}]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="inside-eof-string">
|
||||
<rule><include state="string-escape"/></rule>
|
||||
<rule pattern="\|"><token type="LiteralStringInterpol"/><push state="inside-interpol"/></rule>
|
||||
<rule pattern="\<\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
|
||||
<rule pattern="\Z"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
|
||||
<rule pattern="[^|<]+"><token type="LiteralString"/></rule>
|
||||
</state>
|
||||
<state name="builtin-functions">
|
||||
<rule pattern="\b(all|and|any|ascii|attr|attribute|attributeLabel|binary|blockchar|contains|database|date|dictionary|empty|equal|even|every|exists|false|floatin|function|greater|greaterOrEqual|if|in|inline|integer|is|key|label|leap|less|lessOrEqual|literal|logical|lower|nand|negative|nor|not|notEqual|null|numeric|odd|or|path|pathLabel|positive|prefix|prime|set|some|sorted|standalone|string|subset|suffix|superset|ymbol|true|try|type|unless|upper|when|whitespace|word|xnor|xor|zero)\b\?"><token type="NameBuiltin"/></rule>
|
||||
<rule pattern="\b(abs|acos|acosh|acsec|acsech|actan|actanh|add|after|alphabet|and|angle|append|arg|args|arity|array|as|asec|asech|asin|asinh|atan|atan2|atanh|attr|attrs|average|before|benchmark|blend|break|builtins1|builtins2|call|capitalize|case|ceil|chop|chunk|clear|close|cluster|color|combine|conj|continue|copy|cos|cosh|couple|csec|csech|ctan|ctanh|cursor|darken|dec|decode|decouple|define|delete|desaturate|deviation|dictionary|difference|digest|digits|div|do|download|drop|dup|e|else|empty|encode|ensure|env|epsilon|escape|execute|exit|exp|extend|extract|factors|false|fdiv|filter|first|flatten|floor|fold|from|function|gamma|gcd|get|goto|hash|help|hypot|if|in|inc|indent|index|infinity|info|input|insert|inspect|intersection|invert|join|keys|kurtosis|last|let|levenshtein|lighten|list|ln|log|loop|lower|mail|map|match|max|maybe|median|min|mod|module|mul|nand|neg|new|nor|normalize|not|now|null|open|or|outdent|pad|panic|path|pause|permissions|permutate|pi|pop|pow|powerset|powmod|prefix|print|prints|process|product|query|random|range|read|relative|remove|rename|render|repeat|replace|request|return|reverse|round|sample|saturate|script|sec|sech|select|serve|set|shl|shr|shuffle|sin|sinh|size|skewness|slice|sort|split|sqrt|squeeze|stack|strip|sub|suffix|sum|switch|symbols|symlink|sys|take|tan|tanh|terminal|to|true|truncate|try|type|union|unique|unless|until|unzip|upper|values|var|variance|volume|webview|while|with|wordwrap|write|xnor|xor|zip)\b"><token type="NameBuiltin"/></rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+1
-1
@@ -81,7 +81,7 @@
|
||||
<rule pattern="[^\S\n]+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="//.*?\n">
|
||||
<rule pattern="//[^\n]*\n?">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/\*.*?\*/">
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Gemfile.lock</name>
|
||||
<alias>gemfile-lock</alias>
|
||||
<alias>gemfilelock</alias>
|
||||
<filename>Gemfile.lock</filename>
|
||||
<filename>*.gemfile.lock</filename>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="^(GIT|PATH|GEM|PLUGIN SOURCE|PLATFORMS|DEPENDENCIES|BUNDLED WITH|RUBY VERSION|CHECKSUMS)$">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="^([ \t]+)(remote|revision|ref|branch|tag|submodules|specs|glob)(:)">
|
||||
<bygroups>
|
||||
<token type="Text"/>
|
||||
<token type="NameAttribute"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="\(">
|
||||
<token type="Punctuation"/>
|
||||
<push state="version"/>
|
||||
</rule>
|
||||
<rule pattern="!">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="https?://\S+">
|
||||
<token type="LiteralStringSymbol"/>
|
||||
</rule>
|
||||
<rule pattern="git@\S+">
|
||||
<token type="LiteralStringSymbol"/>
|
||||
</rule>
|
||||
<rule pattern="sha\d+=[A-Fa-f0-9]+">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="\b[a-f0-9]{7,40}\b">
|
||||
<token type="LiteralNumberHex"/>
|
||||
</rule>
|
||||
<rule pattern="\b\d[\w.]*">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
<rule pattern="[A-Za-z_][A-Za-z0-9_.-]*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern="\n">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="[ \t]+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern=".">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="version">
|
||||
<rule pattern="\)">
|
||||
<token type="Punctuation"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="(~>|>=|<=|!=|=|<|>)">
|
||||
<token type="Operator"/>
|
||||
</rule>
|
||||
<rule pattern="[0-9][\w.]*">
|
||||
<token type="LiteralNumber"/>
|
||||
</rule>
|
||||
<rule pattern="[A-Za-z][\w.-]*">
|
||||
<token type="Name"/>
|
||||
</rule>
|
||||
<rule pattern=",">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern=".">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+16
-3
@@ -32,7 +32,7 @@
|
||||
pattern="(assert|break|case|catch|continue|default|do|else|finally|for|if|goto|instanceof|new|return|switch|this|throw|try|while)\b">
|
||||
<token type="Keyword" />
|
||||
</rule>
|
||||
<rule pattern="((?:(?:[^\W\d]|\$)[\w.\[\]$<>]*\s+)+?)((?:[^\W\d]|\$)[\w$]*)(\s*)(\()">
|
||||
<rule pattern="((?:(?:[^\W\d]|\$)[\w.\[\]$<>?]*\s+)+?)((?:[^\W\d]|\$)[\w$]*)(\s*)(\()">
|
||||
<bygroups>
|
||||
<usingself state="root" />
|
||||
<token type="NameFunction" />
|
||||
@@ -44,7 +44,7 @@
|
||||
<token type="NameDecorator" />
|
||||
</rule>
|
||||
<rule
|
||||
pattern="(abstract|const|enum|extends|final|implements|native|private|protected|public|sealed|static|strictfp|super|synchronized|throws|transient|volatile|yield)\b">
|
||||
pattern="(abstract|const|enum|exports|extends|final|implements|native|non-sealed|open|opens|permits|private|protected|provides|public|requires|sealed|static|strictfp|super|synchronized|throws|to|transient|transitive|uses|volatile|with|yield)\b">
|
||||
<token type="KeywordDeclaration" />
|
||||
</rule>
|
||||
<rule pattern="(boolean|byte|char|double|float|int|long|short|void)\b">
|
||||
@@ -64,6 +64,10 @@
|
||||
<token type="KeywordDeclaration" />
|
||||
<push state="class" />
|
||||
</rule>
|
||||
<rule pattern="(module)\b">
|
||||
<token type="KeywordDeclaration" />
|
||||
<push state="module" />
|
||||
</rule>
|
||||
<rule pattern="(var)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration" />
|
||||
@@ -71,7 +75,7 @@
|
||||
</bygroups>
|
||||
<push state="var" />
|
||||
</rule>
|
||||
<rule pattern="(import(?:\s+static)?)(\s+)">
|
||||
<rule pattern="(import(?:\s+(?:static|module))?)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordNamespace" />
|
||||
<token type="TextWhitespace" />
|
||||
@@ -147,6 +151,15 @@
|
||||
<pop depth="1" />
|
||||
</rule>
|
||||
</state>
|
||||
<state name="module">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text" />
|
||||
</rule>
|
||||
<rule pattern="([^\W\d]|\$)[\w$]*">
|
||||
<token type="NameClass" />
|
||||
<pop depth="1" />
|
||||
</rule>
|
||||
</state>
|
||||
<state name="var">
|
||||
<rule pattern="([^\W\d]|\$)[\w$]*">
|
||||
<token type="Name" />
|
||||
|
||||
+3
@@ -2,12 +2,15 @@
|
||||
<config>
|
||||
<name>JSON</name>
|
||||
<alias>json</alias>
|
||||
<alias>jsonl</alias>
|
||||
<filename>*.json</filename>
|
||||
<filename>*.jsonl</filename>
|
||||
<filename>*.jsonc</filename>
|
||||
<filename>*.json5</filename>
|
||||
<filename>*.avsc</filename>
|
||||
<filename>.luaurc</filename>
|
||||
<mime_type>application/json</mime_type>
|
||||
<mime_type>application/jsonl</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
<not_multiline>true</not_multiline>
|
||||
</config>
|
||||
|
||||
+133
File diff suppressed because one or more lines are too long
+6
-1
@@ -3,7 +3,12 @@
|
||||
<name>Nu</name>
|
||||
<alias>nu</alias>
|
||||
<filename>*.nu</filename>
|
||||
<mime_type>application/x-shellscript</mime_type>
|
||||
<mime_type>text/plain</mime_type>
|
||||
<mime_type>text/x-shellscript</mime_type>
|
||||
<analyse first="true" >
|
||||
<regex pattern="(?m)^#!.*/bin/(?:env(?: -[a-zA-Z0-9]+)*(?: --[a-zA-Z0-9-=]+)* |)nu" score="1.0" />
|
||||
</analyse>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
@@ -118,4 +123,4 @@
|
||||
<rule><include state="root" /></rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
</lexer>
|
||||
+5
-1
@@ -5,6 +5,10 @@
|
||||
<alias>postscr</alias>
|
||||
<filename>*.ps</filename>
|
||||
<filename>*.eps</filename>
|
||||
<filename>*.epsf</filename>
|
||||
<filename>*.epsi</filename>
|
||||
<filename>*.pfa</filename>
|
||||
<filename>*.t42</filename>
|
||||
<mime_type>application/postscript</mime_type>
|
||||
</config>
|
||||
<rules>
|
||||
@@ -86,4 +90,4 @@
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
</lexer>
|
||||
|
||||
Generated
Vendored
+12
-6
@@ -7,7 +7,7 @@
|
||||
</config>
|
||||
<rules>
|
||||
<state name="package">
|
||||
<rule pattern="[a-zA-Z_]\w*">
|
||||
<rule pattern="[a-zA-Z_][\w.]*">
|
||||
<token type="NameNamespace"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
@@ -16,7 +16,7 @@
|
||||
</rule>
|
||||
</state>
|
||||
<state name="message">
|
||||
<rule pattern="[a-zA-Z_]\w*">
|
||||
<rule pattern="[a-zA-Z_][\w.]*">
|
||||
<token type="NameClass"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
@@ -34,7 +34,7 @@
|
||||
</rule>
|
||||
</state>
|
||||
<state name="root">
|
||||
<rule pattern="[ \t]+">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="[,;{}\[\]()<>]">
|
||||
@@ -46,9 +46,12 @@
|
||||
<rule pattern="/(\\\n)?\*(.|\n)*?\*(\\\n)?/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="\b(extensions|required|repeated|optional|returns|default|option|packed|import|ctype|oneof|max|rpc|to)\b">
|
||||
<rule pattern="\b(ctype|default|edition|export|local|max|option|optional|packed|public|repeated|required|reserved|returns|stream|syntax|to|weak)\b">
|
||||
<token type="Keyword"/>
|
||||
</rule>
|
||||
<rule pattern="\b(extensions|map)\b">
|
||||
<token type="KeywordDeclaration"/>
|
||||
</rule>
|
||||
<rule pattern="(sfixed32|sfixed64|fixed32|fixed64|sint32|sint64|double|string|uint32|uint64|int32|float|int64|bytes|bool)\b">
|
||||
<token type="KeywordType"/>
|
||||
</rule>
|
||||
@@ -62,6 +65,9 @@
|
||||
</bygroups>
|
||||
<push state="package"/>
|
||||
</rule>
|
||||
<rule pattern="import\b">
|
||||
<token type="KeywordNamespace"/>
|
||||
</rule>
|
||||
<rule pattern="(message|extend)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
@@ -69,7 +75,7 @@
|
||||
</bygroups>
|
||||
<push state="message"/>
|
||||
</rule>
|
||||
<rule pattern="(enum|group|service)(\s+)">
|
||||
<rule pattern="(enum|group|oneof|rpc|service)(\s+)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="Text"/>
|
||||
@@ -115,4 +121,4 @@
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
</lexer>
|
||||
|
||||
+4
@@ -12,7 +12,11 @@
|
||||
<filename>*.rbx</filename>
|
||||
<filename>*.duby</filename>
|
||||
<filename>Gemfile</filename>
|
||||
<filename>*.gemfile</filename>
|
||||
<filename>Vagrantfile</filename>
|
||||
<filename>Appraisals</filename>
|
||||
<filename>.pryrc</filename>
|
||||
<filename>*.json.jbuilder</filename>
|
||||
<mime_type>text/x-ruby</mime_type>
|
||||
<mime_type>application/x-ruby</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
<lexer>
|
||||
<config>
|
||||
<name>Templ</name>
|
||||
<alias>templ</alias>
|
||||
<filename>*.templ</filename>
|
||||
<mime_type>text/x-templ</mime_type>
|
||||
<dot_all>true</dot_all>
|
||||
<analyse>
|
||||
<regex pattern="(?m)^\s*templ\s+[A-Za-z_]\w*\s*\(" score="0.7"/>
|
||||
<regex pattern="(?m)^\s*package\s+\w+[\s\S]*^\s*templ\s+" score="0.5"/>
|
||||
</analyse>
|
||||
</config>
|
||||
<rules>
|
||||
<state name="root">
|
||||
<rule pattern="//[^\n\r]*">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/\*(?:.|\n)*?\*/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="(?m)^(\s*)(package|import|const|type|func)([^\n]*)">
|
||||
<bygroups>
|
||||
<token type="TextWhitespace"/>
|
||||
<using lexer="Go"/>
|
||||
<using lexer="Go"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="\b(templ|css|script)(\s+)([A-Za-z_]\w*)(\s*)(\([^{}]*\))(\s*)({)">
|
||||
<bygroups>
|
||||
<token type="KeywordDeclaration"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="NameFunction"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="@[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?:\([^{}()\r\n]*(?:\([^{}()\r\n]*\)[^{}()\r\n]*)*\))?(?:[ \t]*{)?">
|
||||
<token type="NameFunction"/>
|
||||
</rule>
|
||||
<rule pattern="(?m)^(\s*)(if|for|switch|select)(\s+)([^{}\n]*)(\s*)({)">
|
||||
<bygroups>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(?m)^(\s*)(else)(\s*)(if)?(\s*)([^{}\n]*)(\s*)({)?">
|
||||
<bygroups>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="(})(\s*)(else)(\s*)(if)?(\s*)([^{}\n]*)(\s*)({)?">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="({)([^{}\n]*)(})">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="}">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="`(?:.|\n)*?`">
|
||||
<token type="LiteralStringBacktick"/>
|
||||
</rule>
|
||||
<rule pattern="\s+">
|
||||
<token type="TextWhitespace"/>
|
||||
</rule>
|
||||
<rule pattern="[^<&@{}`\s]+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="&\S*?;">
|
||||
<token type="NameEntity"/>
|
||||
</rule>
|
||||
<rule pattern="\<\!\[CDATA\[.*?\]\]\>">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
<rule pattern="<!--">
|
||||
<token type="Comment"/>
|
||||
<push state="comment"/>
|
||||
</rule>
|
||||
<rule pattern="<\?.*?\?>">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
<rule pattern="<![^>]*>">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
<rule pattern="(<)(script)(\s*)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameTag"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="script-content" state="tag"/>
|
||||
</rule>
|
||||
<rule pattern="(<)(style)(\s*)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameTag"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="style-content" state="tag"/>
|
||||
</rule>
|
||||
<rule pattern="(<)([A-Za-z][\w:.-]*)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameTag"/>
|
||||
</bygroups>
|
||||
<push state="tag"/>
|
||||
</rule>
|
||||
<rule pattern="(</)([A-Za-z][\w:.-]*)(\s*)(>)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="NameTag"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="[@{}<&]">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="script-content">
|
||||
<rule pattern="(<)(\s*)(/)(\s*)(script)(\s*)(>)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameTag"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="\s*[^\r\n]*\{\{.*?\}\}[^\r\n]*">
|
||||
<token type="Other"/>
|
||||
</rule>
|
||||
<rule pattern=".+?(?=<\s*/\s*script\s*>)">
|
||||
<token type="Other"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="style-content">
|
||||
<rule pattern="(<)(\s*)(/)(\s*)(style)(\s*)(>)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="NameTag"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern=".+?(?=<\s*/\s*style\s*>)">
|
||||
<using lexer="CSS"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="comment">
|
||||
<rule pattern="[^-]+">
|
||||
<token type="Comment"/>
|
||||
</rule>
|
||||
<rule pattern="-->">
|
||||
<token type="Comment"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="-">
|
||||
<token type="Comment"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="tag">
|
||||
<rule pattern="\s+">
|
||||
<token type="Text"/>
|
||||
</rule>
|
||||
<rule pattern="//[^\n\r]*">
|
||||
<token type="CommentSingle"/>
|
||||
</rule>
|
||||
<rule pattern="/\*(?:.|\n)*?\*/">
|
||||
<token type="CommentMultiline"/>
|
||||
</rule>
|
||||
<rule pattern="\b(if|for|switch|select)(\s+)([^{}]*)(\s*)({)">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="\b(else)(\s*)(if)?(\s*)([^{}]*)(\s*)({)?">
|
||||
<bygroups>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<token type="Keyword"/>
|
||||
<token type="Text"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="}">
|
||||
<token type="Punctuation"/>
|
||||
</rule>
|
||||
<rule pattern="({)([^{}]*)(})(\??)(\s*)(=)(\s*)({)([^{}]*)(})">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="({)([^{}]*)(})(\??)(\s*)(=)(\s*)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="attr"/>
|
||||
</rule>
|
||||
<rule pattern="({)([^{}]*)(})(\??)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Operator"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="({)([^{}]*)(})">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="((?:[@#.][\w:.*-]+|\[[^\]\s=]+\]|[\w:.*-]+\??)\s*)(=)(\s*)({)([^{}]*)(})">
|
||||
<bygroups>
|
||||
<token type="NameAttribute"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
<using lexer="Go"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="((?:[@#.][\w:.*-]+|\[[^\]\s=]+\]|[\w:.*-]+\??)\s*)(=)(\s*)">
|
||||
<bygroups>
|
||||
<token type="NameAttribute"/>
|
||||
<token type="Operator"/>
|
||||
<token type="Text"/>
|
||||
</bygroups>
|
||||
<push state="attr"/>
|
||||
</rule>
|
||||
<rule pattern="(?:[@#.][\w:.*-]+|\[[^\]\s=]+\]|[\w:.*-]+\??)">
|
||||
<token type="NameAttribute"/>
|
||||
</rule>
|
||||
<rule pattern="(/?)(\s*)(>)">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="Text"/>
|
||||
<token type="Punctuation"/>
|
||||
</bygroups>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="attr">
|
||||
<rule pattern="".*?"">
|
||||
<token type="LiteralString"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="'.*?'">
|
||||
<token type="LiteralString"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
<rule pattern="[^\s>]+">
|
||||
<token type="LiteralString"/>
|
||||
<pop depth="1"/>
|
||||
</rule>
|
||||
</state>
|
||||
</rules>
|
||||
</lexer>
|
||||
+14
-4
@@ -23,6 +23,9 @@
|
||||
<rule pattern="#.*$">
|
||||
<token type="Comment"/>
|
||||
</rule>
|
||||
<rule pattern="//.*$">
|
||||
<token type="Comment"/>
|
||||
</rule>
|
||||
<rule pattern="!![^\s]+">
|
||||
<token type="CommentPreproc"/>
|
||||
</rule>
|
||||
@@ -78,15 +81,22 @@
|
||||
<token type="Comment"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="([^\{\}\[\]\?,\:\!\-\*&\@].*)( )+(//.*)">
|
||||
<bygroups>
|
||||
<token type="Literal"/>
|
||||
<token type="TextWhitespace"/>
|
||||
<token type="Comment"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="[^\{\}\[\]\?,\:\!\-\*&\@].*">
|
||||
<token type="Literal"/>
|
||||
</rule>
|
||||
</state>
|
||||
<state name="key">
|
||||
<rule pattern=""[^"\n].*": ">
|
||||
<rule pattern=""[^"\n#].*": ">
|
||||
<token type="NameTag"/>
|
||||
</rule>
|
||||
<rule pattern="(-)( )([^"\n{]*)(:)( )">
|
||||
<rule pattern="(-)( )((?:(?!//)[^"\n{#])*?)(:)( )">
|
||||
<bygroups>
|
||||
<token type="Punctuation"/>
|
||||
<token type="TextWhitespace"/>
|
||||
@@ -95,14 +105,14 @@
|
||||
<token type="TextWhitespace"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="([^"\n{]*)(:)( )">
|
||||
<rule pattern="((?:(?!//)[^"\n{#])*?)(:)( )">
|
||||
<bygroups>
|
||||
<token type="NameTag"/>
|
||||
<token type="Punctuation"/>
|
||||
<token type="TextWhitespace"/>
|
||||
</bygroups>
|
||||
</rule>
|
||||
<rule pattern="([^"\n{]*)(:)(\n)">
|
||||
<rule pattern="((?:(?!//)[^"\n{#])*?)(:)(\n)">
|
||||
<bygroups>
|
||||
<token type="NameTag"/>
|
||||
<token type="Punctuation"/>
|
||||
|
||||
+2
-2
@@ -30,8 +30,8 @@ func goRules() Rules {
|
||||
"root": {
|
||||
{`\n`, TextWhitespace, nil},
|
||||
{`\s+`, TextWhitespace, nil},
|
||||
{`//[^\s][^\n\r]*`, CommentPreproc, nil},
|
||||
{`//\s+[^\n\r]*`, CommentSingle, nil},
|
||||
{`//[^\s\n\r][^\n\r]*`, CommentPreproc, nil},
|
||||
{`//[^\n\r]*`, CommentSingle, nil},
|
||||
{`/(\\\n)?[*](.|\n)*?[*](\\\n)?/`, CommentMultiline, nil},
|
||||
{`(import|package)\b`, KeywordNamespace, nil},
|
||||
{`(var|func|struct|map|chan|type|interface|const)\b`, KeywordDeclaration, nil},
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ func (d *httpBodyContentTyper) Tokenise(options *TokeniseOptions, text string) (
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return EOF
|
||||
return subIterator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
-3
@@ -1,11 +1,13 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
// Markdown lexer.
|
||||
var Markdown = Register(MustNewLexer(
|
||||
// Markdown lexer with YAML frontmatter and HTML comment support.
|
||||
var Markdown = Register(&markdownLexer{Lexer: MustNewLexer(
|
||||
&Config{
|
||||
Name: "markdown",
|
||||
Aliases: []string{"md", "mkd"},
|
||||
@@ -13,11 +15,69 @@ var Markdown = Register(MustNewLexer(
|
||||
MimeTypes: []string{"text/x-markdown"},
|
||||
},
|
||||
markdownRules,
|
||||
))
|
||||
)})
|
||||
|
||||
// markdownLexer wraps the base Markdown lexer to highlight top-of-file YAML frontmatter.
|
||||
type markdownLexer struct {
|
||||
Lexer
|
||||
}
|
||||
|
||||
// Lexes Markdown, highlighting a leading YAML frontmatter block before delegating to Markdown rules.
|
||||
func (m *markdownLexer) Tokenise(options *TokeniseOptions, text string) (Iterator, error) {
|
||||
frontmatter, rest, ok := splitFrontmatter(text)
|
||||
if !ok {
|
||||
return m.Lexer.Tokenise(options, text)
|
||||
}
|
||||
|
||||
yamlLexer := Get("YAML")
|
||||
if yamlLexer == nil {
|
||||
return m.Lexer.Tokenise(options, text)
|
||||
}
|
||||
|
||||
yamlTokens, err := yamlLexer.Tokenise(options, frontmatter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
markdownTokens, err := m.Lexer.Tokenise(options, rest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Concaterator(yamlTokens, markdownTokens), nil
|
||||
}
|
||||
|
||||
// Extracts a leading YAML frontmatter block if the document starts with one.
|
||||
func splitFrontmatter(text string) (frontmatter string, rest string, ok bool) {
|
||||
if !strings.HasPrefix(text, "---\n") && !strings.HasPrefix(text, "---\r\n") {
|
||||
return "", text, false
|
||||
}
|
||||
|
||||
lineEnd := strings.IndexByte(text, '\n')
|
||||
if lineEnd < 0 {
|
||||
return "", text, false
|
||||
}
|
||||
if strings.TrimSuffix(text[:lineEnd], "\r") != "---" {
|
||||
return "", text, false
|
||||
}
|
||||
|
||||
for pos := lineEnd + 1; pos < len(text); {
|
||||
next := strings.IndexByte(text[pos:], '\n')
|
||||
if next < 0 {
|
||||
break
|
||||
}
|
||||
lineEnd = pos + next
|
||||
line := strings.TrimSuffix(text[pos:lineEnd], "\r")
|
||||
if line == "---" {
|
||||
return text[:lineEnd+1], text[lineEnd+1:], true
|
||||
}
|
||||
pos = lineEnd + 1
|
||||
}
|
||||
return "", text, false
|
||||
}
|
||||
|
||||
func markdownRules() Rules {
|
||||
return Rules{
|
||||
"root": {
|
||||
{`<!--[\w\W]*?-->`, CommentMultiline, nil},
|
||||
{`^(#[^#].+\n)`, ByGroups(GenericHeading), nil},
|
||||
{`^(#{2,6}.+\n)`, ByGroups(GenericSubheading), nil},
|
||||
{`^(\s*)([*-] )(\[[ xX]\])( .+\n)`, ByGroups(Text, Keyword, Keyword, UsingSelf("inline")), nil},
|
||||
@@ -33,6 +93,7 @@ func markdownRules() Rules {
|
||||
Include("inline"),
|
||||
},
|
||||
"inline": {
|
||||
{`<!--[\w\W]*?-->`, CommentMultiline, nil},
|
||||
{`\\.`, Text, nil},
|
||||
{`(\s)(\*|_)((?:(?!\2).)*)(\2)((?=\W|\n))`, ByGroups(Text, GenericEmph, GenericEmph, GenericEmph, Text), nil},
|
||||
{`(\s)((\*\*|__).*?)\3((?=\W|\n))`, ByGroups(Text, GenericStrong, GenericStrong, Text), nil},
|
||||
|
||||
+2
-2
@@ -70,14 +70,14 @@ func marklessRules() Rules {
|
||||
{`(! )([^ ]+)(.+?)$`, ByGroups(Keyword, NameFunction, NameVariable), nil},
|
||||
},
|
||||
"embed": {
|
||||
{`(\[ )([^ ]+)( )([^,]+)`, ByGroups(Keyword, NameFunction, TextWhitespace, String), Push("embed-options")},
|
||||
{`(\[ )([^ ]+)( )([^,\]\n]+)`, ByGroups(Keyword, NameFunction, TextWhitespace, String), Push("embed-options")},
|
||||
},
|
||||
"embed-options": {
|
||||
{`\\.`, Text, nil},
|
||||
{`,`, Punctuation, nil},
|
||||
{`\]?$`, Keyword, Pop(1)},
|
||||
// Generic key or key/value pair
|
||||
{`( *)([^, \]]+)([^,\]]+)?`, ByGroups(TextWhitespace, NameFunction, String), nil},
|
||||
{`( *)([^, \]\n]+)([^,\]\n]+)?`, ByGroups(TextWhitespace, NameFunction, String), nil},
|
||||
{`.`, Text, nil},
|
||||
},
|
||||
"footnote": {
|
||||
|
||||
+4
-6
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
@@ -458,8 +458,7 @@ func rakuRules() Rules {
|
||||
var podRegex *regexp2.Regexp
|
||||
if tokenClass == rakuPod {
|
||||
podRegex = regexp2.MustCompile(
|
||||
state.NamedGroups[`ws`]+`=end`+`\s+`+regexp2.Escape(state.NamedGroups[`name`]),
|
||||
0,
|
||||
state.NamedGroups[`ws`] + `=end` + `\s+` + regexp2.Escape(state.NamedGroups[`name`]),
|
||||
)
|
||||
} else {
|
||||
closingChars = []rune(strings.Repeat(string(closingChar), nChars))
|
||||
@@ -478,7 +477,7 @@ func rakuRules() Rules {
|
||||
match, err := podRegex.FindRunesMatchStartingAt(text, searchPos+nChars)
|
||||
if err == nil {
|
||||
closingChars = match.Runes()
|
||||
nextClosePos = match.Index
|
||||
nextClosePos = match.RuneIndex
|
||||
} else {
|
||||
nextClosePos = -1
|
||||
}
|
||||
@@ -1599,8 +1598,7 @@ func quote(groups []string, state *LexerState) Iterator {
|
||||
var tokenStates []string
|
||||
|
||||
// Set tokenStates based on adverbs
|
||||
adverbs := strings.Split(adverbsStr, ":")
|
||||
for _, adverb := range adverbs {
|
||||
for adverb := range strings.SplitSeq(adverbsStr, ":") {
|
||||
switch adverb {
|
||||
case "c", "closure":
|
||||
tokenStates = append(tokenStates, "Q-closure")
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package lexers
|
||||
|
||||
import (
|
||||
. "github.com/alecthomas/chroma/v2" // nolint
|
||||
)
|
||||
|
||||
// YAML+Jinja is YAML with Jinja templating embedded. Used by Ansible playbooks
|
||||
// and Salt SLS files.
|
||||
var YAMLJinja = Register(DelegatingLexer(
|
||||
MustNewXMLLexer(embedded, "embedded/yaml.xml"),
|
||||
MustNewXMLLexer(embedded, "embedded/django_jinja.xml").SetConfig(
|
||||
&Config{
|
||||
Name: "YAML+Jinja",
|
||||
Aliases: []string{"yaml+jinja", "salt", "sls", "ansible"},
|
||||
Filenames: []string{"*.sls"},
|
||||
MimeTypes: []string{"text/x-yaml+jinja", "text/x-sls"},
|
||||
DotAll: true,
|
||||
},
|
||||
),
|
||||
))
|
||||
+9
-10
@@ -3,6 +3,7 @@ package chroma
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
)
|
||||
|
||||
// A Rule is the fundamental matching unit of the Regex lexer state machine.
|
||||
@@ -70,9 +71,7 @@ func (r Rules) Clone() Rules {
|
||||
// Merge creates a clone of "r" then merges "rules" into the clone.
|
||||
func (r Rules) Merge(rules Rules) Rules {
|
||||
out := r.Clone()
|
||||
for k, v := range rules.Clone() {
|
||||
out[k] = v
|
||||
}
|
||||
maps.Copy(out, rules.Clone())
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -177,19 +176,19 @@ type LexerState struct {
|
||||
// Named Group matches.
|
||||
NamedGroups map[string]string
|
||||
// Custum context for mutators.
|
||||
MutatorContext map[interface{}]interface{}
|
||||
MutatorContext map[any]any
|
||||
iteratorStack []Iterator
|
||||
options *TokeniseOptions
|
||||
newlineAdded bool
|
||||
}
|
||||
|
||||
// Set mutator context.
|
||||
func (l *LexerState) Set(key interface{}, value interface{}) {
|
||||
func (l *LexerState) Set(key any, value any) {
|
||||
l.MutatorContext[key] = value
|
||||
}
|
||||
|
||||
// Get mutator context.
|
||||
func (l *LexerState) Get(key interface{}) interface{} {
|
||||
func (l *LexerState) Get(key any) any {
|
||||
return l.MutatorContext[key]
|
||||
}
|
||||
|
||||
@@ -369,7 +368,7 @@ func (r *RegexLexer) maybeCompile() (err error) {
|
||||
pattern = "(?" + rule.flags + ")" + pattern
|
||||
}
|
||||
pattern = `\G` + pattern
|
||||
rule.Regexp, err = regexp2.Compile(pattern, 0)
|
||||
rule.Regexp, err = regexp2.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to compile rule %s.%d: %s", state, i, err)
|
||||
}
|
||||
@@ -484,7 +483,7 @@ func (r *RegexLexer) Tokenise(options *TokeniseOptions, text string) (Iterator,
|
||||
Text: []rune(text),
|
||||
Stack: []string{options.State},
|
||||
Rules: r.rules,
|
||||
MutatorContext: map[interface{}]interface{}{},
|
||||
MutatorContext: map[any]any{},
|
||||
}
|
||||
return state.Iterator, nil
|
||||
}
|
||||
@@ -501,7 +500,7 @@ func (r *RegexLexer) MustRules() Rules {
|
||||
func matchRules(text []rune, pos int, rules []*CompiledRule) (int, *CompiledRule, []string, map[string]string) {
|
||||
for i, rule := range rules {
|
||||
match, err := rule.Regexp.FindRunesMatchStartingAt(text, pos)
|
||||
if match != nil && err == nil && match.Index == pos {
|
||||
if match != nil && err == nil && match.RuneIndex == pos {
|
||||
groups := []string{}
|
||||
namedGroups := make(map[string]string)
|
||||
for _, g := range match.Groups() {
|
||||
|
||||
+1
@@ -9,6 +9,7 @@
|
||||
"schedule:earlyMondays", // Run once a week.
|
||||
'helpers:pinGitHubActionDigests',
|
||||
],
|
||||
"postUpdateOptions": ["gomodTidy"],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackageNames": ["golangci-lint"],
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
)
|
||||
|
||||
// Serialisation of Chroma rules to XML. The format is:
|
||||
@@ -440,14 +440,14 @@ func (t TokenType) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
}
|
||||
|
||||
// This hijinks is a bit unfortunate but without it we can't deserialise into TokenType.
|
||||
func newFromTemplate(template interface{}) (value func() interface{}, target interface{}) {
|
||||
func newFromTemplate(template any) (value func() any, target any) {
|
||||
t := reflect.TypeOf(template)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
if t.Kind() == reflect.Pointer {
|
||||
v := reflect.New(t.Elem())
|
||||
return v.Interface, v.Interface()
|
||||
}
|
||||
v := reflect.New(t)
|
||||
return func() interface{} { return v.Elem().Interface() }, v.Interface()
|
||||
return func() any { return v.Elem().Interface() }, v.Interface()
|
||||
}
|
||||
|
||||
func (b *Emitters) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
|
||||
+72
-23
@@ -4,7 +4,8 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,24 @@ const (
|
||||
No
|
||||
)
|
||||
|
||||
// Mode indicates whether a style is intended for a light or dark background.
|
||||
type Mode uint8
|
||||
|
||||
// Mode values.
|
||||
const (
|
||||
Light Mode = iota
|
||||
Dark
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case Dark:
|
||||
return "dark"
|
||||
default:
|
||||
return "light"
|
||||
}
|
||||
}
|
||||
|
||||
func (t Trilean) String() string {
|
||||
switch t {
|
||||
case Yes:
|
||||
@@ -31,12 +50,14 @@ func (t Trilean) String() string {
|
||||
|
||||
// Prefix returns s with "no" as a prefix if Trilean is no.
|
||||
func (t Trilean) Prefix(s string) string {
|
||||
if t == Yes {
|
||||
switch t {
|
||||
case Yes:
|
||||
return s
|
||||
} else if t == No {
|
||||
case No:
|
||||
return "no" + s
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A StyleEntry in the Style map.
|
||||
@@ -111,11 +132,10 @@ func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
|
||||
// Ancestors should be provided from oldest to newest.
|
||||
func (s StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry {
|
||||
out := s
|
||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||
for _, ancestor := range slices.Backward(ancestors) {
|
||||
if out.NoInherit {
|
||||
return out
|
||||
}
|
||||
ancestor := ancestors[i]
|
||||
if !out.Colour.IsSet() {
|
||||
out.Colour = ancestor.Colour
|
||||
}
|
||||
@@ -147,19 +167,24 @@ func (s StyleEntry) IsZero() bool {
|
||||
//
|
||||
// Once built, a Style is immutable.
|
||||
type StyleBuilder struct {
|
||||
entries map[TokenType]string
|
||||
name string
|
||||
parent *Style
|
||||
entries map[TokenType]string
|
||||
name string
|
||||
counterpart string
|
||||
parent *Style
|
||||
}
|
||||
|
||||
func NewStyleBuilder(name string) *StyleBuilder {
|
||||
return &StyleBuilder{name: name, entries: map[TokenType]string{}}
|
||||
}
|
||||
|
||||
// Counterpart sets the lowercase name of the opposite-mode style.
|
||||
func (s *StyleBuilder) Counterpart(name string) *StyleBuilder {
|
||||
s.counterpart = strings.ToLower(name)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) AddAll(entries StyleEntries) *StyleBuilder {
|
||||
for ttype, entry := range entries {
|
||||
s.entries[ttype] = entry
|
||||
}
|
||||
maps.Copy(s.entries, entries)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -205,10 +230,15 @@ func (s *StyleBuilder) Transform(transform func(StyleEntry) StyleEntry) *StyleBu
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) Build() (*Style, error) {
|
||||
counterpart := s.counterpart
|
||||
if counterpart == "" && s.parent != nil {
|
||||
counterpart = s.parent.Counterpart
|
||||
}
|
||||
style := &Style{
|
||||
Name: s.name,
|
||||
entries: map[TokenType]StyleEntry{},
|
||||
parent: s.parent,
|
||||
Name: s.name,
|
||||
Counterpart: counterpart,
|
||||
entries: map[TokenType]StyleEntry{},
|
||||
parent: s.parent,
|
||||
}
|
||||
for ttype, descriptor := range s.entries {
|
||||
entry, err := ParseStyleEntry(descriptor)
|
||||
@@ -257,9 +287,23 @@ func MustNewStyle(name string, entries StyleEntries) *Style {
|
||||
//
|
||||
// See http://pygments.org/docs/styles/ for details. Semantics are intended to be identical.
|
||||
type Style struct {
|
||||
Name string
|
||||
entries map[TokenType]StyleEntry
|
||||
parent *Style
|
||||
Name string
|
||||
// Counterpart is the lowercase name of the style intended as this style's
|
||||
// opposite-mode pair (eg. "github-dark" for "github"). Resolved via
|
||||
// styles.GetForMode. May be empty.
|
||||
Counterpart string
|
||||
entries map[TokenType]StyleEntry
|
||||
parent *Style
|
||||
}
|
||||
|
||||
// Mode returns Light or Dark based on the brightness of the Background entry's
|
||||
// background colour. Styles with an unset Background default to Light.
|
||||
func (s *Style) Mode() Mode {
|
||||
bg := s.get(Background).Background
|
||||
if bg.IsSet() && bg.Brightness() < 0.5 {
|
||||
return Dark
|
||||
}
|
||||
return Light
|
||||
}
|
||||
|
||||
func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
@@ -268,6 +312,9 @@ func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
}
|
||||
start.Name = xml.Name{Local: "style"}
|
||||
start.Attr = []xml.Attr{{Name: xml.Name{Local: "name"}, Value: s.Name}}
|
||||
if s.Counterpart != "" {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "counterpart"}, Value: s.Counterpart})
|
||||
}
|
||||
if err := e.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,7 +322,7 @@ func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
for ttype := range s.entries {
|
||||
sorted = append(sorted, ttype)
|
||||
}
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
|
||||
slices.Sort(sorted)
|
||||
for _, ttype := range sorted {
|
||||
entry := s.entries[ttype]
|
||||
el := xml.StartElement{Name: xml.Name{Local: "entry"}}
|
||||
@@ -295,9 +342,12 @@ func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
|
||||
func (s *Style) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
for _, attr := range start.Attr {
|
||||
if attr.Name.Local == "name" {
|
||||
switch attr.Name.Local {
|
||||
case "name":
|
||||
s.Name = attr.Value
|
||||
} else {
|
||||
case "counterpart":
|
||||
s.Counterpart = strings.ToLower(attr.Value)
|
||||
default:
|
||||
return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
|
||||
}
|
||||
}
|
||||
@@ -437,8 +487,7 @@ func MustParseStyleEntry(entry string) StyleEntry {
|
||||
// ParseStyleEntry parses a Pygments style entry.
|
||||
func ParseStyleEntry(entry string) (StyleEntry, error) { // nolint: gocyclo
|
||||
out := StyleEntry{}
|
||||
parts := strings.Fields(entry)
|
||||
for _, part := range parts {
|
||||
for part := range strings.FieldsSeq(entry) {
|
||||
switch {
|
||||
case part == "italic":
|
||||
out.Italic = Yes
|
||||
|
||||
+29
@@ -64,3 +64,32 @@ func Get(name string) *chroma.Style {
|
||||
}
|
||||
return Fallback
|
||||
}
|
||||
|
||||
// GetForMode returns the named style if it already matches mode, otherwise its
|
||||
// registered counterpart if one exists and matches mode. If neither matches,
|
||||
// the originally-requested style is returned (or Fallback if the name is
|
||||
// unknown), so callers always get something usable.
|
||||
func GetForMode(name string, mode chroma.Mode) *chroma.Style {
|
||||
style := Get(name)
|
||||
if style.Mode() == mode {
|
||||
return style
|
||||
}
|
||||
if style.Counterpart == "" {
|
||||
return style
|
||||
}
|
||||
counterpart, ok := Registry[style.Counterpart]
|
||||
if !ok || counterpart.Mode() != mode {
|
||||
return style
|
||||
}
|
||||
return counterpart
|
||||
}
|
||||
|
||||
// RegisterPair links two styles as light/dark counterparts of each other.
|
||||
//
|
||||
// Both styles are also registered if they are not already present.
|
||||
func RegisterPair(a, b *chroma.Style) {
|
||||
Register(a)
|
||||
Register(b)
|
||||
a.Counterpart = strings.ToLower(b.Name)
|
||||
b.Counterpart = strings.ToLower(a.Name)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="catppuccin-latte">
|
||||
<style name="catppuccin-latte" counterpart="catppuccin-mocha">
|
||||
<entry type="Background" style="bg:#eff1f5 #4c4f69"/>
|
||||
<entry type="CodeLine" style="#4c4f69"/>
|
||||
<entry type="Error" style="#d20f39"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="catppuccin-mocha">
|
||||
<style name="catppuccin-mocha" counterpart="catppuccin-latte">
|
||||
<entry type="Background" style="bg:#1e1e2e #cdd6f4"/>
|
||||
<entry type="CodeLine" style="#cdd6f4"/>
|
||||
<entry type="Error" style="#f38ba8"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="github-dark">
|
||||
<style name="github-dark" counterpart="github">
|
||||
<entry type="Error" style="#f85149"/>
|
||||
<entry type="LineHighlight" style="bg:#6e7681"/>
|
||||
<entry type="LineNumbers" style="#6e7681"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="github">
|
||||
<style name="github" counterpart="github-dark">
|
||||
<entry type="Error" style="#f6f8fa bg:#82071e"/>
|
||||
<entry type="Background" style="bg:#f7f7f7"/>
|
||||
<entry type="Keyword" style="#cf222e"/>
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
<style name="gruvbox-light">
|
||||
<style name="gruvbox-light" counterpart="gruvbox">
|
||||
<entry type="Background" style="noinherit #3c3836 bg:#fbf1c7"/>
|
||||
<entry type="Keyword" style="noinherit #af3a03"/>
|
||||
<entry type="KeywordType" style="noinherit #b57614"/>
|
||||
@@ -10,6 +10,7 @@
|
||||
<entry type="NameException" style="noinherit #fb4934"/>
|
||||
<entry type="NameFunction" style="#b57614"/>
|
||||
<entry type="NameLabel" style="noinherit #9d0006"/>
|
||||
<entry type="NameNamespace" style="noinherit #79740e"/>
|
||||
<entry type="NameTag" style="noinherit #9d0006"/>
|
||||
<entry type="NameVariable" style="noinherit #3c3836"/>
|
||||
<entry type="LiteralString" style="noinherit #79740e"/>
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
<style name="gruvbox">
|
||||
<style name="gruvbox" counterpart="gruvbox-light">
|
||||
<entry type="Background" style="noinherit #ebdbb2 bg:#282828"/>
|
||||
<entry type="Keyword" style="noinherit #fe8019"/>
|
||||
<entry type="KeywordType" style="noinherit #fabd2f"/>
|
||||
@@ -11,6 +11,7 @@
|
||||
<entry type="NameFunction" style="#fabd2f"/>
|
||||
<entry type="NameLabel" style="noinherit #fb4934"/>
|
||||
<entry type="NameTag" style="noinherit #fb4934"/>
|
||||
<entry type="NameNamespace" style="noinherit #b8bb26"/>
|
||||
<entry type="NameVariable" style="noinherit #ebdbb2"/>
|
||||
<entry type="LiteralString" style="noinherit #b8bb26"/>
|
||||
<entry type="LiteralStringSymbol" style="#83a598"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="kanagawa-lotus">
|
||||
<style name="kanagawa-lotus" counterpart="kanagawa-wave">
|
||||
<entry type="Background" style="bg:#f2ecbc #545464" />
|
||||
<entry type="CodeLine" style="#545464" />
|
||||
<entry type="Error" style="#e82424" />
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="kanagawa-wave">
|
||||
<style name="kanagawa-wave" counterpart="kanagawa-lotus">
|
||||
<entry type="Background" style="bg:#1f1f28 #dcd7ba" />
|
||||
<entry type="CodeLine" style="#dcd7ba" />
|
||||
<entry type="Error" style="#e82424" />
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="modus-operandi">
|
||||
<style name="modus-operandi" counterpart="modus-vivendi">
|
||||
<entry type="Background" style="#000000 bg:#ffffff"/>
|
||||
<entry type="Keyword" style="#5317ac"/>
|
||||
<entry type="KeywordConstant" style="#0000c0"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="modus-vivendi">
|
||||
<style name="modus-vivendi" counterpart="modus-operandi">
|
||||
<entry type="Background" style="#ffffff bg:#000000"/>
|
||||
<entry type="Keyword" style="#b6a0ff"/>
|
||||
<entry type="KeywordConstant" style="#00bcff"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="monokai">
|
||||
<style name="monokai" counterpart="monokailight">
|
||||
<entry type="Error" style="#960050 bg:#1e0010"/>
|
||||
<entry type="Background" style="bg:#272822"/>
|
||||
<entry type="Keyword" style="#66d9ef"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="monokailight">
|
||||
<style name="monokailight" counterpart="monokai">
|
||||
<entry type="Error" style="#960050 bg:#1e0010"/>
|
||||
<entry type="Background" style="bg:#fafafa"/>
|
||||
<entry type="Keyword" style="#00a8c8"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="paraiso-dark">
|
||||
<style name="paraiso-dark" counterpart="paraiso-light">
|
||||
<entry type="Error" style="#ef6155"/>
|
||||
<entry type="Background" style="bg:#2f1e2e"/>
|
||||
<entry type="Keyword" style="#815ba4"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="paraiso-light">
|
||||
<style name="paraiso-light" counterpart="paraiso-dark">
|
||||
<entry type="Error" style="#ef6155"/>
|
||||
<entry type="Background" style="bg:#e7e9db"/>
|
||||
<entry type="Keyword" style="#815ba4"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="rose-pine-dawn">
|
||||
<style name="rose-pine-dawn" counterpart="rose-pine">
|
||||
<entry type="Error" style="#b4637a"/>
|
||||
<entry type="Background" style="bg:#faf4ed"/>
|
||||
<entry type="Keyword" style="#286983"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="rose-pine">
|
||||
<style name="rose-pine" counterpart="rose-pine-dawn">
|
||||
<entry type="Error" style="#eb6f92"/>
|
||||
<entry type="Background" style="bg:#191724"/>
|
||||
<entry type="Keyword" style="#31748f"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="solarized-dark">
|
||||
<style name="solarized-dark" counterpart="solarized-light">
|
||||
<entry type="Other" style="#cb4b16"/>
|
||||
<entry type="Background" style="#93a1a1 bg:#002b36"/>
|
||||
<entry type="Keyword" style="#719e07"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="solarized-light">
|
||||
<style name="solarized-light" counterpart="solarized-dark">
|
||||
<entry type="Background" style="bg:#fdf6e3"/>
|
||||
<entry type="Keyword" style="#859900"/>
|
||||
<entry type="KeywordConstant" style="bold"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="tokyonight-day">
|
||||
<style name="tokyonight-day" counterpart="tokyonight-night">
|
||||
<entry type="Background" style="bg:#e1e2e7 #3760bf"/>
|
||||
<entry type="CodeLine" style="#3760bf"/>
|
||||
<entry type="Error" style="#c64343"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="tokyonight-night">
|
||||
<style name="tokyonight-night" counterpart="tokyonight-day">
|
||||
<entry type="Background" style="bg:#1a1b26 #c0caf5"/>
|
||||
<entry type="CodeLine" style="#c0caf5"/>
|
||||
<entry type="Error" style="#db4b4b"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="xcode-dark">
|
||||
<style name="xcode-dark" counterpart="xcode">
|
||||
<entry type="Error" style="#960050"/>
|
||||
<entry type="Background" style="#ffffff bg:#1f1f24"/>
|
||||
<entry type="Keyword" style="#fc5fa3"/>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<style name="xcode">
|
||||
<style name="xcode" counterpart="xcode-dark">
|
||||
<entry type="Error" style="#000000"/>
|
||||
<entry type="Background" style="bg:#ffffff"/>
|
||||
<entry type="Keyword" style="#a90d91"/>
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
{"name": "Alice"}
|
||||
{"name": "Bob"}
|
||||
+3
-3
@@ -48,7 +48,7 @@ func (s relativePseudoClassSelector) Match(n *html.Node) bool {
|
||||
}
|
||||
|
||||
// hasChildMatch returns whether n has any child that matches a.
|
||||
func hasChildMatch(n *html.Node, a Matcher) bool {
|
||||
func hasChildMatch(n *html.Node, a SelectorGroup) bool {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if a.Match(c) {
|
||||
return true
|
||||
@@ -60,7 +60,7 @@ func hasChildMatch(n *html.Node, a Matcher) bool {
|
||||
// hasDescendantMatch performs a depth-first search of n's descendants,
|
||||
// testing whether any of them match a. It returns true as soon as a match is
|
||||
// found, or false if no match is found.
|
||||
func hasDescendantMatch(n *html.Node, a Matcher) bool {
|
||||
func hasDescendantMatch(n *html.Node, a SelectorGroup) bool {
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
if a.Match(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
|
||||
return true
|
||||
@@ -327,7 +327,7 @@ func (s emptyElementPseudoClassSelector) Match(n *html.Node) bool {
|
||||
case html.ElementNode:
|
||||
return false
|
||||
case html.TextNode:
|
||||
if strings.TrimSpace(nodeText(c)) == "" {
|
||||
if strings.TrimSpace(c.Data) == "" {
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
language: go
|
||||
arch:
|
||||
- AMD64
|
||||
- ppc64le
|
||||
go:
|
||||
- 1.9
|
||||
- tip
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
# regexp2 - full featured regular expressions for Go
|
||||
Regexp2 is a feature-rich RegExp engine for Go. It doesn't have constant time guarantees like the built-in `regexp` package, but it allows backtracking and is compatible with Perl5 and .NET. You'll likely be better off with the RE2 engine from the `regexp` package and should only use this if you need to write very complex patterns or require compatibility with .NET.
|
||||
|
||||
## Basis of the engine
|
||||
The engine is ported from the .NET framework's System.Text.RegularExpressions.Regex engine. That engine was open sourced in 2015 under the MIT license. There are some fundamental differences between .NET strings and Go strings that required a bit of borrowing from the Go framework regex engine as well. I cleaned up a couple of the dirtier bits during the port (regexcharclass.cs was terrible), but the parse tree, code emmitted, and therefore patterns matched should be identical.
|
||||
|
||||
## New Code Generation
|
||||
For extra performance use `regexp2` with [`regexp2cg`](https://github.com/dlclark/regexp2cg). It is a code generation utility for `regexp2` and you can likely improve your regexp runtime performance by 3-10x in hot code paths. As always you should benchmark your specifics to confirm the results. Give it a try!
|
||||
|
||||
## Installing
|
||||
This is a go-gettable library, so install is easy:
|
||||
|
||||
go get github.com/dlclark/regexp2
|
||||
|
||||
To use the new Code Generation (while it's in beta) you'll need to use the `code_gen` branch:
|
||||
|
||||
go get github.com/dlclark/regexp2@code_gen
|
||||
|
||||
## Usage
|
||||
Usage is similar to the Go `regexp` package. Just like in `regexp`, you start by converting a regex into a state machine via the `Compile` or `MustCompile` methods. They ultimately do the same thing, but `MustCompile` will panic if the regex is invalid. You can then use the provided `Regexp` struct to find matches repeatedly. A `Regexp` struct is safe to use across goroutines.
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your pattern`, 0)
|
||||
if isMatch, _ := re.MatchString(`Something to match`); isMatch {
|
||||
//do something
|
||||
}
|
||||
```
|
||||
|
||||
The only error that the `*Match*` methods *should* return is a Timeout if you set the `re.MatchTimeout` field. Any other error is a bug in the `regexp2` package. If you need more details about capture groups in a match then use the `FindStringMatch` method, like so:
|
||||
|
||||
```go
|
||||
if m, _ := re.FindStringMatch(`Something to match`); m != nil {
|
||||
// the whole match is always group 0
|
||||
fmt.Printf("Group 0: %v\n", m.String())
|
||||
|
||||
// you can get all the groups too
|
||||
gps := m.Groups()
|
||||
|
||||
// a group can be captured multiple times, so each cap is separately addressable
|
||||
fmt.Printf("Group 1, first capture", gps[1].Captures[0].String())
|
||||
fmt.Printf("Group 1, second capture", gps[1].Captures[1].String())
|
||||
}
|
||||
```
|
||||
|
||||
Group 0 is embedded in the Match. Group 0 is an automatically-assigned group that encompasses the whole pattern. This means that `m.String()` is the same as `m.Group.String()` and `m.Groups()[0].String()`
|
||||
|
||||
The __last__ capture is embedded in each group, so `g.String()` will return the same thing as `g.Capture.String()` and `g.Captures[len(g.Captures)-1].String()`.
|
||||
|
||||
If you want to find multiple matches from a single input string you should use the `FindNextMatch` method. For example, to implement a function similar to `regexp.FindAllString`:
|
||||
|
||||
```go
|
||||
func regexp2FindAllString(re *regexp2.Regexp, s string) []string {
|
||||
var matches []string
|
||||
m, _ := re.FindStringMatch(s)
|
||||
for m != nil {
|
||||
matches = append(matches, m.String())
|
||||
m, _ = re.FindNextMatch(m)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
```
|
||||
|
||||
`FindNextMatch` is optmized so that it re-uses the underlying string/rune slice.
|
||||
|
||||
The internals of `regexp2` always operate on `[]rune` so `Index` and `Length` data in a `Match` always reference a position in `rune`s rather than `byte`s (even if the input was given as a string). This is a dramatic difference between `regexp` and `regexp2`. It's advisable to use the provided `String()` methods to avoid having to work with indices.
|
||||
|
||||
## Compare `regexp` and `regexp2`
|
||||
| Category | regexp | regexp2 |
|
||||
| --- | --- | --- |
|
||||
| Catastrophic backtracking possible | no, constant execution time guarantees | yes, if your pattern is at risk you can use the `re.MatchTimeout` field |
|
||||
| Python-style capture groups `(?P<name>re)` | yes | no (yes in RE2 compat mode) |
|
||||
| .NET-style capture groups `(?<name>re)` or `(?'name're)` | yes | yes |
|
||||
| comments `(?#comment)` | no | yes |
|
||||
| branch numbering reset `(?\|a\|b)` | no | no |
|
||||
| possessive match `(?>re)` | no | yes |
|
||||
| positive lookahead `(?=re)` | no | yes |
|
||||
| negative lookahead `(?!re)` | no | yes |
|
||||
| positive lookbehind `(?<=re)` | no | yes |
|
||||
| negative lookbehind `(?<!re)` | no | yes |
|
||||
| back reference `\1` | no | yes |
|
||||
| named back reference `\k'name'` | no | yes |
|
||||
| named ascii character class `[[:foo:]]`| yes | no (yes in RE2 compat mode) |
|
||||
| conditionals `(?(expr)yes\|no)` | no | yes |
|
||||
|
||||
## RE2 compatibility mode
|
||||
The default behavior of `regexp2` is to match the .NET regexp engine, however the `RE2` option is provided to change the parsing to increase compatibility with RE2. Using the `RE2` option when compiling a regexp will not take away any features, but will change the following behaviors:
|
||||
* add support for named ascii character classes (e.g. `[[:foo:]]`)
|
||||
* add support for python-style capture groups (e.g. `(P<name>re)`)
|
||||
* change singleline behavior for `$` to only match end of string (like RE2) (see [#24](https://github.com/dlclark/regexp2/issues/24))
|
||||
* change the character classes `\d` `\s` and `\w` to match the same characters as RE2. NOTE: if you also use the `ECMAScript` option then this will change the `\s` character class to match ECMAScript instead of RE2. ECMAScript allows more whitespace characters in `\s` than RE2 (but still fewer than the the default behavior).
|
||||
* allow character escape sequences to have defaults. For example, by default `\_` isn't a known character escape and will fail to compile, but in RE2 mode it will match the literal character `_`
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your RE2-compatible pattern`, regexp2.RE2)
|
||||
if isMatch, _ := re.MatchString(`Something to match`); isMatch {
|
||||
//do something
|
||||
}
|
||||
```
|
||||
|
||||
This feature is a work in progress and I'm open to ideas for more things to put here (maybe more relaxed character escaping rules?).
|
||||
|
||||
## Catastrophic Backtracking and Timeouts
|
||||
|
||||
`regexp2` supports features that can lead to catastrophic backtracking.
|
||||
`Regexp.MatchTimeout` can be set to to limit the impact of such behavior; the
|
||||
match will fail with an error after approximately MatchTimeout. No timeout
|
||||
checks are done by default.
|
||||
|
||||
Timeout checking is not free. The current timeout checking implementation starts
|
||||
a background worker that updates a clock value approximately once every 100
|
||||
milliseconds. The matching code compares this value against the precomputed
|
||||
deadline for the match. The performance impact is as follows.
|
||||
|
||||
1. A match with a timeout runs almost as fast as a match without a timeout.
|
||||
2. If any live matches have a timeout, there will be a background CPU load
|
||||
(`~0.15%` currently on a modern machine). This load will remain constant
|
||||
regardless of the number of matches done including matches done in parallel.
|
||||
3. If no live matches are using a timeout, the background load will remain
|
||||
until the longest deadline (match timeout + the time when the match started)
|
||||
is reached. E.g., if you set a timeout of one minute the load will persist
|
||||
for approximately a minute even if the match finishes quickly.
|
||||
|
||||
See [PR #58](https://github.com/dlclark/regexp2/pull/58) for more details and
|
||||
alternatives considered.
|
||||
|
||||
## Goroutine leak error
|
||||
If you're using a library during unit tests (e.g. https://github.com/uber-go/goleak) that validates all goroutines are exited then you'll likely get an error if you or any of your dependencies use regex's with a MatchTimeout.
|
||||
To remedy the problem you'll need to tell the unit test to wait until the backgroup timeout goroutine is exited.
|
||||
|
||||
```go
|
||||
func TestSomething(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
defer regexp2.StopTimeoutClock()
|
||||
|
||||
// ... test
|
||||
}
|
||||
|
||||
//or
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// setup
|
||||
// ...
|
||||
|
||||
// run
|
||||
m.Run()
|
||||
|
||||
//tear down
|
||||
regexp2.StopTimeoutClock()
|
||||
goleak.VerifyNone(t)
|
||||
}
|
||||
```
|
||||
|
||||
This will add ~100ms runtime to each test (or TestMain). If that's too much time you can set the clock cycle rate of the timeout goroutine in an init function in a test file. `regexp2.SetTimeoutCheckPeriod` isn't threadsafe so it must be setup before starting any regex's with Timeouts.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
//speed up testing by making the timeout clock 1ms
|
||||
regexp2.SetTimeoutCheckPeriod(time.Millisecond)
|
||||
}
|
||||
```
|
||||
|
||||
## ECMAScript compatibility mode
|
||||
In this mode the engine provides compatibility with the [regex engine](https://tc39.es/ecma262/multipage/text-processing.html#sec-regexp-regular-expression-objects) described in the ECMAScript specification.
|
||||
|
||||
Additionally a Unicode mode is provided which allows parsing of `\u{CodePoint}` syntax that is only when both are provided.
|
||||
|
||||
## Library features that I'm still working on
|
||||
- Regex split
|
||||
|
||||
## Potential bugs
|
||||
I've run a battery of tests against regexp2 from various sources and found the debug output matches the .NET engine, but .NET and Go handle strings very differently. I've attempted to handle these differences, but most of my testing deals with basic ASCII with a little bit of multi-byte Unicode. There's a chance that there are bugs in the string handling related to character sets with supplementary Unicode chars. Right-to-Left support is coded, but not well tested either.
|
||||
|
||||
## Find a bug?
|
||||
I'm open to new issues and pull requests with tests if you find something odd!
|
||||
-395
@@ -1,395 +0,0 @@
|
||||
/*
|
||||
Package regexp2 is a regexp package that has an interface similar to Go's framework regexp engine but uses a
|
||||
more feature full regex engine behind the scenes.
|
||||
|
||||
It doesn't have constant time guarantees, but it allows backtracking and is compatible with Perl5 and .NET.
|
||||
You'll likely be better off with the RE2 engine from the regexp package and should only use this if you
|
||||
need to write very complex patterns or require compatibility with .NET.
|
||||
*/
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dlclark/regexp2/syntax"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultMatchTimeout used when running regexp matches -- "forever"
|
||||
DefaultMatchTimeout = time.Duration(math.MaxInt64)
|
||||
// DefaultUnmarshalOptions used when unmarshaling a regex from text
|
||||
DefaultUnmarshalOptions = None
|
||||
)
|
||||
|
||||
// Regexp is the representation of a compiled regular expression.
|
||||
// A Regexp is safe for concurrent use by multiple goroutines.
|
||||
type Regexp struct {
|
||||
// A match will time out if it takes (approximately) more than
|
||||
// MatchTimeout. This is a safety check in case the match
|
||||
// encounters catastrophic backtracking. The default value
|
||||
// (DefaultMatchTimeout) causes all time out checking to be
|
||||
// suppressed.
|
||||
MatchTimeout time.Duration
|
||||
|
||||
// read-only after Compile
|
||||
pattern string // as passed to Compile
|
||||
options RegexOptions // options
|
||||
|
||||
caps map[int]int // capnum->index
|
||||
capnames map[string]int //capture group name -> index
|
||||
capslist []string //sorted list of capture group names
|
||||
capsize int // size of the capture array
|
||||
|
||||
code *syntax.Code // compiled program
|
||||
|
||||
// cache of machines for running regexp
|
||||
muRun *sync.Mutex
|
||||
runner []*runner
|
||||
}
|
||||
|
||||
// Compile parses a regular expression and returns, if successful,
|
||||
// a Regexp object that can be used to match against text.
|
||||
func Compile(expr string, opt RegexOptions) (*Regexp, error) {
|
||||
// parse it
|
||||
tree, err := syntax.Parse(expr, syntax.RegexOptions(opt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// translate it to code
|
||||
code, err := syntax.Write(tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return it
|
||||
return &Regexp{
|
||||
pattern: expr,
|
||||
options: opt,
|
||||
caps: code.Caps,
|
||||
capnames: tree.Capnames,
|
||||
capslist: tree.Caplist,
|
||||
capsize: code.Capsize,
|
||||
code: code,
|
||||
MatchTimeout: DefaultMatchTimeout,
|
||||
muRun: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MustCompile is like Compile but panics if the expression cannot be parsed.
|
||||
// It simplifies safe initialization of global variables holding compiled regular
|
||||
// expressions.
|
||||
func MustCompile(str string, opt RegexOptions) *Regexp {
|
||||
regexp, error := Compile(str, opt)
|
||||
if error != nil {
|
||||
panic(`regexp2: Compile(` + quote(str) + `): ` + error.Error())
|
||||
}
|
||||
return regexp
|
||||
}
|
||||
|
||||
// Escape adds backslashes to any special characters in the input string
|
||||
func Escape(input string) string {
|
||||
return syntax.Escape(input)
|
||||
}
|
||||
|
||||
// Unescape removes any backslashes from previously-escaped special characters in the input string
|
||||
func Unescape(input string) (string, error) {
|
||||
return syntax.Unescape(input)
|
||||
}
|
||||
|
||||
// SetTimeoutPeriod is a debug function that sets the frequency of the timeout goroutine's sleep cycle.
|
||||
// Defaults to 100ms. The only benefit of setting this lower is that the 1 background goroutine that manages
|
||||
// timeouts may exit slightly sooner after all the timeouts have expired. See Github issue #63
|
||||
func SetTimeoutCheckPeriod(d time.Duration) {
|
||||
clockPeriod = d
|
||||
}
|
||||
|
||||
// StopTimeoutClock should only be used in unit tests to prevent the timeout clock goroutine
|
||||
// from appearing like a leaking goroutine
|
||||
func StopTimeoutClock() {
|
||||
stopClock()
|
||||
}
|
||||
|
||||
// String returns the source text used to compile the regular expression.
|
||||
func (re *Regexp) String() string {
|
||||
return re.pattern
|
||||
}
|
||||
|
||||
func quote(s string) string {
|
||||
if strconv.CanBackquote(s) {
|
||||
return "`" + s + "`"
|
||||
}
|
||||
return strconv.Quote(s)
|
||||
}
|
||||
|
||||
// RegexOptions impact the runtime and parsing behavior
|
||||
// for each specific regex. They are setable in code as well
|
||||
// as in the regex pattern itself.
|
||||
type RegexOptions int32
|
||||
|
||||
const (
|
||||
None RegexOptions = 0x0
|
||||
IgnoreCase = 0x0001 // "i"
|
||||
Multiline = 0x0002 // "m"
|
||||
ExplicitCapture = 0x0004 // "n"
|
||||
Compiled = 0x0008 // "c"
|
||||
Singleline = 0x0010 // "s"
|
||||
IgnorePatternWhitespace = 0x0020 // "x"
|
||||
RightToLeft = 0x0040 // "r"
|
||||
Debug = 0x0080 // "d"
|
||||
ECMAScript = 0x0100 // "e"
|
||||
RE2 = 0x0200 // RE2 (regexp package) compatibility mode
|
||||
Unicode = 0x0400 // "u"
|
||||
)
|
||||
|
||||
func (re *Regexp) RightToLeft() bool {
|
||||
return re.options&RightToLeft != 0
|
||||
}
|
||||
|
||||
func (re *Regexp) Debug() bool {
|
||||
return re.options&Debug != 0
|
||||
}
|
||||
|
||||
// Replace searches the input string and replaces each match found with the replacement text.
|
||||
// Count will limit the number of matches attempted and startAt will allow
|
||||
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
||||
// Set startAt and count to -1 to go through the whole string
|
||||
func (re *Regexp) Replace(input, replacement string, startAt, count int) (string, error) {
|
||||
data, err := syntax.NewReplacerData(replacement, re.caps, re.capsize, re.capnames, syntax.RegexOptions(re.options))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
//TODO: cache ReplacerData
|
||||
|
||||
return replace(re, data, nil, input, startAt, count)
|
||||
}
|
||||
|
||||
// ReplaceFunc searches the input string and replaces each match found using the string from the evaluator
|
||||
// Count will limit the number of matches attempted and startAt will allow
|
||||
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
||||
// Set startAt and count to -1 to go through the whole string.
|
||||
func (re *Regexp) ReplaceFunc(input string, evaluator MatchEvaluator, startAt, count int) (string, error) {
|
||||
return replace(re, nil, evaluator, input, startAt, count)
|
||||
}
|
||||
|
||||
// FindStringMatch searches the input string for a Regexp match
|
||||
func (re *Regexp) FindStringMatch(s string) (*Match, error) {
|
||||
// convert string to runes
|
||||
return re.run(false, -1, getRunes(s))
|
||||
}
|
||||
|
||||
// FindRunesMatch searches the input rune slice for a Regexp match
|
||||
func (re *Regexp) FindRunesMatch(r []rune) (*Match, error) {
|
||||
return re.run(false, -1, r)
|
||||
}
|
||||
|
||||
// FindStringMatchStartingAt searches the input string for a Regexp match starting at the startAt index
|
||||
func (re *Regexp) FindStringMatchStartingAt(s string, startAt int) (*Match, error) {
|
||||
if startAt > len(s) {
|
||||
return nil, errors.New("startAt must be less than the length of the input string")
|
||||
}
|
||||
r, startAt := re.getRunesAndStart(s, startAt)
|
||||
if startAt == -1 {
|
||||
// we didn't find our start index in the string -- that's a problem
|
||||
return nil, errors.New("startAt must align to the start of a valid rune in the input string")
|
||||
}
|
||||
|
||||
return re.run(false, startAt, r)
|
||||
}
|
||||
|
||||
// FindRunesMatchStartingAt searches the input rune slice for a Regexp match starting at the startAt index
|
||||
func (re *Regexp) FindRunesMatchStartingAt(r []rune, startAt int) (*Match, error) {
|
||||
return re.run(false, startAt, r)
|
||||
}
|
||||
|
||||
// FindNextMatch returns the next match in the same input string as the match parameter.
|
||||
// Will return nil if there is no next match or if given a nil match.
|
||||
func (re *Regexp) FindNextMatch(m *Match) (*Match, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If previous match was empty, advance by one before matching to prevent
|
||||
// infinite loop
|
||||
startAt := m.textpos
|
||||
if m.Length == 0 {
|
||||
if m.textpos == len(m.text) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if re.RightToLeft() {
|
||||
startAt--
|
||||
} else {
|
||||
startAt++
|
||||
}
|
||||
}
|
||||
return re.run(false, startAt, m.text)
|
||||
}
|
||||
|
||||
// MatchString return true if the string matches the regex
|
||||
// error will be set if a timeout occurs
|
||||
func (re *Regexp) MatchString(s string) (bool, error) {
|
||||
m, err := re.run(true, -1, getRunes(s))
|
||||
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)
|
||||
}
|
||||
|
||||
// MatchRunes return true if the runes matches the regex
|
||||
// error will be set if a timeout occurs
|
||||
func (re *Regexp) MatchRunes(r []rune) (bool, error) {
|
||||
m, err := re.run(true, -1, r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return m != nil, nil
|
||||
}
|
||||
|
||||
// GetGroupNames Returns the set of strings used to name capturing groups in the expression.
|
||||
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
|
||||
}
|
||||
|
||||
// GetGroupNumbers returns the integer group numbers corresponding to a group name.
|
||||
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
|
||||
}
|
||||
|
||||
// GroupNameFromNumber retrieves a group name that corresponds to a group number.
|
||||
// It will return "" for and unknown group number. Unnamed groups automatically
|
||||
// receive a name that is the decimal string equivalent of its number.
|
||||
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 ""
|
||||
}
|
||||
|
||||
// GroupNumberFromName returns a group number that corresponds to a group name.
|
||||
// Returns -1 if the name is not a recognized group name. Numbered groups
|
||||
// automatically get a group name that is the decimal string equivalent of its number.
|
||||
func (re *Regexp) GroupNumberFromName(name string) int {
|
||||
// look up name if we have a hashtable of names
|
||||
if re.capnames != nil {
|
||||
if k, ok := re.capnames[name]; ok {
|
||||
return k
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// convert to an int if it looks like a number
|
||||
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')
|
||||
}
|
||||
|
||||
// return int if it's in range
|
||||
if result >= 0 && result < re.capsize {
|
||||
return result
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// MarshalText implements [encoding.TextMarshaler]. The output
|
||||
// matches that of calling the [Regexp.String] method.
|
||||
func (re *Regexp) MarshalText() ([]byte, error) {
|
||||
return []byte(re.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements [encoding.TextUnmarshaler] by calling
|
||||
// [Compile] on the encoded value.
|
||||
func (re *Regexp) UnmarshalText(text []byte) error {
|
||||
newRE, err := Compile(string(text), DefaultUnmarshalOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*re = *newRE
|
||||
return nil
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"github.com/dlclark/regexp2/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.
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
m, err := regex.FindStringMatchStartingAt(input, startAt)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m == nil {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
text := m.text
|
||||
|
||||
if !regex.RightToLeft() {
|
||||
prevat := 0
|
||||
for m != nil {
|
||||
if m.Index != prevat {
|
||||
buf.WriteString(string(text[prevat:m.Index]))
|
||||
}
|
||||
prevat = m.Index + m.Length
|
||||
if evaluator == nil {
|
||||
replacementImpl(data, buf, m)
|
||||
} else {
|
||||
buf.WriteString(evaluator(*m))
|
||||
}
|
||||
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
m, err = regex.FindNextMatch(m)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if prevat < len(text) {
|
||||
buf.WriteString(string(text[prevat:]))
|
||||
}
|
||||
} else {
|
||||
prevat := len(text)
|
||||
var al []string
|
||||
|
||||
for m != nil {
|
||||
if m.Index+m.Length != prevat {
|
||||
al = append(al, string(text[m.Index+m.Length:prevat]))
|
||||
}
|
||||
prevat = m.Index
|
||||
if evaluator == nil {
|
||||
replacementImplRTL(data, &al, m)
|
||||
} else {
|
||||
al = append(al, evaluator(*m))
|
||||
}
|
||||
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
m, err = regex.FindNextMatch(m)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
if prevat > 0 {
|
||||
buf.WriteString(string(text[: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:
|
||||
for i := 0; i < m.Index; i++ {
|
||||
buf.WriteRune(m.text[i])
|
||||
}
|
||||
case replaceRightPortion:
|
||||
for i := m.Index + m.Length; i < len(m.text); i++ {
|
||||
buf.WriteRune(m.text[i])
|
||||
}
|
||||
case replaceLastGroup:
|
||||
m.groupValueAppendToBuf(m.GroupCount()-1, buf)
|
||||
case replaceWholeString:
|
||||
for i := 0; i < len(m.text); i++ {
|
||||
buf.WriteRune(m.text[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:
|
||||
for i := 0; i < m.Index; i++ {
|
||||
buf.WriteRune(m.text[i])
|
||||
}
|
||||
case replaceRightPortion:
|
||||
for i := m.Index + m.Length; i < len(m.text); i++ {
|
||||
buf.WriteRune(m.text[i])
|
||||
}
|
||||
case replaceLastGroup:
|
||||
m.groupValueAppendToBuf(m.GroupCount()-1, buf)
|
||||
case replaceWholeString:
|
||||
for i := 0; i < len(m.text); i++ {
|
||||
buf.WriteRune(m.text[i])
|
||||
}
|
||||
}
|
||||
l = append(l, buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
*al = l
|
||||
}
|
||||
-1614
File diff suppressed because it is too large
Load Diff
-865
@@ -1,865 +0,0 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// CharSet combines start-end rune ranges and unicode categories representing a set of characters
|
||||
type CharSet struct {
|
||||
ranges []singleRange
|
||||
categories []category
|
||||
sub *CharSet //optional subtractor
|
||||
negate bool
|
||||
anything bool
|
||||
}
|
||||
|
||||
type category struct {
|
||||
negate bool
|
||||
cat string
|
||||
}
|
||||
|
||||
type singleRange struct {
|
||||
first rune
|
||||
last rune
|
||||
}
|
||||
|
||||
const (
|
||||
spaceCategoryText = " "
|
||||
wordCategoryText = "W"
|
||||
)
|
||||
|
||||
var (
|
||||
ecmaSpace = []rune{0x0009, 0x000e, 0x0020, 0x0021, 0x00a0, 0x00a1, 0x1680, 0x1681, 0x2000, 0x200b, 0x2028, 0x202a, 0x202f, 0x2030, 0x205f, 0x2060, 0x3000, 0x3001, 0xfeff, 0xff00}
|
||||
ecmaWord = []rune{0x0030, 0x003a, 0x0041, 0x005b, 0x005f, 0x0060, 0x0061, 0x007b}
|
||||
ecmaDigit = []rune{0x0030, 0x003a}
|
||||
|
||||
re2Space = []rune{0x0009, 0x000b, 0x000c, 0x000e, 0x0020, 0x0021}
|
||||
)
|
||||
|
||||
var (
|
||||
AnyClass = getCharSetFromOldString([]rune{0}, false)
|
||||
ECMAAnyClass = getCharSetFromOldString([]rune{0, 0x000a, 0x000b, 0x000d, 0x000e}, false)
|
||||
NoneClass = getCharSetFromOldString(nil, false)
|
||||
ECMAWordClass = getCharSetFromOldString(ecmaWord, false)
|
||||
NotECMAWordClass = getCharSetFromOldString(ecmaWord, true)
|
||||
ECMASpaceClass = getCharSetFromOldString(ecmaSpace, false)
|
||||
NotECMASpaceClass = getCharSetFromOldString(ecmaSpace, true)
|
||||
ECMADigitClass = getCharSetFromOldString(ecmaDigit, false)
|
||||
NotECMADigitClass = getCharSetFromOldString(ecmaDigit, true)
|
||||
|
||||
WordClass = getCharSetFromCategoryString(false, false, wordCategoryText)
|
||||
NotWordClass = getCharSetFromCategoryString(true, false, wordCategoryText)
|
||||
SpaceClass = getCharSetFromCategoryString(false, false, spaceCategoryText)
|
||||
NotSpaceClass = getCharSetFromCategoryString(true, false, spaceCategoryText)
|
||||
DigitClass = getCharSetFromCategoryString(false, false, "Nd")
|
||||
NotDigitClass = getCharSetFromCategoryString(false, true, "Nd")
|
||||
|
||||
RE2SpaceClass = getCharSetFromOldString(re2Space, false)
|
||||
NotRE2SpaceClass = getCharSetFromOldString(re2Space, true)
|
||||
)
|
||||
|
||||
var unicodeCategories = func() map[string]*unicode.RangeTable {
|
||||
retVal := make(map[string]*unicode.RangeTable)
|
||||
for k, v := range unicode.Scripts {
|
||||
retVal[k] = v
|
||||
}
|
||||
for k, v := range unicode.Categories {
|
||||
retVal[k] = v
|
||||
}
|
||||
for k, v := range unicode.Properties {
|
||||
retVal[k] = v
|
||||
}
|
||||
return retVal
|
||||
}()
|
||||
|
||||
func getCharSetFromCategoryString(negateSet bool, negateCat bool, cats ...string) func() *CharSet {
|
||||
if negateCat && negateSet {
|
||||
panic("BUG! You should only negate the set OR the category in a constant setup, but not both")
|
||||
}
|
||||
|
||||
c := CharSet{negate: negateSet}
|
||||
|
||||
c.categories = make([]category, len(cats))
|
||||
for i, cat := range cats {
|
||||
c.categories[i] = category{cat: cat, negate: negateCat}
|
||||
}
|
||||
return func() *CharSet {
|
||||
//make a copy each time
|
||||
local := c
|
||||
//return that address
|
||||
return &local
|
||||
}
|
||||
}
|
||||
|
||||
func getCharSetFromOldString(setText []rune, negate bool) func() *CharSet {
|
||||
c := CharSet{}
|
||||
if len(setText) > 0 {
|
||||
fillFirst := false
|
||||
l := len(setText)
|
||||
if negate {
|
||||
if setText[0] == 0 {
|
||||
setText = setText[1:]
|
||||
} else {
|
||||
l++
|
||||
fillFirst = true
|
||||
}
|
||||
}
|
||||
|
||||
if l%2 == 0 {
|
||||
c.ranges = make([]singleRange, l/2)
|
||||
} else {
|
||||
c.ranges = make([]singleRange, l/2+1)
|
||||
}
|
||||
|
||||
first := true
|
||||
if fillFirst {
|
||||
c.ranges[0] = singleRange{first: 0}
|
||||
first = false
|
||||
}
|
||||
|
||||
i := 0
|
||||
for _, r := range setText {
|
||||
if first {
|
||||
// lower bound in a new range
|
||||
c.ranges[i] = singleRange{first: r}
|
||||
first = false
|
||||
} else {
|
||||
c.ranges[i].last = r - 1
|
||||
i++
|
||||
first = true
|
||||
}
|
||||
}
|
||||
if !first {
|
||||
c.ranges[i].last = utf8.MaxRune
|
||||
}
|
||||
}
|
||||
|
||||
return func() *CharSet {
|
||||
local := c
|
||||
return &local
|
||||
}
|
||||
}
|
||||
|
||||
// Copy makes a deep copy to prevent accidental mutation of a set
|
||||
func (c CharSet) Copy() CharSet {
|
||||
ret := CharSet{
|
||||
anything: c.anything,
|
||||
negate: c.negate,
|
||||
}
|
||||
|
||||
ret.ranges = append(ret.ranges, c.ranges...)
|
||||
ret.categories = append(ret.categories, c.categories...)
|
||||
|
||||
if c.sub != nil {
|
||||
sub := c.sub.Copy()
|
||||
ret.sub = &sub
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// gets a human-readable description for a set string
|
||||
func (c CharSet) String() string {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.WriteRune('[')
|
||||
|
||||
if c.IsNegated() {
|
||||
buf.WriteRune('^')
|
||||
}
|
||||
|
||||
for _, r := range c.ranges {
|
||||
|
||||
buf.WriteString(CharDescription(r.first))
|
||||
if r.first != r.last {
|
||||
if r.last-r.first != 1 {
|
||||
//groups that are 1 char apart skip the dash
|
||||
buf.WriteRune('-')
|
||||
}
|
||||
buf.WriteString(CharDescription(r.last))
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range c.categories {
|
||||
buf.WriteString(c.String())
|
||||
}
|
||||
|
||||
if c.sub != nil {
|
||||
buf.WriteRune('-')
|
||||
buf.WriteString(c.sub.String())
|
||||
}
|
||||
|
||||
buf.WriteRune(']')
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// mapHashFill converts a charset into a buffer for use in maps
|
||||
func (c CharSet) mapHashFill(buf *bytes.Buffer) {
|
||||
if c.negate {
|
||||
buf.WriteByte(0)
|
||||
} else {
|
||||
buf.WriteByte(1)
|
||||
}
|
||||
|
||||
binary.Write(buf, binary.LittleEndian, len(c.ranges))
|
||||
binary.Write(buf, binary.LittleEndian, len(c.categories))
|
||||
for _, r := range c.ranges {
|
||||
buf.WriteRune(r.first)
|
||||
buf.WriteRune(r.last)
|
||||
}
|
||||
for _, ct := range c.categories {
|
||||
buf.WriteString(ct.cat)
|
||||
if ct.negate {
|
||||
buf.WriteByte(1)
|
||||
} else {
|
||||
buf.WriteByte(0)
|
||||
}
|
||||
}
|
||||
|
||||
if c.sub != nil {
|
||||
c.sub.mapHashFill(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
val := false
|
||||
// in s && !s.subtracted
|
||||
|
||||
//check ranges
|
||||
for _, r := range c.ranges {
|
||||
if ch < r.first {
|
||||
continue
|
||||
}
|
||||
if ch <= r.last {
|
||||
val = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//check categories if we haven't already found a range
|
||||
if !val && len(c.categories) > 0 {
|
||||
for _, ct := range c.categories {
|
||||
// special categories...then unicode
|
||||
if ct.cat == spaceCategoryText {
|
||||
if unicode.IsSpace(ch) {
|
||||
// we found a space so we're done
|
||||
// negate means this is a "bad" thing
|
||||
val = !ct.negate
|
||||
break
|
||||
} else if ct.negate {
|
||||
val = true
|
||||
break
|
||||
}
|
||||
} else if ct.cat == wordCategoryText {
|
||||
if IsWordChar(ch) {
|
||||
val = !ct.negate
|
||||
break
|
||||
} else if ct.negate {
|
||||
val = true
|
||||
break
|
||||
}
|
||||
} else if unicode.Is(unicodeCategories[ct.cat], ch) {
|
||||
// if we're in this unicode category then we're done
|
||||
// if negate=true on this category then we "failed" our test
|
||||
// otherwise we're good that we found it
|
||||
val = !ct.negate
|
||||
break
|
||||
} else if ct.negate {
|
||||
val = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// negate the whole char set
|
||||
if c.negate {
|
||||
val = !val
|
||||
}
|
||||
|
||||
// get subtracted recurse
|
||||
if val && c.sub != nil {
|
||||
val = !c.sub.CharIn(ch)
|
||||
}
|
||||
|
||||
//log.Printf("Char '%v' in %v == %v", string(ch), c.String(), val)
|
||||
return val
|
||||
}
|
||||
|
||||
func (c category) String() string {
|
||||
switch c.cat {
|
||||
case spaceCategoryText:
|
||||
if c.negate {
|
||||
return "\\S"
|
||||
}
|
||||
return "\\s"
|
||||
case wordCategoryText:
|
||||
if c.negate {
|
||||
return "\\W"
|
||||
}
|
||||
return "\\w"
|
||||
}
|
||||
if _, ok := unicodeCategories[c.cat]; ok {
|
||||
|
||||
if c.negate {
|
||||
return "\\P{" + c.cat + "}"
|
||||
}
|
||||
return "\\p{" + c.cat + "}"
|
||||
}
|
||||
return "Unknown category: " + c.cat
|
||||
}
|
||||
|
||||
// CharDescription Produces a human-readable description for a single character.
|
||||
func CharDescription(ch rune) string {
|
||||
/*if ch == '\\' {
|
||||
return "\\\\"
|
||||
}
|
||||
|
||||
if ch > ' ' && ch <= '~' {
|
||||
return string(ch)
|
||||
} else if ch == '\n' {
|
||||
return "\\n"
|
||||
} else if ch == ' ' {
|
||||
return "\\ "
|
||||
}*/
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
escape(b, ch, false) //fmt.Sprintf("%U", ch)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// According to UTS#18 Unicode Regular Expressions (http://www.unicode.org/reports/tr18/)
|
||||
// RL 1.4 Simple Word Boundaries The class of <word_character> includes all Alphabetic
|
||||
// values from the Unicode character database, from UnicodeData.txt [UData], plus the U+200C
|
||||
// ZERO WIDTH NON-JOINER and U+200D ZERO WIDTH JOINER.
|
||||
func IsWordChar(r rune) bool {
|
||||
//"L", "Mn", "Nd", "Pc"
|
||||
return unicode.In(r,
|
||||
unicode.Categories["L"], unicode.Categories["Mn"],
|
||||
unicode.Categories["Nd"], unicode.Categories["Pc"]) || r == '\u200D' || r == '\u200C'
|
||||
//return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '_'
|
||||
}
|
||||
|
||||
func IsECMAWordChar(r rune) bool {
|
||||
return unicode.In(r,
|
||||
unicode.Categories["L"], unicode.Categories["Mn"],
|
||||
unicode.Categories["Nd"], unicode.Categories["Pc"])
|
||||
|
||||
//return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '_'
|
||||
}
|
||||
|
||||
// SingletonChar will return the char from the first range without validation.
|
||||
// It assumes you have checked for IsSingleton or IsSingletonInverse and will panic given bad input
|
||||
func (c CharSet) SingletonChar() rune {
|
||||
return c.ranges[0].first
|
||||
}
|
||||
|
||||
func (c CharSet) IsSingleton() bool {
|
||||
return !c.negate && //negated is multiple chars
|
||||
len(c.categories) == 0 && len(c.ranges) == 1 && // multiple ranges and unicode classes represent multiple chars
|
||||
c.sub == nil && // subtraction means we've got multiple chars
|
||||
c.ranges[0].first == c.ranges[0].last // first and last equal means we're just 1 char
|
||||
}
|
||||
|
||||
func (c CharSet) IsSingletonInverse() bool {
|
||||
return c.negate && //same as above, but requires negated
|
||||
len(c.categories) == 0 && len(c.ranges) == 1 && // multiple ranges and unicode classes represent multiple chars
|
||||
c.sub == nil && // subtraction means we've got multiple chars
|
||||
c.ranges[0].first == c.ranges[0].last // first and last equal means we're just 1 char
|
||||
}
|
||||
|
||||
func (c CharSet) IsMergeable() bool {
|
||||
return !c.IsNegated() && !c.HasSubtraction()
|
||||
}
|
||||
|
||||
func (c CharSet) IsNegated() bool {
|
||||
return c.negate
|
||||
}
|
||||
|
||||
func (c CharSet) HasSubtraction() bool {
|
||||
return c.sub != nil
|
||||
}
|
||||
|
||||
func (c CharSet) IsEmpty() bool {
|
||||
return len(c.ranges) == 0 && len(c.categories) == 0 && c.sub == nil
|
||||
}
|
||||
|
||||
func (c *CharSet) addDigit(ecma, negate bool, pattern string) {
|
||||
if ecma {
|
||||
if negate {
|
||||
c.addRanges(NotECMADigitClass().ranges)
|
||||
} else {
|
||||
c.addRanges(ECMADigitClass().ranges)
|
||||
}
|
||||
} else {
|
||||
c.addCategories(category{cat: "Nd", negate: negate})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CharSet) addChar(ch rune) {
|
||||
c.addRange(ch, ch)
|
||||
}
|
||||
|
||||
func (c *CharSet) addSpace(ecma, re2, negate bool) {
|
||||
if ecma {
|
||||
if negate {
|
||||
c.addRanges(NotECMASpaceClass().ranges)
|
||||
} else {
|
||||
c.addRanges(ECMASpaceClass().ranges)
|
||||
}
|
||||
} else if re2 {
|
||||
if negate {
|
||||
c.addRanges(NotRE2SpaceClass().ranges)
|
||||
} else {
|
||||
c.addRanges(RE2SpaceClass().ranges)
|
||||
}
|
||||
} else {
|
||||
c.addCategories(category{cat: spaceCategoryText, negate: negate})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CharSet) addWord(ecma, negate bool) {
|
||||
if ecma {
|
||||
if negate {
|
||||
c.addRanges(NotECMAWordClass().ranges)
|
||||
} else {
|
||||
c.addRanges(ECMAWordClass().ranges)
|
||||
}
|
||||
} else {
|
||||
c.addCategories(category{cat: wordCategoryText, negate: negate})
|
||||
}
|
||||
}
|
||||
|
||||
// Add set ranges and categories into ours -- no deduping or anything
|
||||
func (c *CharSet) addSet(set CharSet) {
|
||||
if c.anything {
|
||||
return
|
||||
}
|
||||
if set.anything {
|
||||
c.makeAnything()
|
||||
return
|
||||
}
|
||||
// just append here to prevent double-canon
|
||||
c.ranges = append(c.ranges, set.ranges...)
|
||||
c.addCategories(set.categories...)
|
||||
c.canonicalize()
|
||||
}
|
||||
|
||||
func (c *CharSet) makeAnything() {
|
||||
c.anything = true
|
||||
c.categories = []category{}
|
||||
c.ranges = AnyClass().ranges
|
||||
}
|
||||
|
||||
func (c *CharSet) addCategories(cats ...category) {
|
||||
// don't add dupes and remove positive+negative
|
||||
if c.anything {
|
||||
// if we've had a previous positive+negative group then
|
||||
// just return, we're as broad as we can get
|
||||
return
|
||||
}
|
||||
|
||||
for _, ct := range cats {
|
||||
found := false
|
||||
for _, ct2 := range c.categories {
|
||||
if ct.cat == ct2.cat {
|
||||
if ct.negate != ct2.negate {
|
||||
// oposite negations...this mean we just
|
||||
// take us as anything and move on
|
||||
c.makeAnything()
|
||||
return
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
c.categories = append(c.categories, ct)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merges new ranges to our own
|
||||
func (c *CharSet) addRanges(ranges []singleRange) {
|
||||
if c.anything {
|
||||
return
|
||||
}
|
||||
c.ranges = append(c.ranges, ranges...)
|
||||
c.canonicalize()
|
||||
}
|
||||
|
||||
// Merges everything but the new ranges into our own
|
||||
func (c *CharSet) addNegativeRanges(ranges []singleRange) {
|
||||
if c.anything {
|
||||
return
|
||||
}
|
||||
|
||||
var hi rune
|
||||
|
||||
// convert incoming ranges into opposites, assume they are in order
|
||||
for _, r := range ranges {
|
||||
if hi < r.first {
|
||||
c.ranges = append(c.ranges, singleRange{hi, r.first - 1})
|
||||
}
|
||||
hi = r.last + 1
|
||||
}
|
||||
|
||||
if hi < utf8.MaxRune {
|
||||
c.ranges = append(c.ranges, singleRange{hi, utf8.MaxRune})
|
||||
}
|
||||
|
||||
c.canonicalize()
|
||||
}
|
||||
|
||||
func isValidUnicodeCat(catName string) bool {
|
||||
_, ok := unicodeCategories[catName]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *CharSet) addCategory(categoryName string, negate, caseInsensitive bool, pattern string) {
|
||||
if !isValidUnicodeCat(categoryName) {
|
||||
// unknown unicode category, script, or property "blah"
|
||||
panic(fmt.Errorf("Unknown unicode category, script, or property '%v'", categoryName))
|
||||
|
||||
}
|
||||
|
||||
if caseInsensitive && (categoryName == "Ll" || categoryName == "Lu" || categoryName == "Lt") {
|
||||
// when RegexOptions.IgnoreCase is specified then {Ll} {Lu} and {Lt} cases should all match
|
||||
c.addCategories(
|
||||
category{cat: "Ll", negate: negate},
|
||||
category{cat: "Lu", negate: negate},
|
||||
category{cat: "Lt", negate: negate})
|
||||
}
|
||||
c.addCategories(category{cat: categoryName, negate: negate})
|
||||
}
|
||||
|
||||
func (c *CharSet) addSubtraction(sub *CharSet) {
|
||||
c.sub = sub
|
||||
}
|
||||
|
||||
func (c *CharSet) addRange(chMin, chMax rune) {
|
||||
c.ranges = append(c.ranges, singleRange{first: chMin, last: chMax})
|
||||
c.canonicalize()
|
||||
}
|
||||
|
||||
func (c *CharSet) addNamedASCII(name string, negate bool) bool {
|
||||
var rs []singleRange
|
||||
|
||||
switch name {
|
||||
case "alnum":
|
||||
rs = []singleRange{singleRange{'0', '9'}, singleRange{'A', 'Z'}, singleRange{'a', 'z'}}
|
||||
case "alpha":
|
||||
rs = []singleRange{singleRange{'A', 'Z'}, singleRange{'a', 'z'}}
|
||||
case "ascii":
|
||||
rs = []singleRange{singleRange{0, 0x7f}}
|
||||
case "blank":
|
||||
rs = []singleRange{singleRange{'\t', '\t'}, singleRange{' ', ' '}}
|
||||
case "cntrl":
|
||||
rs = []singleRange{singleRange{0, 0x1f}, singleRange{0x7f, 0x7f}}
|
||||
case "digit":
|
||||
c.addDigit(false, negate, "")
|
||||
case "graph":
|
||||
rs = []singleRange{singleRange{'!', '~'}}
|
||||
case "lower":
|
||||
rs = []singleRange{singleRange{'a', 'z'}}
|
||||
case "print":
|
||||
rs = []singleRange{singleRange{' ', '~'}}
|
||||
case "punct": //[!-/:-@[-`{-~]
|
||||
rs = []singleRange{singleRange{'!', '/'}, singleRange{':', '@'}, singleRange{'[', '`'}, singleRange{'{', '~'}}
|
||||
case "space":
|
||||
c.addSpace(true, false, negate)
|
||||
case "upper":
|
||||
rs = []singleRange{singleRange{'A', 'Z'}}
|
||||
case "word":
|
||||
c.addWord(true, negate)
|
||||
case "xdigit":
|
||||
rs = []singleRange{singleRange{'0', '9'}, singleRange{'A', 'F'}, singleRange{'a', 'f'}}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if len(rs) > 0 {
|
||||
if negate {
|
||||
c.addNegativeRanges(rs)
|
||||
} else {
|
||||
c.addRanges(rs)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type singleRangeSorter []singleRange
|
||||
|
||||
func (p singleRangeSorter) Len() int { return len(p) }
|
||||
func (p singleRangeSorter) Less(i, j int) bool { return p[i].first < p[j].first }
|
||||
func (p singleRangeSorter) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// Logic to reduce a character class to a unique, sorted form.
|
||||
func (c *CharSet) canonicalize() {
|
||||
var i, j int
|
||||
var last rune
|
||||
|
||||
//
|
||||
// Find and eliminate overlapping or abutting ranges
|
||||
//
|
||||
|
||||
if len(c.ranges) > 1 {
|
||||
sort.Sort(singleRangeSorter(c.ranges))
|
||||
|
||||
done := false
|
||||
|
||||
for i, j = 1, 0; ; i++ {
|
||||
for last = c.ranges[j].last; ; i++ {
|
||||
if i == len(c.ranges) || last == utf8.MaxRune {
|
||||
done = true
|
||||
break
|
||||
}
|
||||
|
||||
CurrentRange := c.ranges[i]
|
||||
if CurrentRange.first > last+1 {
|
||||
break
|
||||
}
|
||||
|
||||
if last < CurrentRange.last {
|
||||
last = CurrentRange.last
|
||||
}
|
||||
}
|
||||
|
||||
c.ranges[j] = singleRange{first: c.ranges[j].first, last: last}
|
||||
|
||||
j++
|
||||
|
||||
if done {
|
||||
break
|
||||
}
|
||||
|
||||
if j < i {
|
||||
c.ranges[j] = c.ranges[i]
|
||||
}
|
||||
}
|
||||
|
||||
c.ranges = append(c.ranges[:j], c.ranges[len(c.ranges):]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds to the class any lowercase versions of characters already
|
||||
// in the class. Used for case-insensitivity.
|
||||
func (c *CharSet) addLowercase() {
|
||||
if c.anything {
|
||||
return
|
||||
}
|
||||
toAdd := []singleRange{}
|
||||
for i := 0; i < len(c.ranges); i++ {
|
||||
r := c.ranges[i]
|
||||
if r.first == r.last {
|
||||
lower := unicode.ToLower(r.first)
|
||||
c.ranges[i] = singleRange{first: lower, last: lower}
|
||||
} else {
|
||||
toAdd = append(toAdd, r)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range toAdd {
|
||||
c.addLowercaseRange(r.first, r.last)
|
||||
}
|
||||
c.canonicalize()
|
||||
}
|
||||
|
||||
/**************************************************************************
|
||||
Let U be the set of Unicode character values and let L be the lowercase
|
||||
function, mapping from U to U. To perform case insensitive matching of
|
||||
character sets, we need to be able to map an interval I in U, say
|
||||
|
||||
I = [chMin, chMax] = { ch : chMin <= ch <= chMax }
|
||||
|
||||
to a set A such that A contains L(I) and A is contained in the union of
|
||||
I and L(I).
|
||||
|
||||
The table below partitions U into intervals on which L is non-decreasing.
|
||||
Thus, for any interval J = [a, b] contained in one of these intervals,
|
||||
L(J) is contained in [L(a), L(b)].
|
||||
|
||||
It is also true that for any such J, [L(a), L(b)] is contained in the
|
||||
union of J and L(J). This does not follow from L being non-decreasing on
|
||||
these intervals. It follows from the nature of the L on each interval.
|
||||
On each interval, L has one of the following forms:
|
||||
|
||||
(1) L(ch) = constant (LowercaseSet)
|
||||
(2) L(ch) = ch + offset (LowercaseAdd)
|
||||
(3) L(ch) = ch | 1 (LowercaseBor)
|
||||
(4) L(ch) = ch + (ch & 1) (LowercaseBad)
|
||||
|
||||
It is easy to verify that for any of these forms [L(a), L(b)] is
|
||||
contained in the union of [a, b] and L([a, b]).
|
||||
***************************************************************************/
|
||||
|
||||
const (
|
||||
LowercaseSet = 0 // Set to arg.
|
||||
LowercaseAdd = 1 // Add arg.
|
||||
LowercaseBor = 2 // Bitwise or with 1.
|
||||
LowercaseBad = 3 // Bitwise and with 1 and add original.
|
||||
)
|
||||
|
||||
type lcMap struct {
|
||||
chMin, chMax rune
|
||||
op, data int32
|
||||
}
|
||||
|
||||
var lcTable = []lcMap{
|
||||
lcMap{'\u0041', '\u005A', LowercaseAdd, 32},
|
||||
lcMap{'\u00C0', '\u00DE', LowercaseAdd, 32},
|
||||
lcMap{'\u0100', '\u012E', LowercaseBor, 0},
|
||||
lcMap{'\u0130', '\u0130', LowercaseSet, 0x0069},
|
||||
lcMap{'\u0132', '\u0136', LowercaseBor, 0},
|
||||
lcMap{'\u0139', '\u0147', LowercaseBad, 0},
|
||||
lcMap{'\u014A', '\u0176', LowercaseBor, 0},
|
||||
lcMap{'\u0178', '\u0178', LowercaseSet, 0x00FF},
|
||||
lcMap{'\u0179', '\u017D', LowercaseBad, 0},
|
||||
lcMap{'\u0181', '\u0181', LowercaseSet, 0x0253},
|
||||
lcMap{'\u0182', '\u0184', LowercaseBor, 0},
|
||||
lcMap{'\u0186', '\u0186', LowercaseSet, 0x0254},
|
||||
lcMap{'\u0187', '\u0187', LowercaseSet, 0x0188},
|
||||
lcMap{'\u0189', '\u018A', LowercaseAdd, 205},
|
||||
lcMap{'\u018B', '\u018B', LowercaseSet, 0x018C},
|
||||
lcMap{'\u018E', '\u018E', LowercaseSet, 0x01DD},
|
||||
lcMap{'\u018F', '\u018F', LowercaseSet, 0x0259},
|
||||
lcMap{'\u0190', '\u0190', LowercaseSet, 0x025B},
|
||||
lcMap{'\u0191', '\u0191', LowercaseSet, 0x0192},
|
||||
lcMap{'\u0193', '\u0193', LowercaseSet, 0x0260},
|
||||
lcMap{'\u0194', '\u0194', LowercaseSet, 0x0263},
|
||||
lcMap{'\u0196', '\u0196', LowercaseSet, 0x0269},
|
||||
lcMap{'\u0197', '\u0197', LowercaseSet, 0x0268},
|
||||
lcMap{'\u0198', '\u0198', LowercaseSet, 0x0199},
|
||||
lcMap{'\u019C', '\u019C', LowercaseSet, 0x026F},
|
||||
lcMap{'\u019D', '\u019D', LowercaseSet, 0x0272},
|
||||
lcMap{'\u019F', '\u019F', LowercaseSet, 0x0275},
|
||||
lcMap{'\u01A0', '\u01A4', LowercaseBor, 0},
|
||||
lcMap{'\u01A7', '\u01A7', LowercaseSet, 0x01A8},
|
||||
lcMap{'\u01A9', '\u01A9', LowercaseSet, 0x0283},
|
||||
lcMap{'\u01AC', '\u01AC', LowercaseSet, 0x01AD},
|
||||
lcMap{'\u01AE', '\u01AE', LowercaseSet, 0x0288},
|
||||
lcMap{'\u01AF', '\u01AF', LowercaseSet, 0x01B0},
|
||||
lcMap{'\u01B1', '\u01B2', LowercaseAdd, 217},
|
||||
lcMap{'\u01B3', '\u01B5', LowercaseBad, 0},
|
||||
lcMap{'\u01B7', '\u01B7', LowercaseSet, 0x0292},
|
||||
lcMap{'\u01B8', '\u01B8', LowercaseSet, 0x01B9},
|
||||
lcMap{'\u01BC', '\u01BC', LowercaseSet, 0x01BD},
|
||||
lcMap{'\u01C4', '\u01C5', LowercaseSet, 0x01C6},
|
||||
lcMap{'\u01C7', '\u01C8', LowercaseSet, 0x01C9},
|
||||
lcMap{'\u01CA', '\u01CB', LowercaseSet, 0x01CC},
|
||||
lcMap{'\u01CD', '\u01DB', LowercaseBad, 0},
|
||||
lcMap{'\u01DE', '\u01EE', LowercaseBor, 0},
|
||||
lcMap{'\u01F1', '\u01F2', LowercaseSet, 0x01F3},
|
||||
lcMap{'\u01F4', '\u01F4', LowercaseSet, 0x01F5},
|
||||
lcMap{'\u01FA', '\u0216', LowercaseBor, 0},
|
||||
lcMap{'\u0386', '\u0386', LowercaseSet, 0x03AC},
|
||||
lcMap{'\u0388', '\u038A', LowercaseAdd, 37},
|
||||
lcMap{'\u038C', '\u038C', LowercaseSet, 0x03CC},
|
||||
lcMap{'\u038E', '\u038F', LowercaseAdd, 63},
|
||||
lcMap{'\u0391', '\u03AB', LowercaseAdd, 32},
|
||||
lcMap{'\u03E2', '\u03EE', LowercaseBor, 0},
|
||||
lcMap{'\u0401', '\u040F', LowercaseAdd, 80},
|
||||
lcMap{'\u0410', '\u042F', LowercaseAdd, 32},
|
||||
lcMap{'\u0460', '\u0480', LowercaseBor, 0},
|
||||
lcMap{'\u0490', '\u04BE', LowercaseBor, 0},
|
||||
lcMap{'\u04C1', '\u04C3', LowercaseBad, 0},
|
||||
lcMap{'\u04C7', '\u04C7', LowercaseSet, 0x04C8},
|
||||
lcMap{'\u04CB', '\u04CB', LowercaseSet, 0x04CC},
|
||||
lcMap{'\u04D0', '\u04EA', LowercaseBor, 0},
|
||||
lcMap{'\u04EE', '\u04F4', LowercaseBor, 0},
|
||||
lcMap{'\u04F8', '\u04F8', LowercaseSet, 0x04F9},
|
||||
lcMap{'\u0531', '\u0556', LowercaseAdd, 48},
|
||||
lcMap{'\u10A0', '\u10C5', LowercaseAdd, 48},
|
||||
lcMap{'\u1E00', '\u1EF8', LowercaseBor, 0},
|
||||
lcMap{'\u1F08', '\u1F0F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F18', '\u1F1F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F28', '\u1F2F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F38', '\u1F3F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F48', '\u1F4D', LowercaseAdd, -8},
|
||||
lcMap{'\u1F59', '\u1F59', LowercaseSet, 0x1F51},
|
||||
lcMap{'\u1F5B', '\u1F5B', LowercaseSet, 0x1F53},
|
||||
lcMap{'\u1F5D', '\u1F5D', LowercaseSet, 0x1F55},
|
||||
lcMap{'\u1F5F', '\u1F5F', LowercaseSet, 0x1F57},
|
||||
lcMap{'\u1F68', '\u1F6F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F88', '\u1F8F', LowercaseAdd, -8},
|
||||
lcMap{'\u1F98', '\u1F9F', LowercaseAdd, -8},
|
||||
lcMap{'\u1FA8', '\u1FAF', LowercaseAdd, -8},
|
||||
lcMap{'\u1FB8', '\u1FB9', LowercaseAdd, -8},
|
||||
lcMap{'\u1FBA', '\u1FBB', LowercaseAdd, -74},
|
||||
lcMap{'\u1FBC', '\u1FBC', LowercaseSet, 0x1FB3},
|
||||
lcMap{'\u1FC8', '\u1FCB', LowercaseAdd, -86},
|
||||
lcMap{'\u1FCC', '\u1FCC', LowercaseSet, 0x1FC3},
|
||||
lcMap{'\u1FD8', '\u1FD9', LowercaseAdd, -8},
|
||||
lcMap{'\u1FDA', '\u1FDB', LowercaseAdd, -100},
|
||||
lcMap{'\u1FE8', '\u1FE9', LowercaseAdd, -8},
|
||||
lcMap{'\u1FEA', '\u1FEB', LowercaseAdd, -112},
|
||||
lcMap{'\u1FEC', '\u1FEC', LowercaseSet, 0x1FE5},
|
||||
lcMap{'\u1FF8', '\u1FF9', LowercaseAdd, -128},
|
||||
lcMap{'\u1FFA', '\u1FFB', LowercaseAdd, -126},
|
||||
lcMap{'\u1FFC', '\u1FFC', LowercaseSet, 0x1FF3},
|
||||
lcMap{'\u2160', '\u216F', LowercaseAdd, 16},
|
||||
lcMap{'\u24B6', '\u24D0', LowercaseAdd, 26},
|
||||
lcMap{'\uFF21', '\uFF3A', LowercaseAdd, 32},
|
||||
}
|
||||
|
||||
func (c *CharSet) addLowercaseRange(chMin, chMax rune) {
|
||||
var i, iMax, iMid int
|
||||
var chMinT, chMaxT rune
|
||||
var lc lcMap
|
||||
|
||||
for i, iMax = 0, len(lcTable); i < iMax; {
|
||||
iMid = (i + iMax) / 2
|
||||
if lcTable[iMid].chMax < chMin {
|
||||
i = iMid + 1
|
||||
} else {
|
||||
iMax = iMid
|
||||
}
|
||||
}
|
||||
|
||||
for ; i < len(lcTable); i++ {
|
||||
lc = lcTable[i]
|
||||
if lc.chMin > chMax {
|
||||
return
|
||||
}
|
||||
chMinT = lc.chMin
|
||||
if chMinT < chMin {
|
||||
chMinT = chMin
|
||||
}
|
||||
|
||||
chMaxT = lc.chMax
|
||||
if chMaxT > chMax {
|
||||
chMaxT = chMax
|
||||
}
|
||||
|
||||
switch lc.op {
|
||||
case LowercaseSet:
|
||||
chMinT = rune(lc.data)
|
||||
chMaxT = rune(lc.data)
|
||||
break
|
||||
case LowercaseAdd:
|
||||
chMinT += lc.data
|
||||
chMaxT += lc.data
|
||||
break
|
||||
case LowercaseBor:
|
||||
chMinT |= 1
|
||||
chMaxT |= 1
|
||||
break
|
||||
case LowercaseBad:
|
||||
chMinT += (chMinT & 1)
|
||||
chMaxT += (chMaxT & 1)
|
||||
break
|
||||
}
|
||||
|
||||
if chMinT < chMin || chMaxT > chMax {
|
||||
c.addRange(chMinT, chMaxT)
|
||||
}
|
||||
}
|
||||
}
|
||||
-274
@@ -1,274 +0,0 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// similar to prog.go in the go regex package...also with comment 'may not belong in this package'
|
||||
|
||||
// File provides operator constants for use by the Builder and the Machine.
|
||||
|
||||
// Implementation notes:
|
||||
//
|
||||
// Regexps are built into RegexCodes, which contain an operation array,
|
||||
// a string table, and some constants.
|
||||
//
|
||||
// Each operation is one of the codes below, followed by the integer
|
||||
// operands specified for each op.
|
||||
//
|
||||
// Strings and sets are indices into a string table.
|
||||
|
||||
type InstOp int
|
||||
|
||||
const (
|
||||
// lef/back operands description
|
||||
|
||||
Onerep InstOp = 0 // lef,back char,min,max a {n}
|
||||
Notonerep = 1 // lef,back char,min,max .{n}
|
||||
Setrep = 2 // lef,back set,min,max [\d]{n}
|
||||
|
||||
Oneloop = 3 // lef,back char,min,max a {,n}
|
||||
Notoneloop = 4 // lef,back char,min,max .{,n}
|
||||
Setloop = 5 // lef,back set,min,max [\d]{,n}
|
||||
|
||||
Onelazy = 6 // lef,back char,min,max a {,n}?
|
||||
Notonelazy = 7 // lef,back char,min,max .{,n}?
|
||||
Setlazy = 8 // lef,back set,min,max [\d]{,n}?
|
||||
|
||||
One = 9 // lef char a
|
||||
Notone = 10 // lef char [^a]
|
||||
Set = 11 // lef set [a-z\s] \w \s \d
|
||||
|
||||
Multi = 12 // lef string abcd
|
||||
Ref = 13 // lef group \#
|
||||
|
||||
Bol = 14 // ^
|
||||
Eol = 15 // $
|
||||
Boundary = 16 // \b
|
||||
Nonboundary = 17 // \B
|
||||
Beginning = 18 // \A
|
||||
Start = 19 // \G
|
||||
EndZ = 20 // \Z
|
||||
End = 21 // \Z
|
||||
|
||||
Nothing = 22 // Reject!
|
||||
|
||||
// Primitive control structures
|
||||
|
||||
Lazybranch = 23 // back jump straight first
|
||||
Branchmark = 24 // back jump branch first for loop
|
||||
Lazybranchmark = 25 // back jump straight first for loop
|
||||
Nullcount = 26 // back val set counter, null mark
|
||||
Setcount = 27 // back val set counter, make mark
|
||||
Branchcount = 28 // back jump,limit branch++ if zero<=c<limit
|
||||
Lazybranchcount = 29 // back jump,limit same, but straight first
|
||||
Nullmark = 30 // back save position
|
||||
Setmark = 31 // back save position
|
||||
Capturemark = 32 // back group define group
|
||||
Getmark = 33 // back recall position
|
||||
Setjump = 34 // back save backtrack state
|
||||
Backjump = 35 // zap back to saved state
|
||||
Forejump = 36 // zap backtracking state
|
||||
Testref = 37 // backtrack if ref undefined
|
||||
Goto = 38 // jump just go
|
||||
|
||||
Prune = 39 // prune it baby
|
||||
Stop = 40 // done!
|
||||
|
||||
ECMABoundary = 41 // \b
|
||||
NonECMABoundary = 42 // \B
|
||||
|
||||
// Modifiers for alternate modes
|
||||
|
||||
Mask = 63 // Mask to get unmodified ordinary operator
|
||||
Rtl = 64 // bit to indicate that we're reverse scanning.
|
||||
Back = 128 // bit to indicate that we're backtracking.
|
||||
Back2 = 256 // bit to indicate that we're backtracking on a second branch.
|
||||
Ci = 512 // bit to indicate that we're case-insensitive.
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Codes []int // the code
|
||||
Strings [][]rune // string table
|
||||
Sets []*CharSet //character set table
|
||||
TrackCount int // how many instructions use backtracking
|
||||
Caps map[int]int // mapping of user group numbers -> impl group slots
|
||||
Capsize int // number of impl group slots
|
||||
FcPrefix *Prefix // the set of candidate first characters (may be null)
|
||||
BmPrefix *BmPrefix // the fixed prefix string as a Boyer-Moore machine (may be null)
|
||||
Anchors AnchorLoc // the set of zero-length start anchors (RegexFCD.Bol, etc)
|
||||
RightToLeft bool // true if right to left
|
||||
}
|
||||
|
||||
func opcodeBacktracks(op InstOp) bool {
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case Oneloop, Notoneloop, Setloop, Onelazy, Notonelazy, Setlazy, Lazybranch, Branchmark, Lazybranchmark,
|
||||
Nullcount, Setcount, Branchcount, Lazybranchcount, Setmark, Capturemark, Getmark, Setjump, Backjump,
|
||||
Forejump, Goto:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func opcodeSize(op InstOp) int {
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case Nothing, Bol, Eol, Boundary, Nonboundary, ECMABoundary, NonECMABoundary, Beginning, Start, EndZ,
|
||||
End, Nullmark, Setmark, Getmark, Setjump, Backjump, Forejump, Stop:
|
||||
return 1
|
||||
|
||||
case One, Notone, Multi, Ref, Testref, Goto, Nullcount, Setcount, Lazybranch, Branchmark, Lazybranchmark,
|
||||
Prune, Set:
|
||||
return 2
|
||||
|
||||
case Capturemark, Branchcount, Lazybranchcount, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy,
|
||||
Setlazy, Setrep, Setloop:
|
||||
return 3
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("Unexpected op code: %v", op))
|
||||
}
|
||||
}
|
||||
|
||||
var codeStr = []string{
|
||||
"Onerep", "Notonerep", "Setrep",
|
||||
"Oneloop", "Notoneloop", "Setloop",
|
||||
"Onelazy", "Notonelazy", "Setlazy",
|
||||
"One", "Notone", "Set",
|
||||
"Multi", "Ref",
|
||||
"Bol", "Eol", "Boundary", "Nonboundary", "Beginning", "Start", "EndZ", "End",
|
||||
"Nothing",
|
||||
"Lazybranch", "Branchmark", "Lazybranchmark",
|
||||
"Nullcount", "Setcount", "Branchcount", "Lazybranchcount",
|
||||
"Nullmark", "Setmark", "Capturemark", "Getmark",
|
||||
"Setjump", "Backjump", "Forejump", "Testref", "Goto",
|
||||
"Prune", "Stop",
|
||||
"ECMABoundary", "NonECMABoundary",
|
||||
}
|
||||
|
||||
func operatorDescription(op InstOp) string {
|
||||
desc := codeStr[op&Mask]
|
||||
if (op & Ci) != 0 {
|
||||
desc += "-Ci"
|
||||
}
|
||||
if (op & Rtl) != 0 {
|
||||
desc += "-Rtl"
|
||||
}
|
||||
if (op & Back) != 0 {
|
||||
desc += "-Back"
|
||||
}
|
||||
if (op & Back2) != 0 {
|
||||
desc += "-Back2"
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
// OpcodeDescription is a humman readable string of the specific offset
|
||||
func (c *Code) OpcodeDescription(offset int) string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
op := InstOp(c.Codes[offset])
|
||||
fmt.Fprintf(buf, "%06d ", offset)
|
||||
|
||||
if opcodeBacktracks(op & Mask) {
|
||||
buf.WriteString("*")
|
||||
} else {
|
||||
buf.WriteString(" ")
|
||||
}
|
||||
buf.WriteString(operatorDescription(op))
|
||||
buf.WriteString("(")
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case One, Notone, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy:
|
||||
buf.WriteString("Ch = ")
|
||||
buf.WriteString(CharDescription(rune(c.Codes[offset+1])))
|
||||
|
||||
case Set, Setrep, Setloop, Setlazy:
|
||||
buf.WriteString("Set = ")
|
||||
buf.WriteString(c.Sets[c.Codes[offset+1]].String())
|
||||
|
||||
case Multi:
|
||||
fmt.Fprintf(buf, "String = %s", string(c.Strings[c.Codes[offset+1]]))
|
||||
|
||||
case Ref, Testref:
|
||||
fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1])
|
||||
|
||||
case Capturemark:
|
||||
fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1])
|
||||
if c.Codes[offset+2] != -1 {
|
||||
fmt.Fprintf(buf, ", Unindex = %d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
case Nullcount, Setcount:
|
||||
fmt.Fprintf(buf, "Value = %d", c.Codes[offset+1])
|
||||
|
||||
case Goto, Lazybranch, Branchmark, Lazybranchmark, Branchcount, Lazybranchcount:
|
||||
fmt.Fprintf(buf, "Addr = %d", c.Codes[offset+1])
|
||||
}
|
||||
|
||||
switch op {
|
||||
case Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy, Setrep, Setloop, Setlazy:
|
||||
buf.WriteString(", Rep = ")
|
||||
if c.Codes[offset+2] == math.MaxInt32 {
|
||||
buf.WriteString("inf")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
case Branchcount, Lazybranchcount:
|
||||
buf.WriteString(", Limit = ")
|
||||
if c.Codes[offset+2] == math.MaxInt32 {
|
||||
buf.WriteString("inf")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buf.WriteString(")")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (c *Code) Dump() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if c.RightToLeft {
|
||||
fmt.Fprintln(buf, "Direction: right-to-left")
|
||||
} else {
|
||||
fmt.Fprintln(buf, "Direction: left-to-right")
|
||||
}
|
||||
if c.FcPrefix == nil {
|
||||
fmt.Fprintln(buf, "Firstchars: n/a")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Firstchars: %v\n", c.FcPrefix.PrefixSet.String())
|
||||
}
|
||||
|
||||
if c.BmPrefix == nil {
|
||||
fmt.Fprintln(buf, "Prefix: n/a")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Prefix: %v\n", Escape(c.BmPrefix.String()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "Anchors: %v\n", c.Anchors)
|
||||
fmt.Fprintln(buf)
|
||||
|
||||
if c.BmPrefix != nil {
|
||||
fmt.Fprintln(buf, "BoyerMoore:")
|
||||
fmt.Fprintln(buf, c.BmPrefix.Dump(" "))
|
||||
}
|
||||
for i := 0; i < len(c.Codes); i += opcodeSize(InstOp(c.Codes[i])) {
|
||||
fmt.Fprintln(buf, c.OpcodeDescription(i))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
-654
@@ -1,654 +0,0 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type RegexTree struct {
|
||||
root *regexNode
|
||||
caps map[int]int
|
||||
capnumlist []int
|
||||
captop int
|
||||
Capnames map[string]int
|
||||
Caplist []string
|
||||
options RegexOptions
|
||||
}
|
||||
|
||||
// It is built into a parsed tree for a regular expression.
|
||||
|
||||
// Implementation notes:
|
||||
//
|
||||
// Since the node tree is a temporary data structure only used
|
||||
// during compilation of the regexp to integer codes, it's
|
||||
// designed for clarity and convenience rather than
|
||||
// space efficiency.
|
||||
//
|
||||
// RegexNodes are built into a tree, linked by the n.children list.
|
||||
// Each node also has a n.parent and n.ichild member indicating
|
||||
// its parent and which child # it is in its parent's list.
|
||||
//
|
||||
// RegexNodes come in as many types as there are constructs in
|
||||
// a regular expression, for example, "concatenate", "alternate",
|
||||
// "one", "rept", "group". There are also node types for basic
|
||||
// peephole optimizations, e.g., "onerep", "notsetrep", etc.
|
||||
//
|
||||
// Because perl 5 allows "lookback" groups that scan backwards,
|
||||
// each node also gets a "direction". Normally the value of
|
||||
// boolean n.backward = false.
|
||||
//
|
||||
// During parsing, top-level nodes are also stacked onto a parse
|
||||
// stack (a stack of trees). For this purpose we have a n.next
|
||||
// pointer. [Note that to save a few bytes, we could overload the
|
||||
// n.parent pointer instead.]
|
||||
//
|
||||
// On the parse stack, each tree has a "role" - basically, the
|
||||
// nonterminal in the grammar that the parser has currently
|
||||
// assigned to the tree. That code is stored in n.role.
|
||||
//
|
||||
// Finally, some of the different kinds of nodes have data.
|
||||
// Two integers (for the looping constructs) are stored in
|
||||
// n.operands, an an object (either a string or a set)
|
||||
// is stored in n.data
|
||||
type regexNode struct {
|
||||
t nodeType
|
||||
children []*regexNode
|
||||
str []rune
|
||||
set *CharSet
|
||||
ch rune
|
||||
m int
|
||||
n int
|
||||
options RegexOptions
|
||||
next *regexNode
|
||||
}
|
||||
|
||||
type nodeType int32
|
||||
|
||||
const (
|
||||
// The following are leaves, and correspond to primitive operations
|
||||
|
||||
ntOnerep nodeType = 0 // lef,back char,min,max a {n}
|
||||
ntNotonerep = 1 // lef,back char,min,max .{n}
|
||||
ntSetrep = 2 // lef,back set,min,max [\d]{n}
|
||||
ntOneloop = 3 // lef,back char,min,max a {,n}
|
||||
ntNotoneloop = 4 // lef,back char,min,max .{,n}
|
||||
ntSetloop = 5 // lef,back set,min,max [\d]{,n}
|
||||
ntOnelazy = 6 // lef,back char,min,max a {,n}?
|
||||
ntNotonelazy = 7 // lef,back char,min,max .{,n}?
|
||||
ntSetlazy = 8 // lef,back set,min,max [\d]{,n}?
|
||||
ntOne = 9 // lef char a
|
||||
ntNotone = 10 // lef char [^a]
|
||||
ntSet = 11 // lef set [a-z\s] \w \s \d
|
||||
ntMulti = 12 // lef string abcd
|
||||
ntRef = 13 // lef group \#
|
||||
ntBol = 14 // ^
|
||||
ntEol = 15 // $
|
||||
ntBoundary = 16 // \b
|
||||
ntNonboundary = 17 // \B
|
||||
ntBeginning = 18 // \A
|
||||
ntStart = 19 // \G
|
||||
ntEndZ = 20 // \Z
|
||||
ntEnd = 21 // \Z
|
||||
|
||||
// Interior nodes do not correspond to primitive operations, but
|
||||
// control structures compositing other operations
|
||||
|
||||
// Concat and alternate take n children, and can run forward or backwards
|
||||
|
||||
ntNothing = 22 // []
|
||||
ntEmpty = 23 // ()
|
||||
ntAlternate = 24 // a|b
|
||||
ntConcatenate = 25 // ab
|
||||
ntLoop = 26 // m,x * + ? {,}
|
||||
ntLazyloop = 27 // m,x *? +? ?? {,}?
|
||||
ntCapture = 28 // n ()
|
||||
ntGroup = 29 // (?:)
|
||||
ntRequire = 30 // (?=) (?<=)
|
||||
ntPrevent = 31 // (?!) (?<!)
|
||||
ntGreedy = 32 // (?>) (?<)
|
||||
ntTestref = 33 // (?(n) | )
|
||||
ntTestgroup = 34 // (?(...) | )
|
||||
|
||||
ntECMABoundary = 41 // \b
|
||||
ntNonECMABoundary = 42 // \B
|
||||
)
|
||||
|
||||
func newRegexNode(t nodeType, opt RegexOptions) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
}
|
||||
}
|
||||
|
||||
func newRegexNodeCh(t nodeType, opt RegexOptions, ch rune) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
ch: ch,
|
||||
}
|
||||
}
|
||||
|
||||
func newRegexNodeStr(t nodeType, opt RegexOptions, str []rune) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
str: str,
|
||||
}
|
||||
}
|
||||
|
||||
func newRegexNodeSet(t nodeType, opt RegexOptions, set *CharSet) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
set: set,
|
||||
}
|
||||
}
|
||||
|
||||
func newRegexNodeM(t nodeType, opt RegexOptions, m int) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
m: m,
|
||||
}
|
||||
}
|
||||
func newRegexNodeMN(t nodeType, opt RegexOptions, m, n int) *regexNode {
|
||||
return ®exNode{
|
||||
t: t,
|
||||
options: opt,
|
||||
m: m,
|
||||
n: n,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *regexNode) writeStrToBuf(buf *bytes.Buffer) {
|
||||
for i := 0; i < len(n.str); i++ {
|
||||
buf.WriteRune(n.str[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (n *regexNode) addChild(child *regexNode) {
|
||||
reduced := child.reduce()
|
||||
n.children = append(n.children, reduced)
|
||||
reduced.next = n
|
||||
}
|
||||
|
||||
func (n *regexNode) insertChildren(afterIndex int, nodes []*regexNode) {
|
||||
newChildren := make([]*regexNode, 0, len(n.children)+len(nodes))
|
||||
n.children = append(append(append(newChildren, n.children[:afterIndex]...), nodes...), n.children[afterIndex:]...)
|
||||
}
|
||||
|
||||
// removes children including the start but not the end index
|
||||
func (n *regexNode) removeChildren(startIndex, endIndex int) {
|
||||
n.children = append(n.children[:startIndex], n.children[endIndex:]...)
|
||||
}
|
||||
|
||||
// Pass type as OneLazy or OneLoop
|
||||
func (n *regexNode) makeRep(t nodeType, min, max int) {
|
||||
n.t += (t - ntOne)
|
||||
n.m = min
|
||||
n.n = max
|
||||
}
|
||||
|
||||
func (n *regexNode) reduce() *regexNode {
|
||||
switch n.t {
|
||||
case ntAlternate:
|
||||
return n.reduceAlternation()
|
||||
|
||||
case ntConcatenate:
|
||||
return n.reduceConcatenation()
|
||||
|
||||
case ntLoop, ntLazyloop:
|
||||
return n.reduceRep()
|
||||
|
||||
case ntGroup:
|
||||
return n.reduceGroup()
|
||||
|
||||
case ntSet, ntSetloop:
|
||||
return n.reduceSet()
|
||||
|
||||
default:
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
// Basic optimization. Single-letter alternations can be replaced
|
||||
// by faster set specifications, and nested alternations with no
|
||||
// intervening operators can be flattened:
|
||||
//
|
||||
// a|b|c|def|g|h -> [a-c]|def|[gh]
|
||||
// apple|(?:orange|pear)|grape -> apple|orange|pear|grape
|
||||
func (n *regexNode) reduceAlternation() *regexNode {
|
||||
if len(n.children) == 0 {
|
||||
return newRegexNode(ntNothing, n.options)
|
||||
}
|
||||
|
||||
wasLastSet := false
|
||||
lastNodeCannotMerge := false
|
||||
var optionsLast RegexOptions
|
||||
var i, j int
|
||||
|
||||
for i, j = 0, 0; i < len(n.children); i, j = i+1, j+1 {
|
||||
at := n.children[i]
|
||||
|
||||
if j < i {
|
||||
n.children[j] = at
|
||||
}
|
||||
|
||||
for {
|
||||
if at.t == ntAlternate {
|
||||
for k := 0; k < len(at.children); k++ {
|
||||
at.children[k].next = n
|
||||
}
|
||||
n.insertChildren(i+1, at.children)
|
||||
|
||||
j--
|
||||
} else if at.t == ntSet || at.t == ntOne {
|
||||
// Cannot merge sets if L or I options differ, or if either are negated.
|
||||
optionsAt := at.options & (RightToLeft | IgnoreCase)
|
||||
|
||||
if at.t == ntSet {
|
||||
if !wasLastSet || optionsLast != optionsAt || lastNodeCannotMerge || !at.set.IsMergeable() {
|
||||
wasLastSet = true
|
||||
lastNodeCannotMerge = !at.set.IsMergeable()
|
||||
optionsLast = optionsAt
|
||||
break
|
||||
}
|
||||
} else if !wasLastSet || optionsLast != optionsAt || lastNodeCannotMerge {
|
||||
wasLastSet = true
|
||||
lastNodeCannotMerge = false
|
||||
optionsLast = optionsAt
|
||||
break
|
||||
}
|
||||
|
||||
// The last node was a Set or a One, we're a Set or One and our options are the same.
|
||||
// Merge the two nodes.
|
||||
j--
|
||||
prev := n.children[j]
|
||||
|
||||
var prevCharClass *CharSet
|
||||
if prev.t == ntOne {
|
||||
prevCharClass = &CharSet{}
|
||||
prevCharClass.addChar(prev.ch)
|
||||
} else {
|
||||
prevCharClass = prev.set
|
||||
}
|
||||
|
||||
if at.t == ntOne {
|
||||
prevCharClass.addChar(at.ch)
|
||||
} else {
|
||||
prevCharClass.addSet(*at.set)
|
||||
}
|
||||
|
||||
prev.t = ntSet
|
||||
prev.set = prevCharClass
|
||||
} else if at.t == ntNothing {
|
||||
j--
|
||||
} else {
|
||||
wasLastSet = false
|
||||
lastNodeCannotMerge = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if j < i {
|
||||
n.removeChildren(j, i)
|
||||
}
|
||||
|
||||
return n.stripEnation(ntNothing)
|
||||
}
|
||||
|
||||
// Basic optimization. Adjacent strings can be concatenated.
|
||||
//
|
||||
// (?:abc)(?:def) -> abcdef
|
||||
func (n *regexNode) reduceConcatenation() *regexNode {
|
||||
// Eliminate empties and concat adjacent strings/chars
|
||||
|
||||
var optionsLast RegexOptions
|
||||
var optionsAt RegexOptions
|
||||
var i, j int
|
||||
|
||||
if len(n.children) == 0 {
|
||||
return newRegexNode(ntEmpty, n.options)
|
||||
}
|
||||
|
||||
wasLastString := false
|
||||
|
||||
for i, j = 0, 0; i < len(n.children); i, j = i+1, j+1 {
|
||||
var at, prev *regexNode
|
||||
|
||||
at = n.children[i]
|
||||
|
||||
if j < i {
|
||||
n.children[j] = at
|
||||
}
|
||||
|
||||
if at.t == ntConcatenate &&
|
||||
((at.options & RightToLeft) == (n.options & RightToLeft)) {
|
||||
for k := 0; k < len(at.children); k++ {
|
||||
at.children[k].next = n
|
||||
}
|
||||
|
||||
//insert at.children at i+1 index in n.children
|
||||
n.insertChildren(i+1, at.children)
|
||||
|
||||
j--
|
||||
} else if at.t == ntMulti || at.t == ntOne {
|
||||
// Cannot merge strings if L or I options differ
|
||||
optionsAt = at.options & (RightToLeft | IgnoreCase)
|
||||
|
||||
if !wasLastString || optionsLast != optionsAt {
|
||||
wasLastString = true
|
||||
optionsLast = optionsAt
|
||||
continue
|
||||
}
|
||||
|
||||
j--
|
||||
prev = n.children[j]
|
||||
|
||||
if prev.t == ntOne {
|
||||
prev.t = ntMulti
|
||||
prev.str = []rune{prev.ch}
|
||||
}
|
||||
|
||||
if (optionsAt & RightToLeft) == 0 {
|
||||
if at.t == ntOne {
|
||||
prev.str = append(prev.str, at.ch)
|
||||
} else {
|
||||
prev.str = append(prev.str, at.str...)
|
||||
}
|
||||
} else {
|
||||
if at.t == ntOne {
|
||||
// insert at the front by expanding our slice, copying the data over, and then setting the value
|
||||
prev.str = append(prev.str, 0)
|
||||
copy(prev.str[1:], prev.str)
|
||||
prev.str[0] = at.ch
|
||||
} else {
|
||||
//insert at the front...this one we'll make a new slice and copy both into it
|
||||
merge := make([]rune, len(prev.str)+len(at.str))
|
||||
copy(merge, at.str)
|
||||
copy(merge[len(at.str):], prev.str)
|
||||
prev.str = merge
|
||||
}
|
||||
}
|
||||
} else if at.t == ntEmpty {
|
||||
j--
|
||||
} else {
|
||||
wasLastString = false
|
||||
}
|
||||
}
|
||||
|
||||
if j < i {
|
||||
// remove indices j through i from the children
|
||||
n.removeChildren(j, i)
|
||||
}
|
||||
|
||||
return n.stripEnation(ntEmpty)
|
||||
}
|
||||
|
||||
// Nested repeaters just get multiplied with each other if they're not
|
||||
// too lumpy
|
||||
func (n *regexNode) reduceRep() *regexNode {
|
||||
|
||||
u := n
|
||||
t := n.t
|
||||
min := n.m
|
||||
max := n.n
|
||||
|
||||
for {
|
||||
if len(u.children) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
child := u.children[0]
|
||||
|
||||
// multiply reps of the same type only
|
||||
if child.t != t {
|
||||
childType := child.t
|
||||
|
||||
if !(childType >= ntOneloop && childType <= ntSetloop && t == ntLoop ||
|
||||
childType >= ntOnelazy && childType <= ntSetlazy && t == ntLazyloop) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// child can be too lumpy to blur, e.g., (a {100,105}) {3} or (a {2,})?
|
||||
// [but things like (a {2,})+ are not too lumpy...]
|
||||
if u.m == 0 && child.m > 1 || child.n < child.m*2 {
|
||||
break
|
||||
}
|
||||
|
||||
u = child
|
||||
if u.m > 0 {
|
||||
if (math.MaxInt32-1)/u.m < min {
|
||||
u.m = math.MaxInt32
|
||||
} else {
|
||||
u.m = u.m * min
|
||||
}
|
||||
}
|
||||
if u.n > 0 {
|
||||
if (math.MaxInt32-1)/u.n < max {
|
||||
u.n = math.MaxInt32
|
||||
} else {
|
||||
u.n = u.n * max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if math.MaxInt32 == min {
|
||||
return newRegexNode(ntNothing, n.options)
|
||||
}
|
||||
return u
|
||||
|
||||
}
|
||||
|
||||
// Simple optimization. If a concatenation or alternation has only
|
||||
// one child strip out the intermediate node. If it has zero children,
|
||||
// turn it into an empty.
|
||||
func (n *regexNode) stripEnation(emptyType nodeType) *regexNode {
|
||||
switch len(n.children) {
|
||||
case 0:
|
||||
return newRegexNode(emptyType, n.options)
|
||||
case 1:
|
||||
return n.children[0]
|
||||
default:
|
||||
return n
|
||||
}
|
||||
}
|
||||
|
||||
func (n *regexNode) reduceGroup() *regexNode {
|
||||
u := n
|
||||
|
||||
for u.t == ntGroup {
|
||||
u = u.children[0]
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// Simple optimization. If a set is a singleton, an inverse singleton,
|
||||
// or empty, it's transformed accordingly.
|
||||
func (n *regexNode) reduceSet() *regexNode {
|
||||
// Extract empty-set, one and not-one case as special
|
||||
|
||||
if n.set == nil {
|
||||
n.t = ntNothing
|
||||
} else if n.set.IsSingleton() {
|
||||
n.ch = n.set.SingletonChar()
|
||||
n.set = nil
|
||||
n.t += (ntOne - ntSet)
|
||||
} else if n.set.IsSingletonInverse() {
|
||||
n.ch = n.set.SingletonChar()
|
||||
n.set = nil
|
||||
n.t += (ntNotone - ntSet)
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *regexNode) reverseLeft() *regexNode {
|
||||
if n.options&RightToLeft != 0 && n.t == ntConcatenate && len(n.children) > 0 {
|
||||
//reverse children order
|
||||
for left, right := 0, len(n.children)-1; left < right; left, right = left+1, right-1 {
|
||||
n.children[left], n.children[right] = n.children[right], n.children[left]
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *regexNode) makeQuantifier(lazy bool, min, max int) *regexNode {
|
||||
if min == 0 && max == 0 {
|
||||
return newRegexNode(ntEmpty, n.options)
|
||||
}
|
||||
|
||||
if min == 1 && max == 1 {
|
||||
return n
|
||||
}
|
||||
|
||||
switch n.t {
|
||||
case ntOne, ntNotone, ntSet:
|
||||
if lazy {
|
||||
n.makeRep(Onelazy, min, max)
|
||||
} else {
|
||||
n.makeRep(Oneloop, min, max)
|
||||
}
|
||||
return n
|
||||
|
||||
default:
|
||||
var t nodeType
|
||||
if lazy {
|
||||
t = ntLazyloop
|
||||
} else {
|
||||
t = ntLoop
|
||||
}
|
||||
result := newRegexNodeMN(t, n.options, min, max)
|
||||
result.addChild(n)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// debug functions
|
||||
|
||||
var typeStr = []string{
|
||||
"Onerep", "Notonerep", "Setrep",
|
||||
"Oneloop", "Notoneloop", "Setloop",
|
||||
"Onelazy", "Notonelazy", "Setlazy",
|
||||
"One", "Notone", "Set",
|
||||
"Multi", "Ref",
|
||||
"Bol", "Eol", "Boundary", "Nonboundary",
|
||||
"Beginning", "Start", "EndZ", "End",
|
||||
"Nothing", "Empty",
|
||||
"Alternate", "Concatenate",
|
||||
"Loop", "Lazyloop",
|
||||
"Capture", "Group", "Require", "Prevent", "Greedy",
|
||||
"Testref", "Testgroup",
|
||||
"Unknown", "Unknown", "Unknown",
|
||||
"Unknown", "Unknown", "Unknown",
|
||||
"ECMABoundary", "NonECMABoundary",
|
||||
}
|
||||
|
||||
func (n *regexNode) description() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
buf.WriteString(typeStr[n.t])
|
||||
|
||||
if (n.options & ExplicitCapture) != 0 {
|
||||
buf.WriteString("-C")
|
||||
}
|
||||
if (n.options & IgnoreCase) != 0 {
|
||||
buf.WriteString("-I")
|
||||
}
|
||||
if (n.options & RightToLeft) != 0 {
|
||||
buf.WriteString("-L")
|
||||
}
|
||||
if (n.options & Multiline) != 0 {
|
||||
buf.WriteString("-M")
|
||||
}
|
||||
if (n.options & Singleline) != 0 {
|
||||
buf.WriteString("-S")
|
||||
}
|
||||
if (n.options & IgnorePatternWhitespace) != 0 {
|
||||
buf.WriteString("-X")
|
||||
}
|
||||
if (n.options & ECMAScript) != 0 {
|
||||
buf.WriteString("-E")
|
||||
}
|
||||
|
||||
switch n.t {
|
||||
case ntOneloop, ntNotoneloop, ntOnelazy, ntNotonelazy, ntOne, ntNotone:
|
||||
buf.WriteString("(Ch = " + CharDescription(n.ch) + ")")
|
||||
break
|
||||
case ntCapture:
|
||||
buf.WriteString("(index = " + strconv.Itoa(n.m) + ", unindex = " + strconv.Itoa(n.n) + ")")
|
||||
break
|
||||
case ntRef, ntTestref:
|
||||
buf.WriteString("(index = " + strconv.Itoa(n.m) + ")")
|
||||
break
|
||||
case ntMulti:
|
||||
fmt.Fprintf(buf, "(String = %s)", string(n.str))
|
||||
break
|
||||
case ntSet, ntSetloop, ntSetlazy:
|
||||
buf.WriteString("(Set = " + n.set.String() + ")")
|
||||
break
|
||||
}
|
||||
|
||||
switch n.t {
|
||||
case ntOneloop, ntNotoneloop, ntOnelazy, ntNotonelazy, ntSetloop, ntSetlazy, ntLoop, ntLazyloop:
|
||||
buf.WriteString("(Min = ")
|
||||
buf.WriteString(strconv.Itoa(n.m))
|
||||
buf.WriteString(", Max = ")
|
||||
if n.n == math.MaxInt32 {
|
||||
buf.WriteString("inf")
|
||||
} else {
|
||||
buf.WriteString(strconv.Itoa(n.n))
|
||||
}
|
||||
buf.WriteString(")")
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var padSpace = []byte(" ")
|
||||
|
||||
func (t *RegexTree) Dump() string {
|
||||
return t.root.dump()
|
||||
}
|
||||
|
||||
func (n *regexNode) dump() string {
|
||||
var stack []int
|
||||
CurNode := n
|
||||
CurChild := 0
|
||||
|
||||
buf := bytes.NewBufferString(CurNode.description())
|
||||
buf.WriteRune('\n')
|
||||
|
||||
for {
|
||||
if CurNode.children != nil && CurChild < len(CurNode.children) {
|
||||
stack = append(stack, CurChild+1)
|
||||
CurNode = CurNode.children[CurChild]
|
||||
CurChild = 0
|
||||
|
||||
Depth := len(stack)
|
||||
if Depth > 32 {
|
||||
Depth = 32
|
||||
}
|
||||
buf.Write(padSpace[:Depth])
|
||||
buf.WriteString(CurNode.description())
|
||||
buf.WriteRune('\n')
|
||||
} else {
|
||||
if len(stack) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
CurChild = stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
CurNode = CurNode.next
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
-7061
File diff suppressed because it is too large
Load Diff
+2
@@ -25,3 +25,5 @@ _testmain.go
|
||||
*.out
|
||||
|
||||
.DS_Store
|
||||
*.txt
|
||||
benchmarks/
|
||||
Generated
Vendored
Generated
Vendored
+283
@@ -0,0 +1,283 @@
|
||||
# regexp2 - full featured regular expressions for Go
|
||||
Regexp2 is a feature-rich RegExp engine for Go. It doesn't have constant time guarantees like the built-in `regexp` package, but it allows backtracking and is compatible with Perl5 and .NET. You'll likely be better off with the RE2 engine from the `regexp` package and should only use this if you need to write very complex patterns or require compatibility with .NET.
|
||||
|
||||
## Basis of the engine
|
||||
The engine is ported from the .NET framework's System.Text.RegularExpressions.Regex engine. That engine was open sourced in 2015 under the MIT license. There are some fundamental differences between .NET strings and Go strings that required a bit of borrowing from the Go framework regex engine as well. I cleaned up a couple of the dirtier bits during the port (regexcharclass.cs was terrible), but the parse tree, code emmitted, and therefore patterns matched should be identical.
|
||||
|
||||
## New Code Generation
|
||||
For extra performance use `regexp2` with [`regexp2cg`](https://github.com/dlclark/regexp2cg). It is a code generation utility for `regexp2` and you can likely improve your regexp runtime performance by 3-10x in hot code paths. As always you should benchmark your specifics to confirm the results. Give it a try!
|
||||
|
||||
## Installing
|
||||
This is a go-gettable library, so install is easy:
|
||||
|
||||
go get github.com/dlclark/regexp2/v2@latest
|
||||
|
||||
## Changes in v2
|
||||
Version 2 includes changes that may affect compatibility with existing v1 users:
|
||||
|
||||
* The module path is now `github.com/dlclark/regexp2/v2`, so imports need to use the `/v2` suffix.
|
||||
* The minimum supported Go version is now Go 1.25.
|
||||
* Changes to support https://github.com/dlclark/regexp2cg are merged in to support generated regex engines.
|
||||
* `Regexp.Split` is now available for splitting strings with regexp matches.
|
||||
* The new `compat` sub-package provides a [`regexp` compatibility adapter](#regexp-compatibility-adapter) with the same `Find*` and `Match*` method signatures as `regexp.Regexp`, plus a `compat.Matcher` interface that is implemented by both `*regexp.Regexp` and the adapter.
|
||||
* The parser, optimizer, and runner internals have changed significantly to support generated regexes and additional matching optimizations.
|
||||
* `Compile` and `MustCompile` now use variadic compile options for regex behavior and memory/performance tuning. See [Compile options](#compile-options) for more details.
|
||||
* Moved `regexp2.Debug` and `regexp2.Compile` to new `regexp2.OptionDebug()` and `regexp2.OptionIsCodeGen()` compile options.
|
||||
* Some types and constants in the `syntax` package have been exported or changed to support code generation.
|
||||
* Conceptually changed the goal of the `regexp2.ECMAScript` option to be closer to the ECMAScript standard rather than C#'s ECMAScript behavior.
|
||||
* Renamed the fields `Capture.Index` and `Capture.Length` to `Capture.RuneIndex` and `Capture.RuneLength` to be more clear that we're dealing with rune offsets.
|
||||
* Added `Capture.ByteRange()` to return the byte offset index and length of the captured text. This requires some additional processing to be done behind the scenes the first time it's called for a given capture to convert the native rune offsets to byte offsets.
|
||||
|
||||
## Usage
|
||||
Usage is similar to the Go `regexp` package. Just like in `regexp`, you start by converting a regex into a state machine via the `Compile` or `MustCompile` methods. They ultimately do the same thing, but `MustCompile` will panic if the regex is invalid. You can then use the provided `Regexp` struct to find matches repeatedly. A `Regexp` struct is safe to use across goroutines.
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your pattern`)
|
||||
if isMatch, _ := re.MatchString(`Something to match`); isMatch {
|
||||
//do something
|
||||
}
|
||||
```
|
||||
|
||||
The only error that the `*Match*` methods *should* return is a Timeout if you set the `re.MatchTimeout` field. Any other error is a bug in the `regexp2` package. If you need more details about capture groups in a match then use the `FindStringMatch` method, like so:
|
||||
|
||||
```go
|
||||
if m, _ := re.FindStringMatch(`Something to match`); m != nil {
|
||||
// the whole match is always group 0
|
||||
fmt.Printf("Group 0: %v\n", m.String())
|
||||
|
||||
// you can get all the groups too
|
||||
gps := m.Groups()
|
||||
|
||||
// a group can be captured multiple times, so each cap is separately addressable
|
||||
fmt.Printf("Group 1, first capture", gps[1].Captures[0].String())
|
||||
fmt.Printf("Group 1, second capture", gps[1].Captures[1].String())
|
||||
}
|
||||
```
|
||||
|
||||
Group 0 is embedded in the Match. Group 0 is an automatically-assigned group that encompasses the whole pattern. This means that `m.String()` is the same as `m.Group.String()` and `m.Groups()[0].String()`
|
||||
|
||||
The __last__ capture is embedded in each group, so `g.String()` will return the same thing as `g.Capture.String()` and `g.Captures[len(g.Captures)-1].String()`.
|
||||
|
||||
If you want to find multiple matches from a single input string you should use the `FindNextMatch` method. For example, to implement a function similar to `regexp.FindAllString`:
|
||||
|
||||
```go
|
||||
func regexp2FindAllString(re *regexp2.Regexp, s string) []string {
|
||||
var matches []string
|
||||
m, _ := re.FindStringMatch(s)
|
||||
for m != nil {
|
||||
matches = append(matches, m.String())
|
||||
m, _ = re.FindNextMatch(m)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
```
|
||||
|
||||
`FindNextMatch` is optmized so that it re-uses the underlying string/rune slice.
|
||||
|
||||
The internals of `regexp2` always operate on `[]rune` so `RuneIndex` and `RuneLength` data in a `Match` always reference a position in `rune`s rather than `byte`s (even if the input was given as a string). `ByteRange()` provides UTF-8 byte offsets, matching the original string input for string APIs. It's advisable to use the provided `String()` methods when you do not need explicit offsets. `ByteRange()` lazily caches byte offsets on the shared match text, so the first call on captures from the same match is not safe to run concurrently with other `ByteRange()` calls on that match.
|
||||
|
||||
## Unicode character classes
|
||||
|
||||
`regexp2` supports Unicode character classes with `\p{...}` and negated classes with `\P{...}`. Outside ECMAScript Unicode mode, it also accepts the RE2/PCRE-style one-letter form, such as `\pL`.
|
||||
|
||||
The class name may be a Go Unicode category, category alias, script, or property exposed by the Go standard library Unicode tables. For example:
|
||||
|
||||
```go
|
||||
letter := regexp2.MustCompile(`\p{L}+`)
|
||||
katakana := regexp2.MustCompile(`\p{Katakana}+`)
|
||||
notEmoji := regexp2.MustCompile(`\P{Emoji}+`)
|
||||
```
|
||||
|
||||
`regexp2` also supports Unicode property selection syntax in the form `\p{property=value}`. Property and value aliases are matched loosely: case, hyphens, and underscores are ignored. For example, `\p{GCB=RI}`, `\p{grapheme_cluster_break=regional_indicator}`, and `\p{grapheme-cluster-break=regional-indicator}` all refer to the same class.
|
||||
|
||||
Valid property names and aliases come from Unicode 17.0.0 [`PropertyAliases.txt`](https://www.unicode.org/Public/17.0.0/ucd/PropertyAliases.txt). Valid property values and aliases come from Unicode 17.0.0 [`PropertyValueAliases.txt`](https://www.unicode.org/Public/17.0.0/ucd/PropertyValueAliases.txt). The generated tables use Unicode 17.0.0 data from [`DerivedCoreProperties.txt`](https://www.unicode.org/Public/17.0.0/ucd/DerivedCoreProperties.txt), [`emoji/emoji-data.txt`](https://www.unicode.org/Public/17.0.0/ucd/emoji/emoji-data.txt), [`auxiliary/GraphemeBreakProperty.txt`](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/GraphemeBreakProperty.txt), [`auxiliary/WordBreakProperty.txt`](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/WordBreakProperty.txt), and [`auxiliary/SentenceBreakProperty.txt`](https://www.unicode.org/Public/17.0.0/ucd/auxiliary/SentenceBreakProperty.txt) for the package-local properties whose data changes more frequently than the Go standard library tables.
|
||||
|
||||
## `regexp` compatibility adapter
|
||||
|
||||
The `github.com/dlclark/regexp2/v2/compat` package provides an adapter for callers that want the same `Find*` and `Match*` method signatures as the standard library's `regexp.Regexp`, while still using the `regexp2` engine.
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/dlclark/regexp2/v2"
|
||||
"github.com/dlclark/regexp2/v2/compat"
|
||||
)
|
||||
|
||||
re := compat.MustCompile(`Your pattern`, regexp2.RE2)
|
||||
if re.MatchString(`Something to match`) {
|
||||
// do something
|
||||
}
|
||||
|
||||
matches := re.FindAllString(`abc axbc`, -1)
|
||||
_ = matches
|
||||
```
|
||||
|
||||
You can also wrap an existing compiled regexp:
|
||||
|
||||
```go
|
||||
base := regexp2.MustCompile(`Your pattern`)
|
||||
re := compat.Wrap(base)
|
||||
```
|
||||
|
||||
The adapter includes the standard-library matching surface: `Match`, `MatchString`, `MatchReader`, and all `Find(All)?(String)?(Submatch)?(Index)?` methods. Index-returning methods use UTF-8 byte offsets like `regexp`, not regexp2's rune offsets.
|
||||
|
||||
The package also defines `compat.Matcher`, a common interface implemented by both `*regexp.Regexp` and `*compat.Regexp`. Use it when code should accept either the standard library engine or a regexp2-backed adapter:
|
||||
|
||||
```go
|
||||
func findWords(re compat.Matcher, input string) []string {
|
||||
return re.FindAllString(input, -1)
|
||||
}
|
||||
```
|
||||
|
||||
Because those standard-library method signatures do not return errors, the adapter panics if the wrapped regexp2 matcher returns an error such as a match timeout. Use the main `regexp2` APIs directly when you need to handle timeouts as errors.
|
||||
|
||||
## Compile options
|
||||
|
||||
`Compile` and `MustCompile` take variadic compile options. Most users can omit them and get default regex behavior plus bounded shared pools for rune buffers and replacement output buffers, plus per-regexp caches for parsed replacement patterns and ASCII character class bitmaps.
|
||||
|
||||
Regex option constants can be passed directly, individually or as a bitmask:
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your pattern`, regexp2.IgnoreCase, regexp2.Singleline)
|
||||
re = regexp2.MustCompile(`Your pattern`, regexp2.IgnoreCase|regexp2.Singleline)
|
||||
```
|
||||
|
||||
Performance tuning options override the default cache settings:
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your pattern`,
|
||||
regexp2.IgnoreCase,
|
||||
regexp2.OptionMaxCachedRuneBufferLength(64*1024),
|
||||
regexp2.OptionMaxCachedReplacerDataEntries(8),
|
||||
)
|
||||
```
|
||||
|
||||
Compile-only options configure behavior that is not settable from the pattern:
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`(?<first>This) (is)`, regexp2.OptionMaintainCaptureOrder())
|
||||
```
|
||||
|
||||
The defaults are intentionally bounded:
|
||||
|
||||
| Option | Default | Used by | Working-set growth | Tradeoffs |
|
||||
| --- | ---: | --- | --- | --- |
|
||||
| `OptionMaintainCaptureOrder()` | false | Parser capture-slot assignment for mixed named and unnamed captures. | None at match time. This changes compile-time capture numbering only. | Keeps named and unnamed captures in pattern order instead of appending named captures after unnamed captures. This can change numeric backreference meaning, so it is caller-controlled rather than an inline regex option. |
|
||||
| `OptionDebug()` | false | Compile dumps and runner tracing. | Debug output volume only. | Useful for diagnostics, but it can produce noisy output and slower traced matching. |
|
||||
| `OptionIsCodeGen()` | false | Compile-time find-optimization analysis for [`regexp2cg`](https://github.com/dlclark/regexp2cg). | Per compiled regexp, during `Compile` or `MustCompile`. | Enables more expensive analysis intended for generated engines. Do not use it for normal interpreter execution; the interpreter defaults intentionally avoid this extra compile-time cost. |
|
||||
| `OptionMaxCachedRuneBufferLength(n)` | 256K runes | String APIs that run through pooled runners, such as `MatchString` and replacement-pattern `Replace`, when converting input strings to the engine's internal `[]rune` representation. | Process-wide shared `sync.Pool` retention by size class. This does not grow per compiled regexp or per input string; the practical working set follows recent and concurrent use across all regexps and can be dropped by GC. | Raising this lets calls use larger pooled rune buffers and can reduce allocations for repeated matches against large strings. Lowering it prevents larger buffers from being borrowed or returned, so large inputs allocate directly. |
|
||||
| `OptionMaxCachedReplaceBufferLength(n)` | 256 KB | Replacement-pattern `Replace` calls that build output through a shared byte buffer. | Process-wide shared `sync.Pool` retention by size class after replacement-pattern `Replace` runs. It does not grow from evaluator-based `ReplaceFunc` output and is shared across compiled regexps. | Raising this lets larger replacement outputs use pooled buffers and can reduce allocations. Lowering it prevents larger output buffers from being retained, so large replacements allocate directly. |
|
||||
| `OptionMaxCachedReplacerDataEntries(n)` | `16` | `Replace` with replacement pattern strings, after the replacement pattern is parsed into reusable replacement data. | Per compiled regexp. The cache grows as distinct cacheable replacement strings are used with `Replace`, up to this entry count. | Raising this helps when a single compiled regexp is used with many recurring replacement patterns. It increases per-regexp cache memory and lock-protected cache bookkeeping. Setting it to `0` disables this cache. |
|
||||
| `OptionMaxCachedReplacerDataBytes(n)` | 4 KB | The parsed replacement-pattern cache. Replacement strings longer than this are parsed for the call but not retained. | Per compiled regexp, combined with `OptionMaxCachedReplacerDataEntries`. Only replacement strings whose source text is at or below this size can add parsed data to the cache. | Raising this helps if large replacement patterns are reused. It can retain more memory per cached replacement. Lowering it avoids keeping unusual large replacement patterns around. |
|
||||
| `OptionDisableCharClassASCIIBitmap()` | false | Compile-time preparation of character classes and first-character prefix sets. By default, character classes with ASCII membership get a small bitmap used by `CharIn`. | Per compiled regexp, during `Compile` or `MustCompile`. Each eligible character class can hold one small bitmap; this does not scale with match concurrency or input size. | Leaving this false speeds up ASCII-heavy character class checks at the cost of a small amount of per-char-class memory and compile-time work. Setting to true can reduce memory for large numbers of compiled char classes in regexps, but ASCII character class matching may be slower. |
|
||||
|
||||
For pooled buffer cache options, set `n` to `0` to disable pooling, or `-1` to allow all built-in size classes. The rune buffer classes are 1K, 4K, 16K, 64K, and 256K runes. The replacement byte buffer classes are 4 KB, 16 KB, 64 KB, 256 KB, and 1 MB. By default the 1 MB pool is unused. For replacement data byte-size cache options, `-1` means unbounded. For entry-count cache options, set `n` to `0` to disable the cache.
|
||||
|
||||
## Compare `regexp` and `regexp2`
|
||||
| Category | regexp | regexp2 |
|
||||
| --- | --- | --- |
|
||||
| Catastrophic backtracking possible | no, constant execution time guarantees | yes, if your pattern is at risk you can use the `re.MatchTimeout` field |
|
||||
| Python-style capture groups `(?P<name>re)` | yes | no (yes in RE2 compat mode) |
|
||||
| .NET-style capture groups `(?<name>re)` or `(?'name're)` | yes | yes |
|
||||
| comments `(?#comment)` | no | yes |
|
||||
| branch numbering reset `(?\|a\|b)` | no | no |
|
||||
| possessive match `(?>re)` | no | yes |
|
||||
| positive lookahead `(?=re)` | no | yes |
|
||||
| negative lookahead `(?!re)` | no | yes |
|
||||
| positive lookbehind `(?<=re)` | no | yes |
|
||||
| negative lookbehind `(?<!re)` | no | yes |
|
||||
| back reference `\1` | no | yes |
|
||||
| named back reference `\k'name'` | no | yes |
|
||||
| Python-style named back reference `(?P=name)` | no | no (yes in RE2 compat mode) |
|
||||
| named ascii character class `[[:foo:]]`| yes | no (yes in RE2 compat mode) |
|
||||
| conditionals `(?(expr)yes\|no)` | no | yes |
|
||||
|
||||
## RE2 compatibility mode
|
||||
The default behavior of `regexp2` is to match the .NET regexp engine, however the `RE2` option is provided to change the parsing to increase compatibility with RE2. Using the `RE2` option when compiling a regexp will not take away any features, but will change the following behaviors:
|
||||
* add support for named ascii character classes (e.g. `[[:foo:]]`)
|
||||
* add support for python-style capture groups (e.g. `(?P<name>re)`)
|
||||
* add support for python-style named backreferences (e.g. `(?P=name)`)
|
||||
* change singleline behavior for `$` to only match end of string (like RE2) (see [#24](https://github.com/dlclark/regexp2/issues/24))
|
||||
* change the character classes `\d` `\s` and `\w` to match the same characters as RE2. NOTE: if you also use the `ECMAScript` option then this will change the `\s` character class to match ECMAScript instead of RE2. ECMAScript allows more whitespace characters in `\s` than RE2 (but still fewer than the the default behavior).
|
||||
* allow character escape sequences to have defaults. For example, by default `\_` isn't a known character escape and will fail to compile, but in RE2 mode it will match the literal character `_`
|
||||
|
||||
```go
|
||||
re := regexp2.MustCompile(`Your RE2-compatible pattern`, regexp2.RE2)
|
||||
if isMatch, _ := re.MatchString(`Something to match`); isMatch {
|
||||
//do something
|
||||
}
|
||||
```
|
||||
|
||||
This feature is a work in progress and I'm open to ideas for more things to put here (maybe more relaxed character escaping rules?).
|
||||
|
||||
## Catastrophic Backtracking and Timeouts
|
||||
|
||||
`regexp2` supports features that can lead to catastrophic backtracking.
|
||||
`Regexp.MatchTimeout` can be set to to limit the impact of such behavior; the
|
||||
match will fail with an error after approximately MatchTimeout. No timeout
|
||||
checks are done by default.
|
||||
|
||||
Timeout checking is not free. The current timeout checking implementation starts
|
||||
a background worker that updates a clock value approximately once every 100
|
||||
milliseconds. The matching code compares this value against the precomputed
|
||||
deadline for the match. The performance impact is as follows.
|
||||
|
||||
1. A match with a timeout runs almost as fast as a match without a timeout.
|
||||
2. If any live matches have a timeout, there will be a background CPU load
|
||||
(`~0.15%` currently on a modern machine). This load will remain constant
|
||||
regardless of the number of matches done including matches done in parallel.
|
||||
3. If no live matches are using a timeout, the background load will remain
|
||||
until the longest deadline (match timeout + the time when the match started)
|
||||
is reached. E.g., if you set a timeout of one minute the load will persist
|
||||
for approximately a minute even if the match finishes quickly.
|
||||
|
||||
See [PR #58](https://github.com/dlclark/regexp2/pull/58) for more details and
|
||||
alternatives considered.
|
||||
|
||||
## Goroutine leak error
|
||||
If you're using a library during unit tests (e.g. https://github.com/uber-go/goleak) that validates all goroutines are exited then you'll likely get an error if you or any of your dependencies use regex's with a MatchTimeout.
|
||||
To remedy the problem you'll need to tell the unit test to wait until the backgroup timeout goroutine is exited.
|
||||
|
||||
```go
|
||||
func TestSomething(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
defer regexp2.StopTimeoutClock()
|
||||
|
||||
// ... test
|
||||
}
|
||||
|
||||
//or
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// setup
|
||||
// ...
|
||||
|
||||
// run
|
||||
m.Run()
|
||||
|
||||
//tear down
|
||||
regexp2.StopTimeoutClock()
|
||||
goleak.VerifyNone(t)
|
||||
}
|
||||
```
|
||||
|
||||
This will add ~100ms runtime to each test (or TestMain). If that's too much time you can set the clock cycle rate of the timeout goroutine in an init function in a test file. `regexp2.SetTimeoutCheckPeriod` isn't threadsafe so it must be setup before starting any regex's with Timeouts.
|
||||
|
||||
```go
|
||||
func init() {
|
||||
//speed up testing by making the timeout clock 1ms
|
||||
regexp2.SetTimeoutCheckPeriod(time.Millisecond)
|
||||
}
|
||||
```
|
||||
|
||||
## ECMAScript compatibility mode
|
||||
In this mode the engine attempts to match the [regex engine](https://tc39.es/ecma262/multipage/text-processing.html#sec-regexp-regular-expression-objects) described in the ECMAScript specification as closely as reasonably possible within regexp2's API and implementation.
|
||||
|
||||
This flag should not be treated as compatibility with C#'s `RegexOptions.ECMAScript`. regexp2's ECMAScript behavior prioritizes ECMAScript specification behavior over matching the C# regex engine's interpretation of that option.
|
||||
|
||||
Additionally a Unicode mode is provided which allows parsing of `\u{CodePoint}` syntax only when both `ECMAScript` and `Unicode` are provided.
|
||||
|
||||
## Potential bugs
|
||||
I've run a battery of tests against regexp2 from various sources and found the debug output matches the .NET engine, but .NET and Go handle strings very differently. I've attempted to handle these differences, but most of my testing deals with basic ASCII with a little bit of multi-byte Unicode. There's a chance that there are bugs in the string handling related to character sets with supplementary Unicode chars. Right-to-Left support is coded, but not well tested either.
|
||||
|
||||
## Find a bug?
|
||||
I'm open to new issues and pull requests with tests if you find something odd!
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
benchcmp:
|
||||
desc: Compare current benchmarks against an optional commit, defaulting to HEAD
|
||||
vars:
|
||||
BENCH_COUNT: '{{default "5" .COUNT}}'
|
||||
REF:
|
||||
sh: |
|
||||
if [ -n "{{.COMMIT}}" ]; then
|
||||
printf '%s\n' "{{.COMMIT}}"
|
||||
elif [ -n "{{.CLI_ARGS}}" ]; then
|
||||
set -- {{.CLI_ARGS}}
|
||||
printf '%s\n' "$1"
|
||||
else
|
||||
printf '%s\n' HEAD
|
||||
fi
|
||||
COMMIT_HASH:
|
||||
sh: git rev-parse --verify "{{.REF}}^{commit}"
|
||||
cmds:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
mkdir -p benchmarks
|
||||
|
||||
base_file="benchmarks/{{.COMMIT_HASH}}-count{{.BENCH_COUNT}}.txt"
|
||||
new_file="benchmarks/new.txt"
|
||||
base_tmp=""
|
||||
new_tmp=""
|
||||
worktree_dir="$(mktemp -d /tmp/regexp2-benchcmp.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "$base_tmp" ]; then
|
||||
rm -f "$base_tmp"
|
||||
fi
|
||||
if [ -n "$new_tmp" ]; then
|
||||
rm -f "$new_tmp"
|
||||
fi
|
||||
git worktree remove --force "$worktree_dir" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ -f "$base_file" ] && grep -q '^Benchmark' "$base_file"; then
|
||||
printf 'Using cached baseline benchmarks from %s...\n' "$base_file"
|
||||
else
|
||||
if [ -f "$base_file" ]; then
|
||||
printf 'Ignoring cached baseline without benchmark results at %s...\n' "$base_file"
|
||||
fi
|
||||
printf 'Running baseline benchmarks for %s (%s samples)...\n' "{{.COMMIT_HASH}}" "{{.BENCH_COUNT}}"
|
||||
git worktree add --detach "$worktree_dir" "{{.COMMIT_HASH}}"
|
||||
base_tmp="$(mktemp "${base_file}.tmp.XXXXXX")"
|
||||
(cd "$worktree_dir" && env GOWORK=off go test -run '^$' -bench . -benchmem -count "{{.BENCH_COUNT}}" ./...) > "$base_tmp"
|
||||
mv "$base_tmp" "$base_file"
|
||||
base_tmp=""
|
||||
fi
|
||||
|
||||
printf 'Running current benchmarks (%s samples)...\n' "{{.BENCH_COUNT}}"
|
||||
new_tmp="$(mktemp "${new_file}.tmp.XXXXXX")"
|
||||
env GOWORK=off go test -run '^$' -bench . -benchmem -count "{{.BENCH_COUNT}}" ./... > "$new_tmp"
|
||||
mv "$new_tmp" "$new_file"
|
||||
new_tmp=""
|
||||
|
||||
printf 'Comparing benchmark results with benchstat...\n'
|
||||
benchstat "$base_file" "$new_file"
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type pooledSliceBuffers[T any] struct {
|
||||
sizes []int
|
||||
pools []sync.Pool
|
||||
}
|
||||
|
||||
func newPooledSliceBuffers[T any](sizes ...int) *pooledSliceBuffers[T] {
|
||||
sizes = slices.Clone(sizes)
|
||||
slices.Sort(sizes)
|
||||
return &pooledSliceBuffers[T]{
|
||||
sizes: sizes,
|
||||
pools: make([]sync.Pool, len(sizes)),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pooledSliceBuffers[T]) poolIndex(neededSize, maxSize int) int {
|
||||
if maxSize == 0 {
|
||||
return -1
|
||||
}
|
||||
for i, classSize := range p.sizes {
|
||||
if neededSize <= classSize {
|
||||
if maxSize > 0 && classSize > maxSize {
|
||||
return -1
|
||||
}
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (p *pooledSliceBuffers[T]) get(neededSize, maxSize int) ([]T, *[]T) {
|
||||
idx := p.poolIndex(neededSize, maxSize)
|
||||
if idx < 0 {
|
||||
return make([]T, neededSize), nil
|
||||
}
|
||||
if v := p.pools[idx].Get(); v != nil {
|
||||
bufp := v.(*[]T)
|
||||
if cap(*bufp) >= neededSize {
|
||||
return (*bufp)[:neededSize], bufp
|
||||
}
|
||||
}
|
||||
buf := make([]T, neededSize, p.sizes[idx])
|
||||
return buf, &buf
|
||||
}
|
||||
|
||||
func (p *pooledSliceBuffers[T]) put(bufp *[]T) {
|
||||
idx := p.poolIndex(cap(*bufp), -1)
|
||||
if idx < 0 || cap(*bufp) != p.sizes[idx] {
|
||||
return
|
||||
}
|
||||
*bufp = (*bufp)[:0]
|
||||
p.pools[idx].Put(bufp)
|
||||
}
|
||||
|
||||
// our specific pooled buffers
|
||||
var (
|
||||
pooledRuneBuffers = newPooledSliceBuffers[rune](1<<10, 4<<10, 16<<10, 64<<10, 256<<10)
|
||||
pooledByteBuffers = newPooledSliceBuffers[byte](4<<10, 16<<10, 64<<10, 256<<10, 1<<20)
|
||||
)
|
||||
|
||||
func getPooledReplaceBuffer(neededBytes, maxSize int) (*bytes.Buffer, *[]byte) {
|
||||
buf, pooled := pooledByteBuffers.get(neededBytes, maxSize)
|
||||
return bytes.NewBuffer(buf[:0]), pooled
|
||||
}
|
||||
|
||||
func putPooledReplaceBuffer(buf *bytes.Buffer, pooled *[]byte) {
|
||||
*pooled = buf.Bytes()
|
||||
pooledByteBuffers.put(pooled)
|
||||
}
|
||||
Generated
Vendored
+394
@@ -0,0 +1,394 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/dlclark/regexp2/v2/syntax"
|
||||
)
|
||||
|
||||
func IndexOfAny(in []rune, find []rune) int {
|
||||
// special case
|
||||
if len(find) == 0 {
|
||||
return -1
|
||||
}
|
||||
// naive version
|
||||
for i, c := range in {
|
||||
if slices.Contains(find, c) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAny1(in []rune, find rune) int {
|
||||
//TODO: bytes optimization?
|
||||
return slices.Index(in, find)
|
||||
}
|
||||
|
||||
func IndexOfAny2(in []rune, find1, find2 rune) int {
|
||||
for i, c := range in {
|
||||
if c == find1 || c == find2 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAny3(in []rune, find1, find2, find3 rune) int {
|
||||
for i, c := range in {
|
||||
if c == find1 || c == find2 || c == find3 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyInRange(in []rune, first, last rune) int {
|
||||
for i, c := range in {
|
||||
if c >= first && c <= last {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExcept(in []rune, bad []rune) int {
|
||||
for i, c := range in {
|
||||
found := false
|
||||
for _, b := range bad {
|
||||
if b == c {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExcept1(in []rune, bad rune) int {
|
||||
for i, c := range in {
|
||||
if c != bad {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExcept2(in []rune, bad1, bad2 rune) int {
|
||||
for i, c := range in {
|
||||
if c != bad1 && c != bad2 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExcept3(in []rune, bad1, bad2, bad3 rune) int {
|
||||
for i, c := range in {
|
||||
if c != bad1 && c != bad2 && c != bad3 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExceptInRange(in []rune, first, last rune) int {
|
||||
for i, c := range in {
|
||||
if c > last {
|
||||
return i
|
||||
}
|
||||
if c < first {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexFunc(in []rune, f func(ch rune) bool) int {
|
||||
for i := range in {
|
||||
if f(in[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfAnyExceptInSet(in []rune, set syntax.CharSet) int {
|
||||
//TODO: this
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func LastIndexOf(in []rune, find []rune) int {
|
||||
end := len(in) - len(find)
|
||||
first := find[0]
|
||||
lastOffset := len(find) - 1
|
||||
last := find[lastOffset]
|
||||
for i := end; i >= 0; i-- {
|
||||
//TODO: check 2 chars needed?
|
||||
// match start and end...check the middle
|
||||
if in[i] == first && in[i+lastOffset] == last {
|
||||
// found our first char
|
||||
// check if the rest are equal
|
||||
if bytesEqual(in[i:i+len(find)], find) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//not found
|
||||
return -1
|
||||
}
|
||||
|
||||
func LastIndexOfAnyExcept1(in []rune, not rune) int {
|
||||
for i := len(in) - 1; i >= 0; i-- {
|
||||
if in[i] != not {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func LastIndexOfAny1(in []rune, find rune) int {
|
||||
for i := len(in) - 1; i >= 0; i-- {
|
||||
if in[i] == find {
|
||||
// found our char
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
//not found
|
||||
return -1
|
||||
}
|
||||
|
||||
func LastIndexOfAnyInRange(in []rune, first, last rune) int {
|
||||
for i := len(in) - 1; i >= 0; i-- {
|
||||
if in[i] >= first && in[i] <= last {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
//TODO: LastIndexOf methods
|
||||
//IndexOfAnyInRange
|
||||
//LastIndexOfAnyInRange
|
||||
//LastIndexOfAnyExceptInRange
|
||||
|
||||
// find should always be sent in lower-case
|
||||
func IndexOfIgnoreCase(in []rune, find []rune) int {
|
||||
// search the in slice for the "find" slice, ignoring case in the comparisons
|
||||
end := len(in) - len(find)
|
||||
first := find[0]
|
||||
for i := 0; i <= end; i++ {
|
||||
if in[i] != first && unicode.ToLower(in[i]) != first {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for j := 1; j < len(find); j++ {
|
||||
inChar := in[i+j]
|
||||
if inChar != find[j] && unicode.ToLower(inChar) != find[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexOfIgnoreCaseAscii(in []rune, find []rune) int {
|
||||
// search the in slice for the "find" slice, ignoring case in the comparisons
|
||||
// we can assume the find chars are ascii and do simple masks on them
|
||||
if len(find) == 0 {
|
||||
return 0
|
||||
}
|
||||
end := len(in) - len(find)
|
||||
first := foldASCII(rune(find[0]))
|
||||
for i := 0; i <= end; i++ {
|
||||
if foldASCII(in[i]) != first {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for j := 1; j < len(find); j++ {
|
||||
if foldASCII(in[i+j]) != foldASCII(find[j]) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func IndexStringIgnoreCaseASCII(s, prefix string) int {
|
||||
if len(prefix) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
for start, end := 0, len(s)-len(prefix); start <= end; {
|
||||
offset := indexASCIIByteIgnoreCase(s[start:], prefix[0])
|
||||
if offset < 0 || start+offset > end {
|
||||
return -1
|
||||
}
|
||||
|
||||
i := start + offset
|
||||
if EqualStringIgnoreCaseASCII(s[i:i+len(prefix)], prefix) {
|
||||
return i
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func EqualStringIgnoreCaseASCII(s, prefix string) bool {
|
||||
if len(s) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(prefix); i++ {
|
||||
if foldASCII(rune(s[i])) != foldASCII(rune(prefix[i])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func indexASCIIByteIgnoreCase(s string, ch byte) int {
|
||||
ch = byte(foldASCII(rune(ch)))
|
||||
lower := strings.IndexByte(s, ch)
|
||||
if ch < 'a' || ch > 'z' {
|
||||
return lower
|
||||
}
|
||||
upper := strings.IndexByte(s, ch-('a'-'A'))
|
||||
if lower < 0 {
|
||||
return upper
|
||||
}
|
||||
if upper >= 0 && upper < lower {
|
||||
return upper
|
||||
}
|
||||
return lower
|
||||
}
|
||||
|
||||
func foldASCII(c rune) rune {
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
return c + ('a' - 'A')
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func IndexOf(in []rune, find []rune) int {
|
||||
/*
|
||||
Since we auto-gen the find code this shouldn't happen
|
||||
if len(find) == 0 {
|
||||
//special case
|
||||
return -1
|
||||
}*/
|
||||
end := len(in) - len(find)
|
||||
first := find[0]
|
||||
//TODO: benchmark checking last char too or first two chars
|
||||
for i := 0; i <= end; i++ {
|
||||
// match start...check the rest
|
||||
if in[i] == first {
|
||||
// found our first char
|
||||
// check if the rest are equal
|
||||
if bytesEqual(in[i:i+len(find)], find) {
|
||||
return i
|
||||
}
|
||||
/*if slices.Equal(in[i:i+len(find)], find) {
|
||||
return i
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
//not found
|
||||
return -1
|
||||
}
|
||||
|
||||
func StartsWith(in []rune, find []rune) bool {
|
||||
// if text is less than our "begin" then can't find it
|
||||
if len(in) < len(find) {
|
||||
return false
|
||||
}
|
||||
|
||||
return bytesEqual(in[:len(find)], find)
|
||||
|
||||
/*for i := 0; i < len(find); i++ {
|
||||
if in[i] != find[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true*/
|
||||
}
|
||||
|
||||
//StartsWithIgnoreCaseAscii would be faster
|
||||
|
||||
// find should always be sent in lower-case
|
||||
func StartsWithIgnoreCase(in []rune, find []rune) bool {
|
||||
// if text is less than our "begin" then can't find it
|
||||
if len(in) < len(find) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(find); i++ {
|
||||
if in[i] == find[i] {
|
||||
// if we match the char exactly then we're good
|
||||
continue
|
||||
}
|
||||
// if the to-lower still doesn't match then it's not a match
|
||||
if unicode.ToLower(in[i]) != find[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// internal function, assumes the bounds are already set right on the slices for equality
|
||||
// casts the rune slices to bytes to use framework fast []byte comparison
|
||||
func bytesEqual(a, b []rune) bool {
|
||||
bytesA := unsafe.Slice((*byte)(unsafe.Pointer(&a[0])), len(a)*4)
|
||||
bytesB := unsafe.Slice((*byte)(unsafe.Pointer(&b[0])), len(b)*4)
|
||||
return bytes.Equal(bytesA, bytesB)
|
||||
}
|
||||
|
||||
func Equals(in []rune, start int, length int, find []rune) bool {
|
||||
if len(find) == 0 {
|
||||
return true
|
||||
}
|
||||
return bytesEqual(in[start:start+length], find)
|
||||
}
|
||||
|
||||
func EqualsIgnoreCase(in []rune, start int, length int, find []rune) bool {
|
||||
//fast path if case matches
|
||||
if Equals(in, start, length, find) {
|
||||
return true
|
||||
}
|
||||
|
||||
// search the in slice for the "find" slice, ignoring case in the comparisons
|
||||
// we can't assume casing or ascii-ness for either letter, have to toLower them both
|
||||
for j := 0; j < len(find); j++ {
|
||||
inChar := in[start+j]
|
||||
findChar := find[j]
|
||||
if inChar != findChar && unicode.ToLower(inChar) != unicode.ToLower(findChar) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// we've checked all chars and found matches every time
|
||||
return true
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package helpers
|
||||
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package helpers
|
||||
|
||||
import "unicode"
|
||||
|
||||
func IsBetween(val rune, first, last rune) bool {
|
||||
if val > last {
|
||||
return false
|
||||
}
|
||||
if val >= first {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// According to UTS#18 Unicode Regular Expressions (http://www.unicode.org/reports/tr18/)
|
||||
// RL 1.4 Simple Word Boundaries The class of <word_character> includes all Alphabetic
|
||||
// values from the Unicode character database, from UnicodeData.txt [UData], plus the U+200C
|
||||
// ZERO WIDTH NON-JOINER and U+200D ZERO WIDTH JOINER.
|
||||
func IsWordChar(r rune) bool {
|
||||
// matches charclass.go
|
||||
|
||||
//TODO: add optimization here for ascii
|
||||
|
||||
//"L", "Mn", "Nd", "Pc"
|
||||
return unicode.In(r,
|
||||
unicode.Categories["L"], unicode.Categories["Mn"],
|
||||
unicode.Categories["Nd"], unicode.Categories["Pc"]) || r == '\u200D' || r == '\u200C'
|
||||
//return 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' || '0' <= r && r <= '9' || r == '_'
|
||||
}
|
||||
|
||||
func IsInMask32(ch rune, mask uint32) bool {
|
||||
//BDFHJLNPRTVX = 10101010 10101010 10101010 00000000
|
||||
//B = 00000000 00000000 00000000 01000010
|
||||
//char=B-B = 00000000 00000000 00000000 00000000
|
||||
//BDFH.. << 0 = 10101010 10101010 10101010 00000000
|
||||
//char-32 = 11111111 11111111 11111111 11100000
|
||||
//& = 10101010 10101010 10101010 00000000
|
||||
// first bit is 1 then negative, so it matches
|
||||
|
||||
//charMinusLowUInt32 := int32(ch - low)
|
||||
return int32((mask<<uint16(ch))&uint32(ch-32)) < 0
|
||||
}
|
||||
|
||||
func IsInMask64(ch rune, mask uint64) bool {
|
||||
//64-bit version of the above
|
||||
//charMinusLowUInt64 := int64(ch - low)
|
||||
return int64((mask<<uint32(ch))&uint64(ch-64)) < 0
|
||||
}
|
||||
|
||||
func IsInASCIIBitmap(ch rune, lo uint64, hi uint64) bool {
|
||||
if ch < 64 {
|
||||
return lo&(1<<uint(ch)) != 0
|
||||
}
|
||||
if ch < 128 {
|
||||
return hi&(1<<uint(ch-64)) != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type AsciiSearchValues struct {
|
||||
// each ascii byte is represented by a bit in this array
|
||||
// there are 128bits here and ascii has 128 possible chars
|
||||
set [2]uint64
|
||||
}
|
||||
|
||||
func NewAsciiSearchValues(vals string) AsciiSearchValues {
|
||||
// pre-calc ascii table stuff to make this go faster
|
||||
sv := AsciiSearchValues{}
|
||||
for i := 0; i < len(vals); i++ {
|
||||
c := vals[i]
|
||||
if c > unicode.MaxASCII {
|
||||
// a bug got us here. that's bad.
|
||||
panic(fmt.Errorf("non-ascii value found in ascii search values: %s", vals))
|
||||
}
|
||||
idx := c / 64
|
||||
shift := c % 64
|
||||
sv.set[idx] |= 1 << shift
|
||||
}
|
||||
|
||||
return sv
|
||||
}
|
||||
|
||||
// return the first index of our original vals values within the slice given
|
||||
func (s AsciiSearchValues) IndexOfAny(chars []rune) int {
|
||||
for i := 0; i < len(chars); i++ {
|
||||
c := chars[i]
|
||||
if c > unicode.MaxASCII {
|
||||
continue
|
||||
}
|
||||
idx := c / 64
|
||||
shift := c % 64
|
||||
if s.set[idx]&(1<<shift) != 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// return the first index of anything except our original vals values within the slice given
|
||||
func (s AsciiSearchValues) IndexOfAnyExcept(chars []rune) int {
|
||||
for i := 0; i < len(chars); i++ {
|
||||
c := chars[i]
|
||||
if c > unicode.MaxASCII {
|
||||
return i
|
||||
}
|
||||
idx := c / 64
|
||||
shift := c % 64
|
||||
if s.set[idx]&(1<<shift) == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// return the last index of our original vals values within the slice given
|
||||
func (s AsciiSearchValues) LastIndexOfAny(chars []rune) int {
|
||||
panic("not implemented")
|
||||
//TODO: this
|
||||
}
|
||||
|
||||
// return the last index of our original vals values within the slice given
|
||||
func (s AsciiSearchValues) LastIndexOfAnyExcept(chars []rune) int {
|
||||
panic("not implemented")
|
||||
//TODO: this
|
||||
}
|
||||
|
||||
type RuneSearchValues struct {
|
||||
vals []rune
|
||||
}
|
||||
|
||||
func newRuneSearchValues(vals []rune) RuneSearchValues {
|
||||
//TODO: pre-calc the stuff we need to make each IndexOf go faster
|
||||
return RuneSearchValues{vals: vals}
|
||||
|
||||
}
|
||||
|
||||
func NewRuneSearchValues(vals string) RuneSearchValues {
|
||||
return newRuneSearchValues([]rune(vals))
|
||||
}
|
||||
|
||||
// return the first index of our original vals values within the slice given
|
||||
func (s RuneSearchValues) IndexOfAny(chars []rune) int {
|
||||
//naive implementation
|
||||
//TODO: this
|
||||
return IndexOfAny(chars, s.vals)
|
||||
}
|
||||
|
||||
// return the first index of our original vals values within the slice given
|
||||
func (s RuneSearchValues) IndexOfAnyExcept(chars []rune) int {
|
||||
//TODO: this
|
||||
return IndexOfAnyExcept(chars, s.vals)
|
||||
}
|
||||
|
||||
// return the last index of our original vals values within the slice given
|
||||
func (s RuneSearchValues) LastIndexOfAny(chars []rune) int {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// return the last index of our original vals values within the slice given
|
||||
func (s RuneSearchValues) LastIndexOfAnyExcept(chars []rune) int {
|
||||
panic("not implemented")
|
||||
//TODO: this
|
||||
}
|
||||
|
||||
type StringSearchValues struct {
|
||||
vals [][]rune
|
||||
ignoreCase bool
|
||||
shortestVal int
|
||||
|
||||
firstChars RuneSearchValues
|
||||
}
|
||||
|
||||
func NewStringSearchValues(vals [][]rune, ignoreCase bool) StringSearchValues {
|
||||
min := math.MaxInt
|
||||
firstLetters := make([]rune, 0, len(vals))
|
||||
for _, val := range vals {
|
||||
if min > len(val) {
|
||||
min = len(val)
|
||||
}
|
||||
if !slices.Contains(firstLetters, val[0]) {
|
||||
firstLetters = append(firstLetters, val[0])
|
||||
if ignoreCase && val[0] != unicode.ToUpper(val[0]) {
|
||||
//if we're ignoring case and this letter is impacted by case, add it to our set
|
||||
firstLetters = append(firstLetters, unicode.ToUpper(val[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return StringSearchValues{
|
||||
vals: vals,
|
||||
ignoreCase: ignoreCase,
|
||||
shortestVal: min,
|
||||
firstChars: newRuneSearchValues(firstLetters),
|
||||
}
|
||||
}
|
||||
|
||||
func (s StringSearchValues) StartsWith(chars []rune) int {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (s StringSearchValues) StartsWithIgnoreCase(chars []rune) int {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (s StringSearchValues) IndexOfAny(in []rune) int {
|
||||
// go through our input once
|
||||
end := len(in) - s.shortestVal
|
||||
for i := 0; i <= end; i++ {
|
||||
// check if the char are in our starting chars
|
||||
j := s.firstChars.IndexOfAny(in[i:])
|
||||
// first chars not found at all
|
||||
if j < 0 {
|
||||
return -1
|
||||
}
|
||||
j += i
|
||||
// found a first char, do our full search through each item
|
||||
for _, val := range s.vals {
|
||||
if len(in)-j >= len(val) && Equals(in, j, len(val), val) {
|
||||
return j
|
||||
}
|
||||
if s.ignoreCase && len(in)-j >= len(val) && EqualsIgnoreCase(in, j, len(val), val) {
|
||||
return j
|
||||
}
|
||||
}
|
||||
//skip ahead
|
||||
i = j
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
Generated
Vendored
+132
-26
@@ -3,6 +3,7 @@ package regexp2
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Match is a single regex result match that contains groups and repeated captures
|
||||
@@ -20,7 +21,6 @@ type Match struct {
|
||||
textstart int
|
||||
|
||||
capcount int
|
||||
caps []int
|
||||
sparseCaps map[int]int
|
||||
|
||||
// output from the match
|
||||
@@ -43,25 +43,124 @@ type Group struct {
|
||||
// Capture is a single capture of text within the larger original string
|
||||
type Capture struct {
|
||||
// the original string
|
||||
text []rune
|
||||
// Index is the position in the underlying rune slice where the first character of
|
||||
text *matchText
|
||||
// RuneIndex is the position in the underlying rune slice where the first character of
|
||||
// captured substring was found. Even if you pass in a string this will be in Runes.
|
||||
Index int
|
||||
// Length is the number of runes in the captured substring.
|
||||
Length int
|
||||
RuneIndex int
|
||||
// RuneLength is the number of runes in the captured substring.
|
||||
RuneLength int
|
||||
}
|
||||
|
||||
type matchText struct {
|
||||
runes []rune
|
||||
input string
|
||||
hasStringInput bool
|
||||
byteOffsets []int
|
||||
byteOffsetsReady bool
|
||||
}
|
||||
|
||||
// String returns the captured text as a String
|
||||
func (c *Capture) String() string {
|
||||
return string(c.text[c.Index : c.Index+c.Length])
|
||||
return string(c.text.runes[c.RuneIndex : c.RuneIndex+c.RuneLength])
|
||||
}
|
||||
|
||||
// Runes returns the captured text as a rune slice
|
||||
func (c *Capture) Runes() []rune {
|
||||
return c.text[c.Index : c.Index+c.Length]
|
||||
return c.text.runes[c.RuneIndex : c.RuneIndex+c.RuneLength]
|
||||
}
|
||||
|
||||
func newMatch(regex *Regexp, capcount int, text []rune, startpos int) *Match {
|
||||
// ByteRange returns the UTF-8 byte index and byte length of the captured
|
||||
// substring. The first call lazily caches byte offsets on shared match text,
|
||||
// so it is not safe to call concurrently with ByteRange on another capture
|
||||
// from the same match until the cache has been initialized.
|
||||
func (c *Capture) ByteRange() (index, length int) {
|
||||
if c.text == nil {
|
||||
return c.RuneIndex, c.RuneLength
|
||||
}
|
||||
return c.text.byteRange(c.RuneIndex, c.RuneLength)
|
||||
}
|
||||
|
||||
func newMatchText(r []rune) *matchText {
|
||||
return &matchText{runes: r}
|
||||
}
|
||||
|
||||
func newStringMatchText(input string, r []rune) *matchText {
|
||||
return &matchText{runes: r, input: input, hasStringInput: true}
|
||||
}
|
||||
|
||||
func (t *matchText) byteRange(runeIndex, runeLength int) (int, int) {
|
||||
if !t.byteOffsetsReady {
|
||||
t.byteOffsets = t.buildByteOffsets()
|
||||
t.byteOffsetsReady = true
|
||||
}
|
||||
if t.byteOffsets == nil {
|
||||
return runeIndex, runeLength
|
||||
}
|
||||
byteIndex := t.byteOffsets[runeIndex]
|
||||
return byteIndex, t.byteOffsets[runeIndex+runeLength] - byteIndex
|
||||
}
|
||||
|
||||
func (t *matchText) buildByteOffsets() []int {
|
||||
if t.hasStringInput {
|
||||
return stringByteOffsets(t.input)
|
||||
}
|
||||
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++
|
||||
}
|
||||
if byteOffsets != nil {
|
||||
byteOffsets[runeIndex] = len(s)
|
||||
return byteOffsets[:runeIndex+1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runeByteOffsets(runes []rune) []int {
|
||||
var byteOffsets []int
|
||||
bytePos := 0
|
||||
for i, ch := range runes {
|
||||
if byteOffsets != nil {
|
||||
byteOffsets[i] = bytePos
|
||||
}
|
||||
runeLen := utf8.RuneLen(ch)
|
||||
if runeLen < 0 {
|
||||
runeLen = utf8.RuneLen(utf8.RuneError)
|
||||
}
|
||||
if byteOffsets == nil && runeLen != 1 {
|
||||
byteOffsets = make([]int, len(runes)+1)
|
||||
for j := 0; j < i; j++ {
|
||||
byteOffsets[j] = j
|
||||
}
|
||||
byteOffsets[i] = bytePos
|
||||
}
|
||||
bytePos += runeLen
|
||||
}
|
||||
if byteOffsets != nil {
|
||||
byteOffsets[len(runes)] = bytePos
|
||||
}
|
||||
return byteOffsets
|
||||
}
|
||||
|
||||
func newMatch(regex *Regexp, capcount int, text *matchText, startpos int) *Match {
|
||||
m := Match{
|
||||
regex: regex,
|
||||
matchcount: make([]int, capcount),
|
||||
@@ -69,19 +168,21 @@ func newMatch(regex *Regexp, capcount int, text []rune, startpos int) *Match {
|
||||
textstart: startpos,
|
||||
balancing: false,
|
||||
}
|
||||
m.Name = "0"
|
||||
if (regex.options & ECMAScript) == 0 {
|
||||
m.Name = "0"
|
||||
}
|
||||
m.text = text
|
||||
m.matches[0] = make([]int, 2)
|
||||
return &m
|
||||
}
|
||||
|
||||
func newMatchSparse(regex *Regexp, caps map[int]int, capcount int, text []rune, startpos int) *Match {
|
||||
func newMatchSparse(regex *Regexp, caps map[int]int, capcount int, text *matchText, startpos int) *Match {
|
||||
m := newMatch(regex, capcount, text, startpos)
|
||||
m.sparseCaps = caps
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Match) reset(text []rune, textstart int) {
|
||||
func (m *Match) reset(text *matchText, textstart int) {
|
||||
m.text = text
|
||||
m.textstart = textstart
|
||||
for i := 0; i < len(m.matchcount); i++ {
|
||||
@@ -93,12 +194,11 @@ func (m *Match) reset(text []rune, textstart int) {
|
||||
func (m *Match) tidy(textpos int) {
|
||||
|
||||
interval := m.matches[0]
|
||||
m.Index = interval[0]
|
||||
m.Length = interval[1]
|
||||
setCaptureFields(&m.Capture, interval[0], interval[1])
|
||||
m.textpos = textpos
|
||||
m.capcount = m.matchcount[0]
|
||||
//copy our root capture to the list
|
||||
m.Group.Captures = []Capture{m.Group.Capture}
|
||||
m.Captures = []Capture{m.Capture}
|
||||
|
||||
if m.balancing {
|
||||
// The idea here is that we want to compact all of our unbalanced captures. To do that we
|
||||
@@ -288,31 +388,37 @@ func (m *Match) groupValueAppendToBuf(groupnum int, buf *bytes.Buffer) {
|
||||
last := index + matches[(c*2)-1]
|
||||
|
||||
for ; index < last; index++ {
|
||||
buf.WriteRune(m.text[index])
|
||||
buf.WriteRune(m.text.runes[index])
|
||||
}
|
||||
}
|
||||
|
||||
func newGroup(name string, text []rune, caps []int, capcount int) Group {
|
||||
func newGroup(name string, text *matchText, caps []int, capcount int) Group {
|
||||
g := Group{}
|
||||
g.text = text
|
||||
if capcount > 0 {
|
||||
g.Index = caps[(capcount-1)*2]
|
||||
g.Length = caps[(capcount*2)-1]
|
||||
setCaptureFields(&g.Capture, caps[(capcount-1)*2], caps[(capcount*2)-1])
|
||||
}
|
||||
g.Name = name
|
||||
g.Captures = make([]Capture, capcount)
|
||||
for i := 0; i < capcount; i++ {
|
||||
g.Captures[i] = Capture{
|
||||
text: text,
|
||||
Index: caps[i*2],
|
||||
Length: caps[i*2+1],
|
||||
}
|
||||
g.Captures[i] = newCapture(text, caps[i*2], caps[i*2+1])
|
||||
}
|
||||
//log.Printf("newGroup! capcount %v, %+v", capcount, g)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func newCapture(text *matchText, runeIndex, runeLength int) Capture {
|
||||
c := Capture{text: text}
|
||||
setCaptureFields(&c, runeIndex, runeLength)
|
||||
return c
|
||||
}
|
||||
|
||||
func setCaptureFields(c *Capture, runeIndex, runeLength int) {
|
||||
c.RuneIndex = runeIndex
|
||||
c.RuneLength = runeLength
|
||||
}
|
||||
|
||||
func (m *Match) dump() string {
|
||||
buf := &bytes.Buffer{}
|
||||
buf.WriteRune('\n')
|
||||
@@ -326,7 +432,7 @@ func (m *Match) dump() string {
|
||||
fmt.Fprintf(buf, "Group %v (%v), %v caps:\n", i, g.Name, len(g.Captures))
|
||||
|
||||
for _, c := range g.Captures {
|
||||
fmt.Fprintf(buf, " (%v, %v) %v\n", c.Index, c.Length, c.String())
|
||||
fmt.Fprintf(buf, " (%v, %v) %v\n", c.RuneIndex, c.RuneLength, c.String())
|
||||
}
|
||||
}
|
||||
/*
|
||||
@@ -338,7 +444,7 @@ func (m *Match) dump() string {
|
||||
|
||||
if m.matches[i][j*2] >= 0 {
|
||||
start := m.matches[i][j*2]
|
||||
text = m.text[start : start+m.matches[i][j*2+1]]
|
||||
text = m.text.runes[start : start+m.matches[i][j*2+1]]
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, " (%v, %v) %v\n", m.matches[i][j*2], m.matches[i][j*2+1], text)
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package regexp2
|
||||
|
||||
var (
|
||||
// DefaultUnmarshalOptions used when unmarshaling a regex from text
|
||||
DefaultUnmarshalOptions = None
|
||||
// DefaultOptimizationOptions controls the default memory/performance trade-offs used by Compile.
|
||||
DefaultOptimizationOptions = OptimizationOptions{
|
||||
MaxCachedRuneBufferLength: 256 << 10,
|
||||
MaxCachedReplaceBufferLength: 256 << 10,
|
||||
MaxCachedReplacerDataEntries: 16,
|
||||
MaxCachedReplacerDataBytes: 4 << 10,
|
||||
DisableCharClassASCIIBitmap: false,
|
||||
}
|
||||
)
|
||||
|
||||
// RegexOptions impact the runtime and parsing behavior
|
||||
// for each specific regex. They are setable in code as well
|
||||
// as in the regex pattern itself.
|
||||
type RegexOptions int32
|
||||
|
||||
func (o RegexOptions) applyCompileOption(c *compileConfig) {
|
||||
c.regexOptions |= o
|
||||
}
|
||||
|
||||
const (
|
||||
None RegexOptions = 0x0
|
||||
IgnoreCase RegexOptions = 0x0001 // "i"
|
||||
Multiline RegexOptions = 0x0002 // "m"
|
||||
ExplicitCapture RegexOptions = 0x0004 // "n"
|
||||
Singleline RegexOptions = 0x0010 // "s"
|
||||
IgnorePatternWhitespace RegexOptions = 0x0020 // "x"
|
||||
RightToLeft RegexOptions = 0x0040 // "r"
|
||||
// ECMAScript attempts to follow ECMAScript regex behavior rather than C# RegexOptions.ECMAScript compatibility.
|
||||
ECMAScript RegexOptions = 0x0100 // "e"
|
||||
RE2 RegexOptions = 0x0200 // RE2 (regexp package) compatibility mode
|
||||
Unicode RegexOptions = 0x0400 // "u"
|
||||
)
|
||||
|
||||
// OptimizationOptions controls optional runtime caches and compile-time fast paths.
|
||||
//
|
||||
// For replacement data cache size fields, 0 disables persistent retention and
|
||||
// -1 means unbounded. For pooled buffer cache size fields, 0 disables pooling
|
||||
// and -1 allows all built-in size classes.
|
||||
// Defaults are intentionally bounded so Compile is safe for mixed-cardinality inputs.
|
||||
type OptimizationOptions struct {
|
||||
// MaxCachedRuneBufferLength limits retained string-to-rune buffers in the shared size-classed pool.
|
||||
MaxCachedRuneBufferLength int
|
||||
// MaxCachedReplaceBufferLength limits retained replacement output buffers in the shared size-classed pool.
|
||||
MaxCachedReplaceBufferLength int
|
||||
// MaxCachedReplacerDataEntries limits the number of parsed replacement patterns cached per Regexp.
|
||||
MaxCachedReplacerDataEntries int
|
||||
// MaxCachedReplacerDataBytes skips caching replacement patterns longer than this many bytes.
|
||||
MaxCachedReplacerDataBytes int
|
||||
// DisableCharClassASCIIBitmap disables compile-time ASCII bitmap construction for character classes.
|
||||
DisableCharClassASCIIBitmap bool
|
||||
}
|
||||
|
||||
// CompileOption configures Compile and MustCompile.
|
||||
type CompileOption interface {
|
||||
applyCompileOption(*compileConfig)
|
||||
}
|
||||
|
||||
type compileConfig struct {
|
||||
regexOptions RegexOptions
|
||||
optimizations OptimizationOptions
|
||||
codeGen bool
|
||||
debug bool
|
||||
maintainCaptureOrder bool
|
||||
}
|
||||
|
||||
type compileOptionFunc func(*compileConfig)
|
||||
|
||||
func (f compileOptionFunc) applyCompileOption(c *compileConfig) {
|
||||
f(c)
|
||||
}
|
||||
|
||||
func (o OptimizationOptions) cacheReplacerData(replacement string) bool {
|
||||
if o.MaxCachedReplacerDataEntries == 0 {
|
||||
return false
|
||||
}
|
||||
return keepCacheBytes(o.MaxCachedReplacerDataBytes, len(replacement))
|
||||
}
|
||||
|
||||
func keepCacheBytes(maxBytes, actualBytes int) bool {
|
||||
if maxBytes < 0 {
|
||||
return true
|
||||
}
|
||||
return maxBytes > 0 && actualBytes <= maxBytes
|
||||
}
|
||||
|
||||
func newCompileConfig(options []CompileOption) compileConfig {
|
||||
c := compileConfig{
|
||||
optimizations: DefaultOptimizationOptions,
|
||||
}
|
||||
for _, option := range options {
|
||||
if option != nil {
|
||||
option.applyCompileOption(&c)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// OptionMaxCachedRuneBufferLength limits retained string-to-rune buffers in the shared size-classed pool.
|
||||
func OptionMaxCachedRuneBufferLength(n int) CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.optimizations.MaxCachedRuneBufferLength = n
|
||||
})
|
||||
}
|
||||
|
||||
// OptionMaxCachedReplaceBufferLength limits retained replacement output buffers in the shared size-classed pool.
|
||||
func OptionMaxCachedReplaceBufferLength(n int) CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.optimizations.MaxCachedReplaceBufferLength = n
|
||||
})
|
||||
}
|
||||
|
||||
// OptionMaxCachedReplacerDataEntries limits parsed replacement patterns cached per Regexp.
|
||||
func OptionMaxCachedReplacerDataEntries(n int) CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.optimizations.MaxCachedReplacerDataEntries = n
|
||||
})
|
||||
}
|
||||
|
||||
// OptionMaxCachedReplacerDataBytes skips caching replacement patterns longer than n bytes.
|
||||
func OptionMaxCachedReplacerDataBytes(n int) CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.optimizations.MaxCachedReplacerDataBytes = n
|
||||
})
|
||||
}
|
||||
|
||||
// OptionDisableCharClassASCIIBitmap disables compile-time ASCII bitmaps for character classes.
|
||||
func OptionDisableCharClassASCIIBitmap() CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.optimizations.DisableCharClassASCIIBitmap = true
|
||||
})
|
||||
}
|
||||
|
||||
// OptionIsCodeGen enables more expensive compile-time analysis intended for regexp2cg generated engines.
|
||||
func OptionIsCodeGen() CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.codeGen = true
|
||||
})
|
||||
}
|
||||
|
||||
// OptionDebug enables debug output and runner tracing for the compiled regexp.
|
||||
func OptionDebug() CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.debug = true
|
||||
})
|
||||
}
|
||||
|
||||
// OptionMaintainCaptureOrder assigns named and unnamed capture slots in pattern order.
|
||||
func OptionMaintainCaptureOrder() CompileOption {
|
||||
return compileOptionFunc(func(c *compileConfig) {
|
||||
c.maintainCaptureOrder = true
|
||||
})
|
||||
}
|
||||
+717
@@ -0,0 +1,717 @@
|
||||
/*
|
||||
Package regexp2 is a regexp package that has an interface similar to Go's framework regexp engine but uses a
|
||||
more feature full regex engine behind the scenes.
|
||||
|
||||
It doesn't have constant time guarantees, but it allows backtracking and is compatible with Perl5 and .NET.
|
||||
You'll likely be better off with the RE2 engine from the regexp package and should only use this if you
|
||||
need to write very complex patterns or require compatibility with .NET.
|
||||
*/
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dlclark/regexp2/v2/syntax"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultMatchTimeout used when running regexp matches -- "forever"
|
||||
DefaultMatchTimeout = time.Duration(math.MaxInt64)
|
||||
)
|
||||
|
||||
// Regexp is the representation of a compiled regular expression.
|
||||
// A Regexp is safe for concurrent use by multiple goroutines.
|
||||
type Regexp struct {
|
||||
// A match will time out if it takes (approximately) more than
|
||||
// MatchTimeout. This is a safety check in case the match
|
||||
// encounters catastrophic backtracking. The default value
|
||||
// (DefaultMatchTimeout) causes all time out checking to be
|
||||
// suppressed.
|
||||
MatchTimeout time.Duration
|
||||
|
||||
// read-only after Compile
|
||||
pattern string // as passed to Compile
|
||||
options RegexOptions // options
|
||||
debug bool
|
||||
|
||||
caps map[int]int // capnum->index
|
||||
capnames map[string]int //capture group name -> index
|
||||
capslist []string //sorted list of capture group names
|
||||
capsize int // size of the capture array
|
||||
|
||||
code *syntax.Code // compiled program
|
||||
|
||||
optimizations OptimizationOptions
|
||||
|
||||
// cache of machines for running regexp
|
||||
runnerPool *sync.Pool
|
||||
|
||||
replaceCache *replacerDataCache
|
||||
|
||||
// hook points to override runner functions
|
||||
findFirstChar func(r *Runner) bool
|
||||
execute func(r *Runner) error
|
||||
stringPrefixFilter StringPrefixFilter
|
||||
}
|
||||
|
||||
// Compile parses a regular expression and returns, if successful,
|
||||
// a Regexp object that can be used to match against text.
|
||||
func Compile(expr string, options ...CompileOption) (*Regexp, error) {
|
||||
c := newCompileConfig(options)
|
||||
return compile(expr, c)
|
||||
}
|
||||
|
||||
func compile(expr string, c compileConfig) (*Regexp, error) {
|
||||
// parse it
|
||||
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())
|
||||
}
|
||||
|
||||
// translate it to code
|
||||
code, err := syntax.Write(tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.debug {
|
||||
log.Print(code.Dump())
|
||||
}
|
||||
if !c.optimizations.DisableCharClassASCIIBitmap {
|
||||
code.PrepareCharSetASCIIBitmaps()
|
||||
}
|
||||
|
||||
// return it
|
||||
re := &Regexp{
|
||||
pattern: expr,
|
||||
options: c.regexOptions,
|
||||
debug: c.debug,
|
||||
caps: code.Caps,
|
||||
capnames: tree.Capnames,
|
||||
capslist: tree.Caplist,
|
||||
capsize: code.Capsize,
|
||||
code: code,
|
||||
MatchTimeout: DefaultMatchTimeout,
|
||||
optimizations: c.optimizations,
|
||||
}
|
||||
re.stringPrefixFilter = newStringPrefixFilter(code)
|
||||
re.initCaches()
|
||||
return re, nil
|
||||
}
|
||||
|
||||
// MustCompile is like Compile but panics if the expression cannot be parsed.
|
||||
// It simplifies safe initialization of global variables holding compiled regular
|
||||
// expressions.
|
||||
func MustCompile(str string, options ...CompileOption) *Regexp {
|
||||
c := newCompileConfig(options)
|
||||
|
||||
// lookup if we have a pre-built state machine for this pattern and 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
|
||||
}
|
||||
|
||||
// Escape adds backslashes to any special characters in the input string
|
||||
func Escape(input string) string {
|
||||
return syntax.Escape(input)
|
||||
}
|
||||
|
||||
// Unescape removes any backslashes from previously-escaped special characters in the input string
|
||||
func Unescape(input string) (string, error) {
|
||||
return syntax.Unescape(input)
|
||||
}
|
||||
|
||||
// SetTimeoutPeriod is a debug function that sets the frequency of the timeout goroutine's sleep cycle.
|
||||
// Defaults to 100ms. The only benefit of setting this lower is that the 1 background goroutine that manages
|
||||
// timeouts may exit slightly sooner after all the timeouts have expired. See Github issue #63
|
||||
func SetTimeoutCheckPeriod(d time.Duration) {
|
||||
clockPeriod = d
|
||||
}
|
||||
|
||||
// StopTimeoutClock should only be used in unit tests to prevent the timeout clock goroutine
|
||||
// from appearing like a leaking goroutine
|
||||
func StopTimeoutClock() {
|
||||
stopClock()
|
||||
}
|
||||
|
||||
// String returns the source text used to compile the regular expression.
|
||||
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
|
||||
}
|
||||
|
||||
// Replace searches the input string and replaces each match found with the replacement text.
|
||||
// Count will limit the number of matches attempted and startAt will allow
|
||||
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
||||
// Set startAt and count to -1 to go through the whole string
|
||||
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
|
||||
}
|
||||
|
||||
// ReplaceFunc searches the input string and replaces each match found using the string from the evaluator
|
||||
// Count will limit the number of matches attempted and startAt will allow
|
||||
// us to skip past possible matches at the start of the input (left or right depending on RightToLeft option).
|
||||
// Set startAt and count to -1 to go through the whole string.
|
||||
func (re *Regexp) ReplaceFunc(input string, evaluator MatchEvaluator, startAt, count int) (string, error) {
|
||||
return replace(re, nil, evaluator, input, startAt, count)
|
||||
}
|
||||
|
||||
// FindStringMatch searches the input string for a Regexp match
|
||||
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, r, newStringMatchText(s, r))
|
||||
}
|
||||
|
||||
// FindRunesMatch searches the input rune slice for a Regexp match
|
||||
func (re *Regexp) FindRunesMatch(r []rune) (*Match, error) {
|
||||
return re.run(false, -1, r, newMatchText(r))
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r, startAt := re.getRunesAndStart(s, startAt)
|
||||
if startAt == -1 {
|
||||
// we didn't find our start index in the string -- that's a problem
|
||||
return nil, errors.New("startAt must align to the start of a valid rune in the input string")
|
||||
}
|
||||
|
||||
return re.run(false, startAt, r, newStringMatchText(s, r))
|
||||
}
|
||||
|
||||
// FindRunesMatchStartingAt searches the input rune slice for a Regexp match starting at the startAt index
|
||||
func (re *Regexp) FindRunesMatchStartingAt(r []rune, startAt int) (*Match, error) {
|
||||
return re.run(false, startAt, r, newMatchText(r))
|
||||
}
|
||||
|
||||
// FindAllStringIndex returns a slice of byte index pairs identifying all
|
||||
// successive matches in s.
|
||||
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)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// FindAllRunesIndex returns a slice of rune index pairs identifying all
|
||||
// successive matches in r.
|
||||
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)
|
||||
}
|
||||
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
|
||||
for n != 0 {
|
||||
m, err := runner.scan(input, nil, startAt, 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
|
||||
if m.RuneLength == 0 {
|
||||
if re.RightToLeft() {
|
||||
if m.textpos == 0 {
|
||||
break
|
||||
}
|
||||
if startAt == m.textstart {
|
||||
startAt--
|
||||
}
|
||||
} else {
|
||||
if m.textpos == len(input) {
|
||||
break
|
||||
}
|
||||
if startAt == m.textstart {
|
||||
startAt++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
||||
// FindNextMatch returns the next match in the same input string as the match parameter.
|
||||
// Will return nil if there is no next match or if given a nil match.
|
||||
func (re *Regexp) FindNextMatch(m *Match) (*Match, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If previous match was empty, advance by one before matching to prevent
|
||||
// infinite loop
|
||||
startAt := m.textpos
|
||||
if m.RuneLength == 0 {
|
||||
if re.RightToLeft() {
|
||||
if m.textpos == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if startAt == m.textstart {
|
||||
startAt--
|
||||
}
|
||||
} else {
|
||||
if m.textpos == len(m.text.runes) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if startAt == m.textstart {
|
||||
startAt++
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return re.run(false, startAt, m.text.runes, m.text)
|
||||
}
|
||||
|
||||
// MatchString return true if the string matches the regex
|
||||
// error will be set if a timeout occurs
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := runner.scan(input, nil, runeStart, 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)
|
||||
}
|
||||
|
||||
// MatchRunes return true if the runes matches the regex
|
||||
// error will be set if a timeout occurs
|
||||
func (re *Regexp) MatchRunes(r []rune) (bool, error) {
|
||||
m, err := re.run(true, -1, r, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return m != nil, nil
|
||||
}
|
||||
|
||||
// GetGroupNames Returns the set of strings used to name capturing groups in the expression.
|
||||
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
|
||||
}
|
||||
|
||||
// GetGroupNumbers returns the integer group numbers corresponding to a group name.
|
||||
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
|
||||
}
|
||||
|
||||
// GroupNameFromNumber retrieves a group name that corresponds to a group number.
|
||||
// It will return "" for an unknown group number. Unnamed groups automatically
|
||||
// receive a name that is the decimal string equivalent of its number, except in
|
||||
// ECMAScript mode where unnamed groups have no name.
|
||||
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 ""
|
||||
}
|
||||
|
||||
// GroupNumberFromName returns a group number that corresponds to a group name.
|
||||
// Returns -1 if the name is not a recognized group name. Numbered groups
|
||||
// automatically get a group name that is the decimal string equivalent of its
|
||||
// number, except in ECMAScript mode where unnamed groups have no name.
|
||||
func (re *Regexp) GroupNumberFromName(name string) int {
|
||||
// look up name if we have a hashtable of names
|
||||
if re.capnames != nil {
|
||||
if k, ok := re.capnames[name]; ok {
|
||||
return k
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// convert to an int if it looks like a number
|
||||
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')
|
||||
}
|
||||
|
||||
// return int if it's in range
|
||||
if result >= 0 && result < re.capsize {
|
||||
return result
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// MarshalText implements [encoding.TextMarshaler]. The output
|
||||
// matches that of calling the [Regexp.String] method.
|
||||
func (re *Regexp) MarshalText() ([]byte, error) {
|
||||
return []byte(re.String()), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements [encoding.TextUnmarshaler] by calling
|
||||
// [Compile] on the encoded value.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RuntimeEngineData struct {
|
||||
Caps map[int]int // capnum->index
|
||||
CapNames map[string]int // cap group name -> index
|
||||
CapsList []string // sorted list of capture group names
|
||||
CapSize int // size of the capture array
|
||||
FindFirstChar func(*Runner) bool // generated candidate search
|
||||
Execute func(*Runner) error
|
||||
StringPrefixFilter StringPrefixFilter // optional pre-decode candidate search for string input
|
||||
}
|
||||
|
||||
type cacheKey struct {
|
||||
pattern string
|
||||
opt RegexOptions
|
||||
maintainCaptureOrder bool
|
||||
}
|
||||
|
||||
func RegisterEngine(pattern string, engine RuntimeEngineData, options ...CompileOption) {
|
||||
c := newCompileConfig(options)
|
||||
enginesMu.Lock()
|
||||
engines[cacheKeyFromConfig(pattern, c)] = engine
|
||||
enginesMu.Unlock()
|
||||
}
|
||||
|
||||
func newEngineRegexp(pattern string, c compileConfig, engine RuntimeEngineData) *Regexp {
|
||||
re := &Regexp{
|
||||
pattern: pattern,
|
||||
options: c.regexOptions,
|
||||
debug: c.debug,
|
||||
caps: engine.Caps,
|
||||
capnames: engine.CapNames,
|
||||
capslist: engine.CapsList,
|
||||
capsize: engine.CapSize,
|
||||
MatchTimeout: DefaultMatchTimeout,
|
||||
optimizations: c.optimizations,
|
||||
findFirstChar: engine.FindFirstChar,
|
||||
execute: engine.Execute,
|
||||
stringPrefixFilter: engine.StringPrefixFilter,
|
||||
}
|
||||
re.initCaches()
|
||||
return re
|
||||
}
|
||||
|
||||
func getEngineRegexp(pattern string, c compileConfig) *Regexp {
|
||||
enginesMu.RLock()
|
||||
engine, ok := engines[cacheKeyFromConfig(pattern, c)]
|
||||
enginesMu.RUnlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return newEngineRegexp(pattern, c, engine)
|
||||
}
|
||||
|
||||
func cacheKeyFromConfig(pattern string, c compileConfig) cacheKey {
|
||||
return cacheKey{
|
||||
pattern: pattern,
|
||||
opt: c.regexOptions,
|
||||
maintainCaptureOrder: c.maintainCaptureOrder,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
enginesMu sync.RWMutex
|
||||
engines = map[cacheKey]RuntimeEngineData{}
|
||||
)
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
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{}
|
||||
text := m.text.runes
|
||||
|
||||
if !regex.RightToLeft() {
|
||||
prevat := 0
|
||||
for m != nil {
|
||||
if m.RuneIndex != prevat {
|
||||
buf.WriteString(string(text[prevat:m.RuneIndex]))
|
||||
}
|
||||
prevat = m.RuneIndex + m.RuneLength
|
||||
buf.WriteString(evaluator(*m))
|
||||
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
m, err = regex.FindNextMatch(m)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if prevat < len(text) {
|
||||
buf.WriteString(string(text[prevat:]))
|
||||
}
|
||||
} else {
|
||||
prevat := len(text)
|
||||
var al []string
|
||||
|
||||
for m != nil {
|
||||
if m.RuneIndex+m.RuneLength != prevat {
|
||||
al = append(al, string(text[m.RuneIndex+m.RuneLength:prevat]))
|
||||
}
|
||||
prevat = m.RuneIndex
|
||||
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(string(text[:prevat]))
|
||||
}
|
||||
|
||||
for i := len(al) - 1; i >= 0; i-- {
|
||||
buf.WriteString(al[i])
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
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()
|
||||
text, runeStart, pooledText := runner.decodeStringWithStart(input, startAt)
|
||||
textInfo := newStringMatchText(input, text)
|
||||
defer func() {
|
||||
regex.putRunner(runner)
|
||||
if pooledText != nil {
|
||||
pooledRuneBuffers.put(pooledText)
|
||||
}
|
||||
}()
|
||||
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, 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)
|
||||
}
|
||||
|
||||
if m.RuneIndex != prevat {
|
||||
writeRunes(buf, text, prevat, m.RuneIndex)
|
||||
}
|
||||
prevat = m.RuneIndex + m.RuneLength
|
||||
replacementImpl(data, buf, m)
|
||||
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
scanStart := m.textpos
|
||||
if m.RuneLength == 0 {
|
||||
if scanStart >= len(text) {
|
||||
break
|
||||
}
|
||||
scanStart++
|
||||
}
|
||||
|
||||
m, err = runner.scan(text, textInfo, scanStart, 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()
|
||||
text, runeStart, pooledText := runner.decodeStringWithStart(input, startAt)
|
||||
textInfo := newStringMatchText(input, text)
|
||||
defer func() {
|
||||
regex.putRunner(runner)
|
||||
if pooledText != nil {
|
||||
pooledRuneBuffers.put(pooledText)
|
||||
}
|
||||
}()
|
||||
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, 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)
|
||||
}
|
||||
|
||||
if m.RuneIndex+m.RuneLength != prevat {
|
||||
al = append(al, string(text[m.RuneIndex+m.RuneLength:prevat]))
|
||||
}
|
||||
prevat = m.RuneIndex
|
||||
replacementImplRTL(data, &al, m)
|
||||
|
||||
count--
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
scanStart := m.textpos
|
||||
if m.RuneLength == 0 {
|
||||
if scanStart <= 0 {
|
||||
break
|
||||
}
|
||||
scanStart--
|
||||
}
|
||||
|
||||
m, err = runner.scan(text, textInfo, scanStart, 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:
|
||||
for i := 0; i < m.RuneIndex; i++ {
|
||||
buf.WriteRune(m.text.runes[i])
|
||||
}
|
||||
case replaceRightPortion:
|
||||
for i := m.RuneIndex + 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:
|
||||
for i := 0; i < m.RuneIndex; i++ {
|
||||
buf.WriteRune(m.text.runes[i])
|
||||
}
|
||||
case replaceRightPortion:
|
||||
for i := m.RuneIndex + 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
|
||||
}
|
||||
+2197
File diff suppressed because it is too large
Load Diff
+67
@@ -0,0 +1,67 @@
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Split splits the given input string using the pattern and returns
|
||||
// a slice of the parts. Count limits the number of matches to process.
|
||||
// If Count is -1, then it will process the input fully.
|
||||
// If Count is 0, returns nil. If Count is 1, returns the original input.
|
||||
// The only expected error is a Timeout, if it's set.
|
||||
//
|
||||
// If capturing parentheses are used in the Regex expression, any captured
|
||||
// text is included in the resulting string array
|
||||
// For example, a pattern of "-" Split("a-b") will return ["a", "b"]
|
||||
// but a pattern with "(-)" Split ("a-b") will return ["a", "-", "b"]
|
||||
func (re *Regexp) Split(input string, count int) ([]string, error) {
|
||||
if count < -1 {
|
||||
return nil, errors.New("count too small")
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if count == 1 {
|
||||
return []string{input}, nil
|
||||
}
|
||||
if count == -1 {
|
||||
// no limit
|
||||
count = math.MaxInt
|
||||
}
|
||||
|
||||
// iterate through the matches
|
||||
priorIndex := 0
|
||||
var retVal []string
|
||||
var txt []rune
|
||||
|
||||
m, err := re.FindStringMatch(input)
|
||||
|
||||
for ; m != nil && count > 0; m, err = re.FindNextMatch(m) {
|
||||
txt = m.text.runes
|
||||
// if we have an m, we don't have an err
|
||||
// append our match
|
||||
retVal = append(retVal, string(txt[priorIndex:m.RuneIndex]))
|
||||
// append any capture groups, skipping group 0
|
||||
gs := m.Groups()
|
||||
for i := 1; i < len(gs); i++ {
|
||||
retVal = append(retVal, gs[i].String())
|
||||
}
|
||||
priorIndex = m.RuneIndex + m.RuneLength
|
||||
count--
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if txt == nil {
|
||||
// we never matched, return the original string
|
||||
return []string{input}, nil
|
||||
}
|
||||
|
||||
// append our remainder
|
||||
retVal = append(retVal, string(txt[priorIndex:]))
|
||||
|
||||
return retVal, nil
|
||||
}
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
package regexp2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dlclark/regexp2/v2/helpers"
|
||||
"github.com/dlclark/regexp2/v2/syntax"
|
||||
)
|
||||
|
||||
const maxStringFilterLiteralLen = 8
|
||||
|
||||
var (
|
||||
errStringStartAtTooLarge = errors.New("startAt must be less than the length of the input string")
|
||||
errStringStartAtNotRuneBoundary = errors.New("startAt must align to the start of a valid rune in the input string")
|
||||
)
|
||||
|
||||
// StringPrefixFilter optionally searches string input before the engine decodes it
|
||||
// to runes. It returns a byte index for a candidate match start, or ok=false if
|
||||
// the regex cannot match. The filter must be conservative: false positives are
|
||||
// allowed, false negatives are not.
|
||||
type StringPrefixFilter func(input string, startAt int) (candidateByteIndex int, ok bool)
|
||||
|
||||
func newStringPrefixFilter(code *syntax.Code) StringPrefixFilter {
|
||||
if code == nil || code.RightToLeft || code.FindOptimizations == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts := code.FindOptimizations
|
||||
minRequiredLength := opts.MinRequiredLength
|
||||
|
||||
switch opts.FindMode {
|
||||
case syntax.LeadingString_LeftToRight:
|
||||
return stringIndexPrefixFilter(opts.LeadingPrefix, false, minRequiredLength)
|
||||
case syntax.LeadingString_OrdinalIgnoreCase_LeftToRight:
|
||||
return stringIndexPrefixFilter(opts.LeadingPrefix, true, minRequiredLength)
|
||||
case syntax.LeadingStrings_LeftToRight:
|
||||
return stringIndexPrefixesFilter(opts.LeadingPrefixes, false, minRequiredLength)
|
||||
case syntax.LeadingStrings_OrdinalIgnoreCase_LeftToRight:
|
||||
return stringIndexPrefixesFilter(opts.LeadingPrefixes, true, minRequiredLength)
|
||||
case syntax.FixedDistanceChar_LeftToRight:
|
||||
return stringFixedDistanceCharFilter(opts.FixedDistanceLiteral.C, opts.FixedDistanceLiteral.Distance, minRequiredLength)
|
||||
case syntax.FixedDistanceString_LeftToRight:
|
||||
return stringFixedDistanceStringFilter(opts.FixedDistanceLiteral.S, opts.FixedDistanceLiteral.Distance, minRequiredLength)
|
||||
case syntax.LiteralAfterLoop_LeftToRight:
|
||||
return stringLiteralAfterLoopFilter(opts.LiteralAfterLoop, minRequiredLength)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func stringIndexPrefixFilter(prefix string, ignoreCase bool, minRequiredLength int) StringPrefixFilter {
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
if ignoreCase && !isASCIIString(prefix) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, minRequiredLength) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var offset int
|
||||
if ignoreCase {
|
||||
offset = helpers.IndexStringIgnoreCaseASCII(input[startAt:], prefix)
|
||||
} else {
|
||||
offset = strings.Index(input[startAt:], prefix)
|
||||
}
|
||||
if offset < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return startAt + offset, true
|
||||
}
|
||||
}
|
||||
|
||||
func stringIndexPrefixesFilter(prefixes []string, ignoreCase bool, minRequiredLength int) StringPrefixFilter {
|
||||
if len(prefixes) == 0 {
|
||||
return nil
|
||||
}
|
||||
if ignoreCase {
|
||||
for _, prefix := range prefixes {
|
||||
if !isASCIIString(prefix) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if filter, ok := compileASCIIStringSetPrefixFilter(prefixes, ignoreCase, minRequiredLength); ok {
|
||||
return filter.index
|
||||
}
|
||||
|
||||
return func(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
return indexAnyPrefixFallback(input, startAt, prefixes, ignoreCase, minRequiredLength)
|
||||
}
|
||||
}
|
||||
|
||||
func indexAnyPrefixFallback(input string, startAt int, prefixes []string, ignoreCase bool, minRequiredLength int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, minRequiredLength) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
best := -1
|
||||
remaining := input[startAt:]
|
||||
for _, prefix := range prefixes {
|
||||
var offset int
|
||||
if ignoreCase {
|
||||
offset = helpers.IndexStringIgnoreCaseASCII(remaining, prefix)
|
||||
} else {
|
||||
offset = strings.Index(remaining, prefix)
|
||||
}
|
||||
if offset >= 0 && (best < 0 || offset < best) {
|
||||
best = offset
|
||||
}
|
||||
}
|
||||
if best < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return startAt + best, true
|
||||
}
|
||||
|
||||
type asciiStringSetPrefixFilter struct {
|
||||
firstChars string
|
||||
prefixesByFirst [256][]string
|
||||
minRequiredBytes int
|
||||
}
|
||||
|
||||
// compileASCIIStringSetPrefixFilter builds a byte-oriented multi-prefix scanner
|
||||
// for the narrow shape where it beats running strings.Index once per prefix:
|
||||
// case-sensitive ASCII prefixes with at least two prefixes sharing a first byte.
|
||||
// It indexes possible first bytes with strings.IndexAny, then verifies only the
|
||||
// bucket for the byte found. Other shapes fall back to the old implementation.
|
||||
func compileASCIIStringSetPrefixFilter(prefixes []string, ignoreCase bool, minRequiredLength int) (*asciiStringSetPrefixFilter, bool) {
|
||||
if ignoreCase {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
filter := &asciiStringSetPrefixFilter{
|
||||
minRequiredBytes: minRequiredLength,
|
||||
}
|
||||
var firstChars [256]bool
|
||||
var hasSharedFirst bool
|
||||
for _, prefix := range prefixes {
|
||||
if prefix == "" || !isASCIIString(prefix) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
first := prefix[0]
|
||||
filter.prefixesByFirst[first] = append(filter.prefixesByFirst[first], prefix)
|
||||
if len(filter.prefixesByFirst[first]) > 1 {
|
||||
hasSharedFirst = true
|
||||
}
|
||||
firstChars[first] = true
|
||||
}
|
||||
|
||||
if !hasSharedFirst {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
firstBytes := make([]byte, 0, len(prefixes)*2)
|
||||
for i, ok := range firstChars {
|
||||
if ok {
|
||||
firstBytes = append(firstBytes, byte(i))
|
||||
}
|
||||
}
|
||||
if len(firstBytes) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
filter.firstChars = string(firstBytes)
|
||||
return filter, true
|
||||
}
|
||||
|
||||
func (f *asciiStringSetPrefixFilter) index(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, f.minRequiredBytes) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for searchAt := startAt; searchAt < len(input); {
|
||||
offset := strings.IndexAny(input[searchAt:], f.firstChars)
|
||||
if offset < 0 {
|
||||
return 0, false
|
||||
}
|
||||
i := searchAt + offset
|
||||
first := input[i]
|
||||
for _, prefix := range f.prefixesByFirst[first] {
|
||||
if len(input)-i >= len(prefix) && strings.HasPrefix(input[i:], prefix) {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
searchAt = i + 1
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func stringFixedDistanceCharFilter(ch rune, distance, minRequiredLength int) StringPrefixFilter {
|
||||
if distance < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, minRequiredLength) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
searchAt := startAt
|
||||
for {
|
||||
offset := strings.IndexRune(input[searchAt:], ch)
|
||||
if offset < 0 {
|
||||
return 0, false
|
||||
}
|
||||
byteIndex := searchAt + offset
|
||||
candidateByteIndex, ok := stringFixedDistanceCandidateStart(input, startAt, byteIndex, distance)
|
||||
if ok && hasMinRequiredBytes(input, candidateByteIndex, minRequiredLength) {
|
||||
return candidateByteIndex, true
|
||||
}
|
||||
if ok {
|
||||
return 0, false
|
||||
}
|
||||
_, size := utf8.DecodeRuneInString(input[byteIndex:])
|
||||
if size == 0 {
|
||||
return 0, false
|
||||
}
|
||||
searchAt = byteIndex + size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringFixedDistanceStringFilter(literal string, distance, minRequiredLength int) StringPrefixFilter {
|
||||
if literal == "" || distance < 0 || len(literal) > maxStringFilterLiteralLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, minRequiredLength) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
searchAt := startAt
|
||||
for searchAt <= len(input)-len(literal) {
|
||||
offset := strings.Index(input[searchAt:], literal)
|
||||
if offset < 0 {
|
||||
return 0, false
|
||||
}
|
||||
literalIndex := searchAt + offset
|
||||
candidateByteIndex, ok := stringFixedDistanceCandidateStart(input, startAt, literalIndex, distance)
|
||||
if ok && hasMinRequiredBytes(input, candidateByteIndex, minRequiredLength) {
|
||||
return candidateByteIndex, true
|
||||
}
|
||||
if ok {
|
||||
return 0, false
|
||||
}
|
||||
searchAt = literalIndex + 1
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func stringLiteralAfterLoopFilter(literal *syntax.LiteralAfterLoop, minRequiredLength int) StringPrefixFilter {
|
||||
if literal == nil || literal.LoopNode == nil || literal.LoopNode.Set == nil {
|
||||
return nil
|
||||
}
|
||||
if literal.StringIgnoreCase && (literal.String == "" || !isASCIIString(literal.String)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return func(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if !hasMinRequiredBytes(input, startAt, minRequiredLength) {
|
||||
return 0, false
|
||||
}
|
||||
if !stringHasLiteralAfterLoop(input, startAt, literal) {
|
||||
return 0, false
|
||||
}
|
||||
return startAt, true
|
||||
}
|
||||
}
|
||||
|
||||
func stringHasLiteralAfterLoop(input string, searchAt int, literal *syntax.LiteralAfterLoop) bool {
|
||||
switch {
|
||||
case literal.String != "":
|
||||
if literal.StringIgnoreCase {
|
||||
return helpers.IndexStringIgnoreCaseASCII(input[searchAt:], literal.String) >= 0
|
||||
}
|
||||
return strings.Contains(input[searchAt:], literal.String)
|
||||
case len(literal.Chars) > 0:
|
||||
needle := string(literal.Chars)
|
||||
return strings.ContainsAny(input[searchAt:], needle)
|
||||
default:
|
||||
return strings.ContainsRune(input[searchAt:], literal.Char)
|
||||
}
|
||||
}
|
||||
|
||||
func stringFixedDistanceCandidateStart(input string, startAt, byteIndex, distance int) (int, bool) {
|
||||
candidateByteIndex := byteIndex
|
||||
for i := 0; i < distance; i++ {
|
||||
if candidateByteIndex <= startAt {
|
||||
return 0, false
|
||||
}
|
||||
_, size := utf8.DecodeLastRuneInString(input[:candidateByteIndex])
|
||||
if size == 0 {
|
||||
return 0, false
|
||||
}
|
||||
candidateByteIndex -= size
|
||||
}
|
||||
return candidateByteIndex, true
|
||||
}
|
||||
|
||||
func (re *Regexp) findStringPrefixCandidate(input string, startAt int) (candidateByteIndex int, ok bool) {
|
||||
if re.stringPrefixFilter == nil || re.RightToLeft() {
|
||||
return startAt, true
|
||||
}
|
||||
candidateByteIndex, ok = re.stringPrefixFilter(input, startAt)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if candidateByteIndex < startAt || candidateByteIndex > len(input) || !isStringRuneBoundary(input, candidateByteIndex) {
|
||||
return startAt, true
|
||||
}
|
||||
return candidateByteIndex, true
|
||||
}
|
||||
|
||||
func (re *Regexp) findStringMatchStart(input string, startAt int) (candidateByteIndex int, ok bool, err error) {
|
||||
if startAt > len(input) {
|
||||
return 0, false, errStringStartAtTooLarge
|
||||
}
|
||||
if startAt >= 0 && !isStringRuneBoundary(input, startAt) {
|
||||
return 0, false, errStringStartAtNotRuneBoundary
|
||||
}
|
||||
|
||||
if startAt < 0 {
|
||||
if re.RightToLeft() {
|
||||
startAt = len(input)
|
||||
} else {
|
||||
startAt = 0
|
||||
}
|
||||
}
|
||||
|
||||
candidateByteIndex, ok = re.findStringPrefixCandidate(input, startAt)
|
||||
return candidateByteIndex, ok, nil
|
||||
}
|
||||
|
||||
func hasMinRequiredBytes(input string, startAt, minRequiredLength int) bool {
|
||||
if startAt < 0 || startAt > len(input) {
|
||||
return false
|
||||
}
|
||||
return minRequiredLength <= 0 || len(input)-startAt >= minRequiredLength
|
||||
}
|
||||
|
||||
func isStringRuneBoundary(s string, index int) bool {
|
||||
if index == 0 || index == len(s) {
|
||||
return true
|
||||
}
|
||||
if index < 0 || index > len(s) {
|
||||
return false
|
||||
}
|
||||
for strIdx := range s {
|
||||
if strIdx == index {
|
||||
return true
|
||||
}
|
||||
if strIdx > index {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isASCIIString(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+1493
File diff suppressed because it is too large
Load Diff
+316
@@ -0,0 +1,316 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
// similar to prog.go in the go regex package...also with comment 'may not belong in this package'
|
||||
|
||||
// File provides operator constants for use by the Builder and the Machine.
|
||||
|
||||
// Implementation notes:
|
||||
//
|
||||
// Regexps are built into RegexCodes, which contain an operation array,
|
||||
// a string table, and some constants.
|
||||
//
|
||||
// Each operation is one of the codes below, followed by the integer
|
||||
// operands specified for each op.
|
||||
//
|
||||
// Strings and sets are indices into a string table.
|
||||
|
||||
type InstOp int
|
||||
|
||||
const (
|
||||
// lef/back operands description
|
||||
|
||||
Onerep InstOp = 0 // lef,back char,min,max a {n}
|
||||
Notonerep InstOp = 1 // lef,back char,min,max .{n}
|
||||
Setrep InstOp = 2 // lef,back set,min,max [\d]{n}
|
||||
|
||||
Oneloop InstOp = 3 // lef,back char,min,max a {,n}
|
||||
Notoneloop InstOp = 4 // lef,back char,min,max .{,n}
|
||||
Setloop InstOp = 5 // lef,back set,min,max [\d]{,n}
|
||||
|
||||
Onelazy InstOp = 6 // lef,back char,min,max a {,n}?
|
||||
Notonelazy InstOp = 7 // lef,back char,min,max .{,n}?
|
||||
Setlazy InstOp = 8 // lef,back set,min,max [\d]{,n}?
|
||||
|
||||
One InstOp = 9 // lef char a
|
||||
Notone InstOp = 10 // lef char [^a]
|
||||
Set InstOp = 11 // lef set [a-z\s] \w \s \d
|
||||
|
||||
Multi InstOp = 12 // lef string abcd
|
||||
Ref InstOp = 13 // lef group \#
|
||||
|
||||
Bol InstOp = 14 // ^
|
||||
Eol InstOp = 15 // $
|
||||
Boundary InstOp = 16 // \b
|
||||
Nonboundary InstOp = 17 // \B
|
||||
Beginning InstOp = 18 // \A
|
||||
Start InstOp = 19 // \G
|
||||
EndZ InstOp = 20 // \Z
|
||||
End InstOp = 21 // \Z
|
||||
|
||||
Nothing InstOp = 22 // Reject!
|
||||
|
||||
// Primitive control structures
|
||||
|
||||
Lazybranch InstOp = 23 // back jump straight first
|
||||
Branchmark InstOp = 24 // back jump branch first for loop
|
||||
Lazybranchmark InstOp = 25 // back jump straight first for loop
|
||||
Nullcount InstOp = 26 // back val set counter, null mark
|
||||
Setcount InstOp = 27 // back val set counter, make mark
|
||||
Branchcount InstOp = 28 // back jump,limit branch++ if zero<=c<limit
|
||||
Lazybranchcount InstOp = 29 // back jump,limit same, but straight first
|
||||
Nullmark InstOp = 30 // back save position
|
||||
Setmark InstOp = 31 // back save position
|
||||
Capturemark InstOp = 32 // back group define group
|
||||
Getmark InstOp = 33 // back recall position
|
||||
Setjump InstOp = 34 // back save backtrack state
|
||||
Backjump InstOp = 35 // zap back to saved state
|
||||
Forejump InstOp = 36 // zap backtracking state
|
||||
Testref InstOp = 37 // backtrack if ref undefined
|
||||
Goto InstOp = 38 // jump just go
|
||||
|
||||
Prune InstOp = 39 // prune it baby
|
||||
Stop InstOp = 40 // done!
|
||||
|
||||
ECMABoundary InstOp = 41 // \b
|
||||
NonECMABoundary InstOp = 42 // \B
|
||||
|
||||
// Atomic loop of the specified character.
|
||||
// Operand 0 is the character. Operand 1 is the max iteration count.
|
||||
Oneloopatomic InstOp = 43
|
||||
// Atomic loop of a single character other than the one specified.
|
||||
// Operand 0 is the character. Operand 1 is the max iteration count.
|
||||
Notoneloopatomic InstOp = 44
|
||||
// Atomic loop of a single character matching the specified set
|
||||
// Operand 0 is index into the strings table of the character class description. Operand 1 is the repetition count.
|
||||
Setloopatomic InstOp = 45
|
||||
// Updates the bumpalong position to the current position.
|
||||
UpdateBumpalong InstOp = 46
|
||||
|
||||
// Modifiers for alternate modes
|
||||
|
||||
Mask InstOp = 63 // Mask to get unmodified ordinary operator
|
||||
Rtl InstOp = 64 // bit to indicate that we're reverse scanning.
|
||||
Back InstOp = 128 // bit to indicate that we're backtracking.
|
||||
Back2 InstOp = 256 // bit to indicate that we're backtracking on a second branch.
|
||||
Ci InstOp = 512 // bit to indicate that we're case-insensitive.
|
||||
)
|
||||
|
||||
type Code struct {
|
||||
Codes []int // the code
|
||||
Strings [][]rune // string table
|
||||
Sets []*CharSet //character set table
|
||||
TrackCount int // how many instructions use backtracking
|
||||
Caps map[int]int // mapping of user group numbers -> impl group slots
|
||||
Capsize int // number of impl group slots
|
||||
FcPrefix *Prefix // the set of candidate first characters (may be null)
|
||||
BmPrefix *BmPrefix // the fixed prefix string as a Boyer-Moore machine (may be null)
|
||||
Anchors AnchorLoc // the set of zero-length start anchors (RegexFCD.Bol, etc)
|
||||
RightToLeft bool // true if right to left
|
||||
FindOptimizations *FindOptimizations // analyzed candidate search strategy
|
||||
}
|
||||
|
||||
// PrepareCharSetASCIIBitmaps builds bounded ASCII lookup tables for compiled
|
||||
// character classes before the regexp is shared across goroutines.
|
||||
func (c *Code) PrepareCharSetASCIIBitmaps() {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
for _, set := range c.Sets {
|
||||
set.prepareASCIIBitmap()
|
||||
}
|
||||
if c.FcPrefix != nil {
|
||||
c.FcPrefix.PrefixSet.prepareASCIIBitmap()
|
||||
}
|
||||
if c.FindOptimizations != nil {
|
||||
for _, set := range c.FindOptimizations.FixedDistanceSets {
|
||||
set.Set.prepareASCIIBitmap()
|
||||
}
|
||||
if c.FindOptimizations.LiteralAfterLoop != nil && c.FindOptimizations.LiteralAfterLoop.LoopNode != nil {
|
||||
c.FindOptimizations.LiteralAfterLoop.LoopNode.Set.prepareASCIIBitmap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func opcodeBacktracks(op InstOp) bool {
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case Oneloop, Notoneloop, Setloop, Onelazy, Notonelazy, Setlazy, Lazybranch, Branchmark, Lazybranchmark,
|
||||
Nullcount, Setcount, Branchcount, Lazybranchcount, Setmark, Capturemark, Getmark, Setjump, Backjump,
|
||||
Forejump, Goto:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func opcodeSize(op InstOp) int {
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case Nothing, Bol, Eol, Boundary, Nonboundary, ECMABoundary, NonECMABoundary, Beginning, Start, EndZ,
|
||||
End, Nullmark, Setmark, Getmark, Setjump, Backjump, Forejump, Stop, UpdateBumpalong:
|
||||
return 1
|
||||
|
||||
case One, Notone, Multi, Ref, Testref, Goto, Nullcount, Setcount, Lazybranch, Branchmark, Lazybranchmark,
|
||||
Prune, Set:
|
||||
return 2
|
||||
|
||||
case Capturemark, Branchcount, Lazybranchcount, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy,
|
||||
Setlazy, Setrep, Setloop, Oneloopatomic, Notoneloopatomic, Setloopatomic:
|
||||
return 3
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected op code: %v", op))
|
||||
}
|
||||
}
|
||||
|
||||
var codeStr = []string{
|
||||
"Onerep", "Notonerep", "Setrep",
|
||||
"Oneloop", "Notoneloop", "Setloop",
|
||||
"Onelazy", "Notonelazy", "Setlazy",
|
||||
"One", "Notone", "Set",
|
||||
"Multi", "Ref",
|
||||
"Bol", "Eol", "Boundary", "Nonboundary", "Beginning", "Start", "EndZ", "End",
|
||||
"Nothing",
|
||||
"Lazybranch", "Branchmark", "Lazybranchmark",
|
||||
"Nullcount", "Setcount", "Branchcount", "Lazybranchcount",
|
||||
"Nullmark", "Setmark", "Capturemark", "Getmark",
|
||||
"Setjump", "Backjump", "Forejump", "Testref", "Goto",
|
||||
"Prune", "Stop",
|
||||
"ECMABoundary", "NonECMABoundary",
|
||||
"Oneloopatomic", "Notoneloopatomic", "Setloopatomic",
|
||||
"Bumpalong",
|
||||
}
|
||||
|
||||
func operatorDescription(op InstOp) string {
|
||||
desc := codeStr[op&Mask]
|
||||
if (op & Ci) != 0 {
|
||||
desc += "-Ci"
|
||||
}
|
||||
if (op & Rtl) != 0 {
|
||||
desc += "-Rtl"
|
||||
}
|
||||
if (op & Back) != 0 {
|
||||
desc += "-Back"
|
||||
}
|
||||
if (op & Back2) != 0 {
|
||||
desc += "-Back2"
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
// OpcodeDescription is a humman readable string of the specific offset
|
||||
func (c *Code) OpcodeDescription(offset int) string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
op := InstOp(c.Codes[offset])
|
||||
fmt.Fprintf(buf, "%06d ", offset)
|
||||
|
||||
if opcodeBacktracks(op & Mask) {
|
||||
buf.WriteString("*")
|
||||
} else {
|
||||
buf.WriteString(" ")
|
||||
}
|
||||
buf.WriteString(operatorDescription(op))
|
||||
buf.WriteString("(")
|
||||
op &= Mask
|
||||
|
||||
switch op {
|
||||
case One, Notone, Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy,
|
||||
Oneloopatomic, Notoneloopatomic:
|
||||
buf.WriteString("Ch = ")
|
||||
buf.WriteString(CharDescription(rune(c.Codes[offset+1])))
|
||||
|
||||
case Set, Setrep, Setloop, Setlazy, Setloopatomic:
|
||||
buf.WriteString("Set = ")
|
||||
buf.WriteString(c.Sets[c.Codes[offset+1]].String())
|
||||
|
||||
case Multi:
|
||||
fmt.Fprintf(buf, "String = %s", string(c.Strings[c.Codes[offset+1]]))
|
||||
|
||||
case Ref, Testref:
|
||||
fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1])
|
||||
|
||||
case Capturemark:
|
||||
fmt.Fprintf(buf, "Index = %d", c.Codes[offset+1])
|
||||
if c.Codes[offset+2] != -1 {
|
||||
fmt.Fprintf(buf, ", Unindex = %d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
case Nullcount, Setcount:
|
||||
fmt.Fprintf(buf, "Value = %d", c.Codes[offset+1])
|
||||
|
||||
case Goto, Lazybranch, Branchmark, Lazybranchmark, Branchcount, Lazybranchcount:
|
||||
fmt.Fprintf(buf, "Addr = %d", c.Codes[offset+1])
|
||||
}
|
||||
|
||||
switch op {
|
||||
case Onerep, Notonerep, Oneloop, Notoneloop, Onelazy, Notonelazy, Setrep, Setloop, Setlazy,
|
||||
Oneloopatomic, Notoneloopatomic, Setloopatomic:
|
||||
buf.WriteString(", Rep = ")
|
||||
if c.Codes[offset+2] == math.MaxInt32 {
|
||||
buf.WriteString("inf")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
case Branchcount, Lazybranchcount:
|
||||
buf.WriteString(", Limit = ")
|
||||
if c.Codes[offset+2] == math.MaxInt32 {
|
||||
buf.WriteString("inf")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "%d", c.Codes[offset+2])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
buf.WriteString(")")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (c *Code) Dump() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if c.RightToLeft {
|
||||
fmt.Fprintln(buf, "Direction: right-to-left")
|
||||
} else {
|
||||
fmt.Fprintln(buf, "Direction: left-to-right")
|
||||
}
|
||||
if c.FcPrefix == nil {
|
||||
fmt.Fprintln(buf, "Firstchars: n/a")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Firstchars: %v\n", c.FcPrefix.PrefixSet.String())
|
||||
}
|
||||
|
||||
if c.BmPrefix == nil {
|
||||
fmt.Fprintln(buf, "Prefix: n/a")
|
||||
} else {
|
||||
fmt.Fprintf(buf, "Prefix: %v\n", Escape(c.BmPrefix.String()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "Anchors: %v\n", c.Anchors)
|
||||
if c.FindOptimizations != nil {
|
||||
fmt.Fprint(buf, c.FindOptimizations.Dump())
|
||||
}
|
||||
fmt.Fprintln(buf)
|
||||
|
||||
if c.BmPrefix != nil {
|
||||
fmt.Fprintln(buf, "BoyerMoore:")
|
||||
fmt.Fprintln(buf, c.BmPrefix.Dump(" "))
|
||||
}
|
||||
for i := 0; i < len(c.Codes); i += opcodeSize(InstOp(c.Codes[i])) {
|
||||
fmt.Fprintln(buf, c.OpcodeDescription(i))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
Generated
Vendored
+1
-1
@@ -19,7 +19,7 @@ const meta = `\.+*?()|[]{}^$# `
|
||||
|
||||
func escape(b *bytes.Buffer, r rune, force bool) {
|
||||
if unicode.IsPrint(r) {
|
||||
if strings.IndexRune(meta, r) >= 0 || force {
|
||||
if strings.ContainsRune(meta, r) || force {
|
||||
b.WriteRune('\\')
|
||||
}
|
||||
b.WriteRune(r)
|
||||
Generated
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package syntax
|
||||
@@ -5,7 +6,7 @@ package syntax
|
||||
// Fuzz is the input point for go-fuzz
|
||||
func Fuzz(data []byte) int {
|
||||
sdata := string(data)
|
||||
tree, err := Parse(sdata, RegexOptions(0))
|
||||
tree, err := Parse(sdata, ParseOptions{})
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
+672
@@ -0,0 +1,672 @@
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type FindOptimizations struct {
|
||||
rightToLeft bool
|
||||
asciiLookups [][]uint
|
||||
|
||||
FindMode FindNextStartingPositionMode
|
||||
LeadingAnchor NodeType
|
||||
TrailingAnchor NodeType
|
||||
MinRequiredLength int
|
||||
MaxPossibleLength int
|
||||
LeadingPrefix string
|
||||
LeadingPrefixes []string
|
||||
LeadingPrefixesRunes [][]rune
|
||||
//LeadingStrings *helpers.StringSearchValues
|
||||
|
||||
FixedDistanceLiteral FixedDistanceLiteral
|
||||
FixedDistanceSets []FixedDistanceSet
|
||||
LiteralAfterLoop *LiteralAfterLoop
|
||||
LandmarkChain *RequiredLandmarkChain
|
||||
}
|
||||
|
||||
type LiteralAfterLoop struct {
|
||||
String string
|
||||
StringIgnoreCase bool
|
||||
Char rune
|
||||
Chars []rune
|
||||
|
||||
LoopNode *RegexNode
|
||||
}
|
||||
|
||||
type FixedDistanceSet struct {
|
||||
Set *CharSet
|
||||
Chars []rune
|
||||
Negated bool
|
||||
Range *SingleRange
|
||||
Distance int
|
||||
}
|
||||
|
||||
type FixedDistanceLiteral struct {
|
||||
S string
|
||||
C rune
|
||||
Distance int
|
||||
}
|
||||
|
||||
type RequiredLandmarkChain struct {
|
||||
// LeadingLoopSet is the unbounded leading set loop that precedes every
|
||||
// landmark in the original concatenation. At run time, after the first
|
||||
// landmark alternative is found, the scanner walks backward over this set
|
||||
// to recover the earliest plausible regex start position.
|
||||
LeadingLoopSet *CharSet
|
||||
|
||||
// Landmarks must all be found, in slice order, for the chain prefilter to
|
||||
// produce a candidate. Each landmark is satisfied by exactly one matching
|
||||
// alternative; alternatives are tried independently by the runner.
|
||||
Landmarks []RequiredLandmark
|
||||
}
|
||||
|
||||
type RequiredLandmark struct {
|
||||
// Alternatives describes the mutually exclusive shapes that can satisfy
|
||||
// this single required landmark. A landmark matches when any one alternative
|
||||
// matches at a position in the input.
|
||||
Alternatives []RequiredLandmarkAlternative
|
||||
}
|
||||
|
||||
type RequiredLandmarkAlternative struct {
|
||||
// Literal is the core token for literal alternatives. When non-empty, it
|
||||
// must match exactly at the candidate core position, and Set must be nil.
|
||||
Literal []rune
|
||||
|
||||
// Set is the core token for character-class alternatives. When non-nil, it
|
||||
// must match between MinRepeat and MaxRepeat runes at the candidate core
|
||||
// position, and Literal must be empty. The analyzer only builds set
|
||||
// alternatives for non-negated sets that can be cheaply enumerated, but the
|
||||
// runner uses Set for membership checks.
|
||||
Set *CharSet
|
||||
|
||||
// LeadingWhitespaceSet is the optional or required whitespace immediately
|
||||
// before the core token. If RequireWhitespaceBefore is true, at least one
|
||||
// rune from this set must precede the core. When this alternative is the
|
||||
// first matched landmark, the runner may rewind over additional contiguous
|
||||
// leading whitespace from this set before rewinding over LeadingLoopSet.
|
||||
LeadingWhitespaceSet *CharSet
|
||||
|
||||
// TrailingWhitespaceSet is the optional or required whitespace immediately
|
||||
// after the core token. If RequireWhitespaceAfter is true, at least one rune
|
||||
// from this set must follow the core. The runner validates the requirement,
|
||||
// but does not consume optional trailing whitespace into the landmark end.
|
||||
TrailingWhitespaceSet *CharSet
|
||||
|
||||
// MinRepeat and MaxRepeat describe the core token width. Literal alternatives
|
||||
// use 1..1 regardless of literal length because the literal is matched as one
|
||||
// fixed core token; Set alternatives use the source set repetition.
|
||||
MinRepeat int
|
||||
MaxRepeat int
|
||||
|
||||
RequireWhitespaceBefore bool
|
||||
RequireWhitespaceAfter bool
|
||||
}
|
||||
|
||||
type FindNextStartingPositionMode int
|
||||
|
||||
const (
|
||||
NoSearch FindNextStartingPositionMode = iota
|
||||
// A "beginning" anchor at the beginning of the pattern.
|
||||
LeadingAnchor_LeftToRight_Beginning
|
||||
// A "start" anchor at the beginning of the pattern.
|
||||
LeadingAnchor_LeftToRight_Start
|
||||
// An "endz" anchor at the beginning of the pattern. This is rare.
|
||||
LeadingAnchor_LeftToRight_EndZ
|
||||
// An "end" anchor at the beginning of the pattern. This is rare.
|
||||
LeadingAnchor_LeftToRight_End
|
||||
// A "beginning" anchor at the beginning of the right-to-left pattern.
|
||||
LeadingAnchor_RightToLeft_Beginning
|
||||
// A "start" anchor at the beginning of the right-to-left pattern.
|
||||
LeadingAnchor_RightToLeft_Start
|
||||
// An "endz" anchor at the beginning of the right-to-left pattern. This is rare.
|
||||
LeadingAnchor_RightToLeft_EndZ
|
||||
// An "end" anchor at the beginning of the right-to-left pattern. This is rare.
|
||||
LeadingAnchor_RightToLeft_End
|
||||
// An "end" anchor at the end of the pattern, with the pattern always matching a fixed-length expression.
|
||||
TrailingAnchor_FixedLength_LeftToRight_End
|
||||
// An "endz" anchor at the end of the pattern, with the pattern always matching a fixed-length expression.
|
||||
TrailingAnchor_FixedLength_LeftToRight_EndZ
|
||||
// A multi-character substring at the beginning of the pattern.
|
||||
LeadingString_LeftToRight
|
||||
// A multi-character substring at the beginning of the right-to-left pattern.
|
||||
LeadingString_RightToLeft
|
||||
// A multi-character ordinal case-insensitive substring at the beginning of the pattern.
|
||||
LeadingString_OrdinalIgnoreCase_LeftToRight
|
||||
// Multiple leading prefix strings
|
||||
LeadingStrings_LeftToRight
|
||||
// Multiple leading ordinal case-insensitive prefix strings
|
||||
LeadingStrings_OrdinalIgnoreCase_LeftToRight
|
||||
|
||||
// A set starting the pattern.
|
||||
LeadingSet_LeftToRight
|
||||
// A set starting the right-to-left pattern.
|
||||
LeadingSet_RightToLeft
|
||||
|
||||
// A single character at the start of the right-to-left pattern.
|
||||
LeadingChar_RightToLeft
|
||||
|
||||
// A single character at a fixed distance from the start of the pattern.
|
||||
FixedDistanceChar_LeftToRight
|
||||
// A multi-character case-sensitive string at a fixed distance from the start of the pattern.
|
||||
FixedDistanceString_LeftToRight
|
||||
|
||||
// One or more sets at a fixed distance from the start of the pattern.
|
||||
FixedDistanceSets_LeftToRight
|
||||
|
||||
// A literal (single character, multi-char string, or set with small number of characters) after a non-overlapping set loop at the start of the pattern.
|
||||
LiteralAfterLoop_LeftToRight
|
||||
|
||||
// A sequence of required landmarks after a leading loop.
|
||||
RequiredLandmarkChain_LeftToRight
|
||||
)
|
||||
|
||||
func (m FindNextStartingPositionMode) String() string {
|
||||
switch m {
|
||||
case NoSearch:
|
||||
return "NoSearch"
|
||||
case LeadingAnchor_LeftToRight_Beginning:
|
||||
return "LeadingAnchor_LeftToRight_Beginning"
|
||||
case LeadingAnchor_LeftToRight_Start:
|
||||
return "LeadingAnchor_LeftToRight_Start"
|
||||
case LeadingAnchor_LeftToRight_EndZ:
|
||||
return "LeadingAnchor_LeftToRight_EndZ"
|
||||
case LeadingAnchor_LeftToRight_End:
|
||||
return "LeadingAnchor_LeftToRight_End"
|
||||
case LeadingAnchor_RightToLeft_Beginning:
|
||||
return "LeadingAnchor_RightToLeft_Beginning"
|
||||
case LeadingAnchor_RightToLeft_Start:
|
||||
return "LeadingAnchor_RightToLeft_Start"
|
||||
case LeadingAnchor_RightToLeft_EndZ:
|
||||
return "LeadingAnchor_RightToLeft_EndZ"
|
||||
case LeadingAnchor_RightToLeft_End:
|
||||
return "LeadingAnchor_RightToLeft_End"
|
||||
case TrailingAnchor_FixedLength_LeftToRight_End:
|
||||
return "TrailingAnchor_FixedLength_LeftToRight_End"
|
||||
case TrailingAnchor_FixedLength_LeftToRight_EndZ:
|
||||
return "TrailingAnchor_FixedLength_LeftToRight_EndZ"
|
||||
case LeadingString_LeftToRight:
|
||||
return "LeadingString_LeftToRight"
|
||||
case LeadingString_RightToLeft:
|
||||
return "LeadingString_RightToLeft"
|
||||
case LeadingString_OrdinalIgnoreCase_LeftToRight:
|
||||
return "LeadingString_OrdinalIgnoreCase_LeftToRight"
|
||||
case LeadingStrings_LeftToRight:
|
||||
return "LeadingStrings_LeftToRight"
|
||||
case LeadingStrings_OrdinalIgnoreCase_LeftToRight:
|
||||
return "LeadingStrings_OrdinalIgnoreCase_LeftToRight"
|
||||
case LeadingSet_LeftToRight:
|
||||
return "LeadingSet_LeftToRight"
|
||||
case LeadingSet_RightToLeft:
|
||||
return "LeadingSet_RightToLeft"
|
||||
case LeadingChar_RightToLeft:
|
||||
return "LeadingChar_RightToLeft"
|
||||
case FixedDistanceChar_LeftToRight:
|
||||
return "FixedDistanceChar_LeftToRight"
|
||||
case FixedDistanceString_LeftToRight:
|
||||
return "FixedDistanceString_LeftToRight"
|
||||
case FixedDistanceSets_LeftToRight:
|
||||
return "FixedDistanceSets_LeftToRight"
|
||||
case LiteralAfterLoop_LeftToRight:
|
||||
return "LiteralAfterLoop_LeftToRight"
|
||||
case RequiredLandmarkChain_LeftToRight:
|
||||
return "RequiredLandmarkChain_LeftToRight"
|
||||
default:
|
||||
return fmt.Sprintf("FindNextStartingPositionMode(%d)", int(m))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FindOptimizations) Dump() string {
|
||||
buf := &bytes.Buffer{}
|
||||
if f == nil {
|
||||
fmt.Fprintln(buf, "Find mode: n/a")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
fmt.Fprintf(buf, "Find mode: %s\n", f.FindMode)
|
||||
fmt.Fprintf(buf, "Min length: %d\n", f.MinRequiredLength)
|
||||
if f.MaxPossibleLength > 0 {
|
||||
fmt.Fprintf(buf, "Max length: %d\n", f.MaxPossibleLength)
|
||||
}
|
||||
|
||||
switch f.FindMode {
|
||||
case LeadingAnchor_LeftToRight_Beginning, LeadingAnchor_LeftToRight_Start,
|
||||
LeadingAnchor_LeftToRight_EndZ, LeadingAnchor_LeftToRight_End,
|
||||
LeadingAnchor_RightToLeft_Beginning, LeadingAnchor_RightToLeft_Start,
|
||||
LeadingAnchor_RightToLeft_EndZ, LeadingAnchor_RightToLeft_End:
|
||||
fmt.Fprintf(buf, "Anchor: %v\n", f.LeadingAnchor)
|
||||
|
||||
case TrailingAnchor_FixedLength_LeftToRight_End, TrailingAnchor_FixedLength_LeftToRight_EndZ:
|
||||
fmt.Fprintf(buf, "Anchor: %v\n", f.TrailingAnchor)
|
||||
|
||||
case LeadingString_LeftToRight, LeadingString_RightToLeft, LeadingString_OrdinalIgnoreCase_LeftToRight:
|
||||
fmt.Fprintf(buf, "Prefix: %q\n", f.LeadingPrefix)
|
||||
|
||||
case LeadingStrings_LeftToRight, LeadingStrings_OrdinalIgnoreCase_LeftToRight:
|
||||
fmt.Fprintf(buf, "Prefixes: %q\n", f.LeadingPrefixes)
|
||||
|
||||
case FixedDistanceChar_LeftToRight:
|
||||
fmt.Fprintf(buf, "Literal: %q at distance %d\n", f.FixedDistanceLiteral.C, f.FixedDistanceLiteral.Distance)
|
||||
|
||||
case FixedDistanceString_LeftToRight:
|
||||
fmt.Fprintf(buf, "Literal: %q at distance %d\n", f.FixedDistanceLiteral.S, f.FixedDistanceLiteral.Distance)
|
||||
|
||||
case FixedDistanceSets_LeftToRight:
|
||||
for i, set := range f.FixedDistanceSets {
|
||||
fmt.Fprintf(buf, "Set[%d]: distance=%d %s\n", i, set.Distance, fixedDistanceSetDescription(set))
|
||||
}
|
||||
|
||||
case LiteralAfterLoop_LeftToRight:
|
||||
fmt.Fprintf(buf, "Literal: %s\n", literalAfterLoopDescription(f.LiteralAfterLoop))
|
||||
|
||||
case RequiredLandmarkChain_LeftToRight:
|
||||
fmt.Fprintf(buf, "Landmarks: %s\n", landmarkChainDescription(f.LandmarkChain))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fixedDistanceSetDescription(set FixedDistanceSet) string {
|
||||
switch {
|
||||
case len(set.Chars) > 0:
|
||||
return fmt.Sprintf("chars=%q negated=%t", string(set.Chars), set.Negated)
|
||||
case set.Range != nil:
|
||||
return fmt.Sprintf("range=%q-%q negated=%t", set.Range.First, set.Range.Last, set.Negated)
|
||||
case set.Set != nil:
|
||||
return fmt.Sprintf("set=%s", set.Set.String())
|
||||
default:
|
||||
return "set=<nil>"
|
||||
}
|
||||
}
|
||||
|
||||
func literalAfterLoopDescription(literal *LiteralAfterLoop) string {
|
||||
if literal == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
loopSet := "<nil>"
|
||||
if literal.LoopNode != nil && literal.LoopNode.Set != nil {
|
||||
loopSet = literal.LoopNode.Set.String()
|
||||
}
|
||||
switch {
|
||||
case literal.String != "":
|
||||
return fmt.Sprintf("string=%q ignoreCase=%t after loop set %s", literal.String, literal.StringIgnoreCase, loopSet)
|
||||
case len(literal.Chars) > 0:
|
||||
return fmt.Sprintf("chars=%q after loop set %s", string(literal.Chars), loopSet)
|
||||
default:
|
||||
return fmt.Sprintf("char=%q after loop set %s", literal.Char, loopSet)
|
||||
}
|
||||
}
|
||||
|
||||
func landmarkChainDescription(chain *RequiredLandmarkChain) string {
|
||||
if chain == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
loopSet := "<nil>"
|
||||
if chain.LeadingLoopSet != nil {
|
||||
loopSet = chain.LeadingLoopSet.String()
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprintf(buf, "leadingLoop=%s", loopSet)
|
||||
for i, landmark := range chain.Landmarks {
|
||||
fmt.Fprintf(buf, " landmark[%d]=", i)
|
||||
for j, alt := range landmark.Alternatives {
|
||||
if j > 0 {
|
||||
buf.WriteString("|")
|
||||
}
|
||||
buf.WriteString(requiredLandmarkAlternativeDescription(alt))
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func requiredLandmarkAlternativeDescription(alt RequiredLandmarkAlternative) string {
|
||||
core := ""
|
||||
switch {
|
||||
case len(alt.Literal) > 0:
|
||||
core = fmt.Sprintf("literal=%q", string(alt.Literal))
|
||||
case alt.Set != nil:
|
||||
core = fmt.Sprintf("set=%s repeat=%d..%d", alt.Set.String(), alt.MinRepeat, alt.MaxRepeat)
|
||||
default:
|
||||
core = "<empty>"
|
||||
}
|
||||
if alt.LeadingWhitespaceSet != nil {
|
||||
core += fmt.Sprintf(" leadingWhitespace=%s required=%t", alt.LeadingWhitespaceSet.String(), alt.RequireWhitespaceBefore)
|
||||
}
|
||||
if alt.TrailingWhitespaceSet != nil {
|
||||
core += fmt.Sprintf(" trailingWhitespace=%s required=%t", alt.TrailingWhitespaceSet.String(), alt.RequireWhitespaceAfter)
|
||||
}
|
||||
return core
|
||||
}
|
||||
|
||||
func newFindOptimizations(tree *RegexTree, opt ParseOptions) *FindOptimizations {
|
||||
f := newFindOptimizationsForNode(tree.Root, opt, false)
|
||||
|
||||
if !f.rightToLeft && !f.isUseful() {
|
||||
if positiveLookahead, _ := findLeadingPositiveLookahead(tree.Root); positiveLookahead != nil {
|
||||
positiveLookaheadOpts := newFindOptimizationsForNode(positiveLookahead.Children[0], opt, true)
|
||||
|
||||
// Lookaheads don't currently factor into the whole-pattern length analysis,
|
||||
// as they can overlap with the rest of the expression. Keep the whole-pattern
|
||||
// minimum if it's larger, and preserve any max from the original expression.
|
||||
positiveLookaheadOpts.MinRequiredLength = max(f.MinRequiredLength, positiveLookaheadOpts.MinRequiredLength)
|
||||
positiveLookaheadOpts.MaxPossibleLength = f.MaxPossibleLength
|
||||
f = positiveLookaheadOpts
|
||||
}
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func newFindOptimizationsForNode(root *RegexNode, opt ParseOptions, isLeadingPartial bool) *FindOptimizations {
|
||||
f := &FindOptimizations{
|
||||
rightToLeft: opt.RegexOptions&RightToLeft != 0,
|
||||
MinRequiredLength: root.ComputeMinLength(),
|
||||
LeadingAnchor: findLeadingOrTrailingAnchor(root, true),
|
||||
MaxPossibleLength: -1,
|
||||
}
|
||||
|
||||
if f.rightToLeft && f.LeadingAnchor == NtBol {
|
||||
// Filter out Bol for RightToLeft, as we don't currently optimize for it.
|
||||
f.LeadingAnchor = NtUnknown
|
||||
}
|
||||
|
||||
f.FindMode = getFindMode(f.rightToLeft, f.LeadingAnchor)
|
||||
if f.FindMode != NoSearch {
|
||||
return f
|
||||
}
|
||||
|
||||
// Compute any anchor trailing the expression. If there is one, and we can also compute a fixed length
|
||||
// for the whole expression, we can use that to quickly jump to the right location in the input.
|
||||
if !f.rightToLeft && !isLeadingPartial {
|
||||
f.TrailingAnchor = findLeadingOrTrailingAnchor(root, false)
|
||||
if f.TrailingAnchor == NtEnd || f.TrailingAnchor == NtEndZ {
|
||||
f.MaxPossibleLength = root.computeMaxLength()
|
||||
if f.MinRequiredLength == f.MaxPossibleLength {
|
||||
if f.TrailingAnchor == NtEnd {
|
||||
f.FindMode = TrailingAnchor_FixedLength_LeftToRight_End
|
||||
} else {
|
||||
f.FindMode = TrailingAnchor_FixedLength_LeftToRight_EndZ
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's a leading substring, just use IndexOf and inherit all of its optimizations.
|
||||
prefix := findPrefix(root)
|
||||
if len(prefix) > 1 {
|
||||
f.LeadingPrefix = prefix
|
||||
if f.rightToLeft {
|
||||
f.FindMode = LeadingString_RightToLeft
|
||||
} else {
|
||||
f.FindMode = LeadingString_LeftToRight
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// At this point there are no fast-searchable anchors or case-sensitive prefixes. We can now analyze the
|
||||
// pattern for sets and then use any found sets to determine what kind of search to perform.
|
||||
|
||||
// If we're generating code, then the code generation process already handles sets that reduce to a single literal,
|
||||
// so we can simplify and just always go for the sets.
|
||||
dfa := false //(opt&NonBacktracking) != 0
|
||||
codeGen := opt.CodeGen && !dfa // for now, we never generate code for NonBacktracking, so treat it as non-codegen
|
||||
interpreter := !codeGen && !dfa
|
||||
//usesRfoTryFind := !codeGen
|
||||
|
||||
// For interpreter, we want to employ optimizations, but we don't want to make construction significantly
|
||||
// more expensive. regexp2cg can opt into more expensive analysis with OptionIsCodeGen. So for the
|
||||
// interpreter we focus only on creating a set for the first character. Same for right-to-left, which
|
||||
// is used very rarely and thus we don't need to invest in special-casing it.
|
||||
if f.rightToLeft {
|
||||
// Determine a set for anything that can possibly start the expression.
|
||||
set := findFirstCharClass(root)
|
||||
if set != nil {
|
||||
var chars []rune
|
||||
if !set.IsNegated() {
|
||||
// See if the set is limited to holding only a few characters.
|
||||
chars = set.GetSetChars(5)
|
||||
}
|
||||
|
||||
if !codeGen && len(chars) == 1 {
|
||||
// The set contains one and only one character, meaning every match starts
|
||||
// with the same literal value (potentially case-insensitive). Search for that.
|
||||
f.FixedDistanceLiteral.C = chars[0]
|
||||
f.FindMode = LeadingChar_RightToLeft
|
||||
} else {
|
||||
// The set may match multiple characters. Search for that.
|
||||
f.FixedDistanceSets = []FixedDistanceSet{{
|
||||
Chars: chars,
|
||||
Set: set,
|
||||
Distance: 0,
|
||||
}}
|
||||
f.FindMode = LeadingSet_RightToLeft
|
||||
f.asciiLookups = make([][]uint, 1)
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// We're now left-to-right only.
|
||||
|
||||
prefix = findPrefixOrdinalCaseInsensitive(root)
|
||||
if len(prefix) > 1 {
|
||||
f.LeadingPrefix = prefix
|
||||
f.FindMode = LeadingString_OrdinalIgnoreCase_LeftToRight
|
||||
return f
|
||||
}
|
||||
|
||||
// We're now left-to-right only and looking for multiple prefixes and/or sets.
|
||||
|
||||
// If there are multiple leading strings, we can search for any of them.
|
||||
// this works in the interpreter, but we avoid it due to additional cost during construction
|
||||
|
||||
if !interpreter {
|
||||
ciPrefixes := findPrefixes(root, true)
|
||||
if len(ciPrefixes) > 1 {
|
||||
f.LeadingPrefixes = ciPrefixes
|
||||
f.LeadingPrefixesRunes = toRunePrefixes(ciPrefixes)
|
||||
f.FindMode = LeadingStrings_OrdinalIgnoreCase_LeftToRight
|
||||
/*SYSTEM_TEXT_REGULAREXPRESSIONS
|
||||
if usesRfoTryFind {
|
||||
f.LeadingStrings = helpers.NewSearchValues(f.LeadingPrefixes, true)
|
||||
}*/
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// Build up a list of all of the sets that are a fixed distance from the start of the expression.
|
||||
fixedDistanceSets := findFixedDistanceSets(root, !interpreter)
|
||||
|
||||
// See if we can make a string of at least two characters long out of those sets. We should have already caught
|
||||
// one at the beginning of the pattern, but there may be one hiding at a non-zero fixed distance into the pattern.
|
||||
if len(fixedDistanceSets) > 0 {
|
||||
bestFixedDistanceString := findFixedDistanceString(fixedDistanceSets)
|
||||
if bestFixedDistanceString != nil {
|
||||
f.FindMode = FixedDistanceString_LeftToRight
|
||||
f.FixedDistanceLiteral = *bestFixedDistanceString
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// A landmark chain is more selective than a single leading set for shapes like
|
||||
// /name-separator host-separator domain/. Prefer it before falling back to
|
||||
// one-position or one-literal candidate searches.
|
||||
if landmarkChain := findRequiredLandmarkChain(root); landmarkChain != nil {
|
||||
f.FindMode = RequiredLandmarkChain_LeftToRight
|
||||
f.LandmarkChain = landmarkChain
|
||||
return f
|
||||
}
|
||||
|
||||
// As a backup, see if we can find a literal after a leading atomic loop. That might be better than whatever sets we find, so
|
||||
// we want to know whether we have one in our pocket before deciding whether to use a leading set (we'll prefer a leading
|
||||
// set if it's something for which we can search efficiently).
|
||||
literalAfterLoop := findLiteralFollowingLeadingLoop(root)
|
||||
|
||||
// If we got such sets, we'll likely use them. However, if the best of them is something that doesn't support an efficient
|
||||
// search and we did successfully find a literal after an atomic loop we could search instead, we prefer the efficient search.
|
||||
// For example, if we have a negated set, we will still prefer the literal-after-an-atomic-loop because negated sets typically
|
||||
// contain _many_ characters (e.g. [^a] is everything but 'a') and are thus more likely to very quickly match, which means any
|
||||
// vectorization employed is less likely to kick in and be worth the startup overhead.
|
||||
if len(fixedDistanceSets) > 0 {
|
||||
// Sort the sets by "quality", such that whatever set is first is the one deemed most efficient to use.
|
||||
// In some searches, we may use multiple sets, so we want the subsequent ones to also be the efficiency runners-up.
|
||||
slices.SortFunc(fixedDistanceSets, compareFixedDistanceSetsByQuality)
|
||||
|
||||
// If the best fixed-distance set is composed of high-frequency characters, IndexOfAny on
|
||||
// those characters is likely to match too many positions. Prefer a case-sensitive
|
||||
// multi-prefix search when one is available.
|
||||
if !interpreter && !mayContainCaseInsensitiveMatching(root) && hasHighFrequencyChars(fixedDistanceSets[0]) {
|
||||
caseSensitivePrefixes := findPrefixes(root, false)
|
||||
if len(caseSensitivePrefixes) > 1 {
|
||||
f.LeadingPrefixes = caseSensitivePrefixes
|
||||
f.LeadingPrefixesRunes = toRunePrefixes(caseSensitivePrefixes)
|
||||
f.FindMode = LeadingStrings_LeftToRight
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no literal after the loop, use whatever set we got.
|
||||
// If there is a literal after the loop, consider it to be better than a negated set and better than a set with many characters.
|
||||
if literalAfterLoop == nil || (len(fixedDistanceSets[0].Chars) > 0 && !fixedDistanceSets[0].Negated) {
|
||||
// Determine whether to do searching based on one or more sets or on a single literal. Code-generated engines
|
||||
// don't need to special-case literals as they already do codegen to create the optimal lookup based on
|
||||
// the set's characteristics.
|
||||
if !codeGen && len(fixedDistanceSets) == 1 && len(fixedDistanceSets[0].Chars) == 1 &&
|
||||
!fixedDistanceSets[0].Negated {
|
||||
f.FixedDistanceLiteral = FixedDistanceLiteral{
|
||||
C: fixedDistanceSets[0].Chars[0],
|
||||
Distance: fixedDistanceSets[0].Distance,
|
||||
}
|
||||
f.FindMode = FixedDistanceChar_LeftToRight
|
||||
} else {
|
||||
// Limit how many sets we use to avoid doing lots of unnecessary work. The list was already
|
||||
// sorted from best to worst, so just keep the first ones up to our limit.
|
||||
const MaxSetsToUse = 3 // arbitrary tuned limit
|
||||
if len(fixedDistanceSets) > MaxSetsToUse {
|
||||
fixedDistanceSets = fixedDistanceSets[:MaxSetsToUse]
|
||||
}
|
||||
|
||||
// Store the sets, and compute which mode to use.
|
||||
f.FixedDistanceSets = fixedDistanceSets
|
||||
if len(fixedDistanceSets) == 1 && fixedDistanceSets[0].Distance == 0 {
|
||||
f.FindMode = LeadingSet_LeftToRight
|
||||
} else {
|
||||
f.FindMode = FixedDistanceSets_LeftToRight
|
||||
}
|
||||
f.asciiLookups = make([][]uint, len(fixedDistanceSets))
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a literal we can search for after a leading set loop, use it.
|
||||
if literalAfterLoop != nil {
|
||||
f.FindMode = LiteralAfterLoop_LeftToRight
|
||||
f.LiteralAfterLoop = literalAfterLoop
|
||||
f.asciiLookups = make([][]uint, 1)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *FindOptimizations) isUseful() bool {
|
||||
return f.FindMode != NoSearch || f.LeadingAnchor == NtBol
|
||||
}
|
||||
|
||||
func toRunePrefixes(prefixes []string) [][]rune {
|
||||
if len(prefixes) == 0 {
|
||||
return nil
|
||||
}
|
||||
runes := make([][]rune, len(prefixes))
|
||||
for i, prefix := range prefixes {
|
||||
runes[i] = []rune(prefix)
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
func getFindMode(rtl bool, t NodeType) FindNextStartingPositionMode {
|
||||
if rtl {
|
||||
switch t {
|
||||
case NtBeginning:
|
||||
return LeadingAnchor_RightToLeft_Beginning
|
||||
case NtStart:
|
||||
return LeadingAnchor_RightToLeft_Start
|
||||
case NtEnd:
|
||||
return LeadingAnchor_RightToLeft_End
|
||||
case NtEndZ:
|
||||
return LeadingAnchor_RightToLeft_EndZ
|
||||
}
|
||||
} else {
|
||||
switch t {
|
||||
case NtBeginning:
|
||||
return LeadingAnchor_LeftToRight_Beginning
|
||||
case NtStart:
|
||||
return LeadingAnchor_LeftToRight_Start
|
||||
case NtEnd:
|
||||
return LeadingAnchor_LeftToRight_End
|
||||
case NtEndZ:
|
||||
return LeadingAnchor_LeftToRight_EndZ
|
||||
}
|
||||
}
|
||||
|
||||
return NoSearch
|
||||
}
|
||||
|
||||
// Analyzes a list of fixed-distance sets to extract a case-sensitive string at a fixed distance.</summary>
|
||||
func findFixedDistanceString(fixedDistanceSets []FixedDistanceSet) *FixedDistanceLiteral {
|
||||
var best *FixedDistanceLiteral
|
||||
|
||||
// A result string must be at least two characters in length; therefore we require at least that many sets.
|
||||
if len(fixedDistanceSets) >= 2 {
|
||||
// We're walking the sets from beginning to end, so we need them sorted by distance.
|
||||
slices.SortFunc(fixedDistanceSets, func(s1, s2 FixedDistanceSet) int {
|
||||
return cmp.Compare(s1.Distance, s2.Distance)
|
||||
})
|
||||
|
||||
vsb := &bytes.Buffer{}
|
||||
|
||||
// Looking for strings of length >= 2
|
||||
start := -1
|
||||
for i := 0; i < len(fixedDistanceSets)+1; i++ {
|
||||
chars := []rune(nil)
|
||||
if i < len(fixedDistanceSets) {
|
||||
chars = fixedDistanceSets[i].Chars
|
||||
}
|
||||
invalidChars := len(chars) != 1 || fixedDistanceSets[i].Negated
|
||||
|
||||
// If the current set ends a sequence (or we've walked off the end), see whether
|
||||
// what we've gathered constitues a valid string, and if it's better than the
|
||||
// best we've already seen, store it. Regardless, reset the sequence in order
|
||||
// to continue analyzing.
|
||||
if invalidChars || (i > 0 && fixedDistanceSets[i].Distance != fixedDistanceSets[i-1].Distance+1) {
|
||||
bestLen := 2
|
||||
if best != nil {
|
||||
bestLen = len(best.S)
|
||||
}
|
||||
if start != -1 && i-start >= bestLen {
|
||||
best = &FixedDistanceLiteral{
|
||||
S: vsb.String(),
|
||||
Distance: fixedDistanceSets[start].Distance,
|
||||
}
|
||||
}
|
||||
|
||||
vsb.Reset()
|
||||
start = -1
|
||||
if invalidChars {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if start == -1 {
|
||||
start = i
|
||||
}
|
||||
|
||||
vsb.WriteRune(chars[0])
|
||||
}
|
||||
}
|
||||
|
||||
return best
|
||||
}
|
||||
Generated
Vendored
+480
-213
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+162
-115
@@ -45,19 +45,19 @@ type regexFcd struct {
|
||||
* and after each child of an interior node, and at each leaf.
|
||||
*/
|
||||
func (s *regexFcd) regexFCFromRegexTree(tree *RegexTree) *regexFc {
|
||||
curNode := tree.root
|
||||
curNode := tree.Root
|
||||
curChild := 0
|
||||
|
||||
for {
|
||||
if len(curNode.children) == 0 {
|
||||
if len(curNode.Children) == 0 {
|
||||
// This is a leaf node
|
||||
s.calculateFC(curNode.t, curNode, 0)
|
||||
} else if curChild < len(curNode.children) && !s.skipAllChildren {
|
||||
s.calculateFC(curNode.T, curNode, 0)
|
||||
} else if curChild < len(curNode.Children) && !s.skipAllChildren {
|
||||
// This is an interior node, and we have more children to analyze
|
||||
s.calculateFC(curNode.t|beforeChild, curNode, curChild)
|
||||
s.calculateFC(curNode.T|BeforeChild, curNode, curChild)
|
||||
|
||||
if !s.skipchild {
|
||||
curNode = curNode.children[curChild]
|
||||
curNode = curNode.Children[curChild]
|
||||
// this stack is how we get a depth first walk of the tree.
|
||||
s.pushInt(curChild)
|
||||
curChild = 0
|
||||
@@ -77,9 +77,9 @@ func (s *regexFcd) regexFCFromRegexTree(tree *RegexTree) *regexFc {
|
||||
}
|
||||
|
||||
curChild = s.popInt()
|
||||
curNode = curNode.next
|
||||
curNode = curNode.Parent
|
||||
|
||||
s.calculateFC(curNode.t|afterChild, curNode, curChild)
|
||||
s.calculateFC(curNode.T|AfterChild, curNode, curChild)
|
||||
if s.failed {
|
||||
return nil
|
||||
}
|
||||
@@ -153,35 +153,32 @@ func (s *regexFcd) skipChild() {
|
||||
}
|
||||
|
||||
// FC computation and shortcut cases for each node type
|
||||
func (s *regexFcd) calculateFC(nt nodeType, node *regexNode, CurIndex int) {
|
||||
func (s *regexFcd) calculateFC(nt NodeType, node *RegexNode, CurIndex int) {
|
||||
//fmt.Printf("NodeType: %v, CurIndex: %v, Desc: %v\n", nt, CurIndex, node.description())
|
||||
ci := false
|
||||
rtl := false
|
||||
|
||||
if nt <= ntRef {
|
||||
if (node.options & IgnoreCase) != 0 {
|
||||
if nt <= NtRef {
|
||||
if (node.Options & IgnoreCase) != 0 {
|
||||
ci = true
|
||||
}
|
||||
if (node.options & RightToLeft) != 0 {
|
||||
if (node.Options & RightToLeft) != 0 {
|
||||
rtl = true
|
||||
}
|
||||
}
|
||||
|
||||
switch nt {
|
||||
case ntConcatenate | beforeChild, ntAlternate | beforeChild, ntTestref | beforeChild, ntLoop | beforeChild, ntLazyloop | beforeChild:
|
||||
break
|
||||
case NtConcatenate | BeforeChild, NtAlternate | BeforeChild, NtBackRefCond | BeforeChild, NtLoop | BeforeChild, NtLazyloop | BeforeChild:
|
||||
|
||||
case ntTestgroup | beforeChild:
|
||||
case NtExprCond | BeforeChild:
|
||||
if CurIndex == 0 {
|
||||
s.skipChild()
|
||||
}
|
||||
break
|
||||
|
||||
case ntEmpty:
|
||||
case NtEmpty:
|
||||
s.pushFC(regexFc{nullable: true})
|
||||
break
|
||||
|
||||
case ntConcatenate | afterChild:
|
||||
case NtConcatenate | AfterChild:
|
||||
if CurIndex != 0 {
|
||||
child := s.popFC()
|
||||
cumul := s.topFC()
|
||||
@@ -193,81 +190,66 @@ func (s *regexFcd) calculateFC(nt nodeType, node *regexNode, CurIndex int) {
|
||||
if !fc.nullable {
|
||||
s.skipAllChildren = true
|
||||
}
|
||||
break
|
||||
|
||||
case ntTestgroup | afterChild:
|
||||
case NtExprCond | AfterChild:
|
||||
if CurIndex > 1 {
|
||||
child := s.popFC()
|
||||
cumul := s.topFC()
|
||||
|
||||
s.failed = !cumul.addFC(*child, false)
|
||||
}
|
||||
break
|
||||
|
||||
case ntAlternate | afterChild, ntTestref | afterChild:
|
||||
case NtAlternate | AfterChild, NtBackRefCond | AfterChild:
|
||||
if CurIndex != 0 {
|
||||
child := s.popFC()
|
||||
cumul := s.topFC()
|
||||
|
||||
s.failed = !cumul.addFC(*child, false)
|
||||
}
|
||||
break
|
||||
|
||||
case ntLoop | afterChild, ntLazyloop | afterChild:
|
||||
if node.m == 0 {
|
||||
case NtLoop | AfterChild, NtLazyloop | AfterChild:
|
||||
if node.M == 0 {
|
||||
fc := s.topFC()
|
||||
fc.nullable = true
|
||||
}
|
||||
break
|
||||
|
||||
case ntGroup | beforeChild, ntGroup | afterChild, ntCapture | beforeChild, ntCapture | afterChild, ntGreedy | beforeChild, ntGreedy | afterChild:
|
||||
break
|
||||
case NtGroup | BeforeChild, NtGroup | AfterChild, NtCapture | BeforeChild, NtCapture | AfterChild, NtAtomic | BeforeChild, NtAtomic | AfterChild:
|
||||
|
||||
case ntRequire | beforeChild, ntPrevent | beforeChild:
|
||||
case NtPosLook | BeforeChild, NtNegLook | BeforeChild:
|
||||
s.skipChild()
|
||||
s.pushFC(regexFc{nullable: true})
|
||||
break
|
||||
|
||||
case ntRequire | afterChild, ntPrevent | afterChild:
|
||||
break
|
||||
case NtPosLook | AfterChild, NtNegLook | AfterChild:
|
||||
|
||||
case ntOne, ntNotone:
|
||||
s.pushFC(newRegexFc(node.ch, nt == ntNotone, false, ci))
|
||||
break
|
||||
case NtOne, NtNotone:
|
||||
s.pushFC(newRegexFc(node.Ch, nt == NtNotone, false, ci))
|
||||
|
||||
case ntOneloop, ntOnelazy:
|
||||
s.pushFC(newRegexFc(node.ch, false, node.m == 0, ci))
|
||||
break
|
||||
case NtOneloop, NtOnelazy, NtOneloopatomic:
|
||||
s.pushFC(newRegexFc(node.Ch, false, node.M == 0, ci))
|
||||
|
||||
case ntNotoneloop, ntNotonelazy:
|
||||
s.pushFC(newRegexFc(node.ch, true, node.m == 0, ci))
|
||||
break
|
||||
case NtNotoneloop, NtNotonelazy, NtNotoneloopatomic:
|
||||
s.pushFC(newRegexFc(node.Ch, true, node.M == 0, ci))
|
||||
|
||||
case ntMulti:
|
||||
if len(node.str) == 0 {
|
||||
case NtMulti:
|
||||
if len(node.Str) == 0 {
|
||||
s.pushFC(regexFc{nullable: true})
|
||||
} else if !rtl {
|
||||
s.pushFC(newRegexFc(node.str[0], false, false, ci))
|
||||
s.pushFC(newRegexFc(node.Str[0], false, false, ci))
|
||||
} else {
|
||||
s.pushFC(newRegexFc(node.str[len(node.str)-1], false, false, ci))
|
||||
s.pushFC(newRegexFc(node.Str[len(node.Str)-1], false, false, ci))
|
||||
}
|
||||
break
|
||||
|
||||
case ntSet:
|
||||
s.pushFC(regexFc{cc: node.set.Copy(), nullable: false, caseInsensitive: ci})
|
||||
break
|
||||
case NtSet:
|
||||
s.pushFC(regexFc{cc: node.Set.Copy(), nullable: false, caseInsensitive: ci})
|
||||
|
||||
case ntSetloop, ntSetlazy:
|
||||
s.pushFC(regexFc{cc: node.set.Copy(), nullable: node.m == 0, caseInsensitive: ci})
|
||||
break
|
||||
case NtSetloop, NtSetlazy, NtSetloopatomic:
|
||||
s.pushFC(regexFc{cc: node.Set.Copy(), nullable: node.M == 0, caseInsensitive: ci})
|
||||
|
||||
case ntRef:
|
||||
case NtRef:
|
||||
s.pushFC(regexFc{cc: *AnyClass(), nullable: true, caseInsensitive: false})
|
||||
break
|
||||
|
||||
case ntNothing, ntBol, ntEol, ntBoundary, ntNonboundary, ntECMABoundary, ntNonECMABoundary, ntBeginning, ntStart, ntEndZ, ntEnd:
|
||||
case NtNothing, NtBol, NtEol, NtBoundary, NtNonboundary, NtECMABoundary, NtNonECMABoundary, NtBeginning, NtStart, NtEndZ, NtEnd, NtUpdateBumpalong:
|
||||
s.pushFC(regexFc{nullable: true})
|
||||
break
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected op code: %v", nt))
|
||||
@@ -334,57 +316,57 @@ func (r *regexFc) addFC(fc regexFc, concatenate bool) bool {
|
||||
// This is a related computation: it takes a RegexTree and computes the
|
||||
// leading substring if it sees one. It's quite trivial and gives up easily.
|
||||
func getPrefix(tree *RegexTree) *Prefix {
|
||||
var concatNode *regexNode
|
||||
var concatNode *RegexNode
|
||||
nextChild := 0
|
||||
|
||||
curNode := tree.root
|
||||
curNode := tree.Root
|
||||
|
||||
for {
|
||||
switch curNode.t {
|
||||
case ntConcatenate:
|
||||
if len(curNode.children) > 0 {
|
||||
switch curNode.T {
|
||||
case NtConcatenate:
|
||||
if len(curNode.Children) > 0 {
|
||||
concatNode = curNode
|
||||
nextChild = 0
|
||||
}
|
||||
|
||||
case ntGreedy, ntCapture:
|
||||
curNode = curNode.children[0]
|
||||
case NtAtomic, NtCapture:
|
||||
curNode = curNode.Children[0]
|
||||
concatNode = nil
|
||||
continue
|
||||
|
||||
case ntOneloop, ntOnelazy:
|
||||
if curNode.m > 0 {
|
||||
case NtOneloop, NtOnelazy:
|
||||
if curNode.M > 0 {
|
||||
return &Prefix{
|
||||
PrefixStr: repeat(curNode.ch, curNode.m),
|
||||
CaseInsensitive: (curNode.options & IgnoreCase) != 0,
|
||||
PrefixStr: repeat(curNode.Ch, curNode.M),
|
||||
CaseInsensitive: (curNode.Options & IgnoreCase) != 0,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case ntOne:
|
||||
case NtOne:
|
||||
return &Prefix{
|
||||
PrefixStr: []rune{curNode.ch},
|
||||
CaseInsensitive: (curNode.options & IgnoreCase) != 0,
|
||||
PrefixStr: []rune{curNode.Ch},
|
||||
CaseInsensitive: (curNode.Options & IgnoreCase) != 0,
|
||||
}
|
||||
|
||||
case ntMulti:
|
||||
case NtMulti:
|
||||
return &Prefix{
|
||||
PrefixStr: curNode.str,
|
||||
CaseInsensitive: (curNode.options & IgnoreCase) != 0,
|
||||
PrefixStr: curNode.Str,
|
||||
CaseInsensitive: (curNode.Options & IgnoreCase) != 0,
|
||||
}
|
||||
|
||||
case ntBol, ntEol, ntBoundary, ntECMABoundary, ntBeginning, ntStart,
|
||||
ntEndZ, ntEnd, ntEmpty, ntRequire, ntPrevent:
|
||||
case NtBol, NtEol, NtBoundary, NtECMABoundary, NtBeginning, NtStart,
|
||||
NtEndZ, NtEnd, NtEmpty, NtPosLook, NtNegLook:
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if concatNode == nil || nextChild >= len(concatNode.children) {
|
||||
if concatNode == nil || nextChild >= len(concatNode.Children) {
|
||||
return nil
|
||||
}
|
||||
|
||||
curNode = concatNode.children[nextChild]
|
||||
curNode = concatNode.Children[nextChild]
|
||||
nextChild++
|
||||
}
|
||||
}
|
||||
@@ -788,71 +770,71 @@ type AnchorLoc int16
|
||||
// where the regex can be pegged
|
||||
const (
|
||||
AnchorBeginning AnchorLoc = 0x0001
|
||||
AnchorBol = 0x0002
|
||||
AnchorStart = 0x0004
|
||||
AnchorEol = 0x0008
|
||||
AnchorEndZ = 0x0010
|
||||
AnchorEnd = 0x0020
|
||||
AnchorBoundary = 0x0040
|
||||
AnchorECMABoundary = 0x0080
|
||||
AnchorBol AnchorLoc = 0x0002
|
||||
AnchorStart AnchorLoc = 0x0004
|
||||
AnchorEol AnchorLoc = 0x0008
|
||||
AnchorEndZ AnchorLoc = 0x0010
|
||||
AnchorEnd AnchorLoc = 0x0020
|
||||
AnchorBoundary AnchorLoc = 0x0040
|
||||
AnchorECMABoundary AnchorLoc = 0x0080
|
||||
)
|
||||
|
||||
func getAnchors(tree *RegexTree) AnchorLoc {
|
||||
|
||||
var concatNode *regexNode
|
||||
var concatNode *RegexNode
|
||||
nextChild, result := 0, AnchorLoc(0)
|
||||
|
||||
curNode := tree.root
|
||||
curNode := tree.Root
|
||||
|
||||
for {
|
||||
switch curNode.t {
|
||||
case ntConcatenate:
|
||||
if len(curNode.children) > 0 {
|
||||
switch curNode.T {
|
||||
case NtConcatenate:
|
||||
if len(curNode.Children) > 0 {
|
||||
concatNode = curNode
|
||||
nextChild = 0
|
||||
}
|
||||
|
||||
case ntGreedy, ntCapture:
|
||||
curNode = curNode.children[0]
|
||||
case NtAtomic, NtCapture:
|
||||
curNode = curNode.Children[0]
|
||||
concatNode = nil
|
||||
continue
|
||||
|
||||
case ntBol, ntEol, ntBoundary, ntECMABoundary, ntBeginning,
|
||||
ntStart, ntEndZ, ntEnd:
|
||||
return result | anchorFromType(curNode.t)
|
||||
case NtBol, NtEol, NtBoundary, NtECMABoundary, NtBeginning,
|
||||
NtStart, NtEndZ, NtEnd:
|
||||
return result | anchorFromType(curNode.T)
|
||||
|
||||
case ntEmpty, ntRequire, ntPrevent:
|
||||
case NtEmpty, NtPosLook, NtNegLook:
|
||||
|
||||
default:
|
||||
return result
|
||||
}
|
||||
|
||||
if concatNode == nil || nextChild >= len(concatNode.children) {
|
||||
if concatNode == nil || nextChild >= len(concatNode.Children) {
|
||||
return result
|
||||
}
|
||||
|
||||
curNode = concatNode.children[nextChild]
|
||||
curNode = concatNode.Children[nextChild]
|
||||
nextChild++
|
||||
}
|
||||
}
|
||||
|
||||
func anchorFromType(t nodeType) AnchorLoc {
|
||||
func anchorFromType(t NodeType) AnchorLoc {
|
||||
switch t {
|
||||
case ntBol:
|
||||
case NtBol:
|
||||
return AnchorBol
|
||||
case ntEol:
|
||||
case NtEol:
|
||||
return AnchorEol
|
||||
case ntBoundary:
|
||||
case NtBoundary:
|
||||
return AnchorBoundary
|
||||
case ntECMABoundary:
|
||||
case NtECMABoundary:
|
||||
return AnchorECMABoundary
|
||||
case ntBeginning:
|
||||
case NtBeginning:
|
||||
return AnchorBeginning
|
||||
case ntStart:
|
||||
case NtStart:
|
||||
return AnchorStart
|
||||
case ntEndZ:
|
||||
case NtEndZ:
|
||||
return AnchorEndZ
|
||||
case ntEnd:
|
||||
case NtEnd:
|
||||
return AnchorEnd
|
||||
default:
|
||||
return 0
|
||||
@@ -863,28 +845,28 @@ func anchorFromType(t nodeType) AnchorLoc {
|
||||
func (anchors AnchorLoc) String() string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
if 0 != (anchors & AnchorBeginning) {
|
||||
if (anchors & AnchorBeginning) != 0 {
|
||||
buf.WriteString(", Beginning")
|
||||
}
|
||||
if 0 != (anchors & AnchorStart) {
|
||||
if (anchors & AnchorStart) != 0 {
|
||||
buf.WriteString(", Start")
|
||||
}
|
||||
if 0 != (anchors & AnchorBol) {
|
||||
if (anchors & AnchorBol) != 0 {
|
||||
buf.WriteString(", Bol")
|
||||
}
|
||||
if 0 != (anchors & AnchorBoundary) {
|
||||
if (anchors & AnchorBoundary) != 0 {
|
||||
buf.WriteString(", Boundary")
|
||||
}
|
||||
if 0 != (anchors & AnchorECMABoundary) {
|
||||
if (anchors & AnchorECMABoundary) != 0 {
|
||||
buf.WriteString(", ECMABoundary")
|
||||
}
|
||||
if 0 != (anchors & AnchorEol) {
|
||||
if (anchors & AnchorEol) != 0 {
|
||||
buf.WriteString(", Eol")
|
||||
}
|
||||
if 0 != (anchors & AnchorEnd) {
|
||||
if (anchors & AnchorEnd) != 0 {
|
||||
buf.WriteString(", End")
|
||||
}
|
||||
if 0 != (anchors & AnchorEndZ) {
|
||||
if (anchors & AnchorEndZ) != 0 {
|
||||
buf.WriteString(", EndZ")
|
||||
}
|
||||
|
||||
@@ -894,3 +876,68 @@ func (anchors AnchorLoc) String() string {
|
||||
}
|
||||
return "None"
|
||||
}
|
||||
|
||||
func findLeadingOrTrailingAnchor(node *RegexNode, leading bool) NodeType {
|
||||
for {
|
||||
switch node.T {
|
||||
case NtBol, NtEol, NtBeginning, NtStart, NtEndZ, NtEnd, NtBoundary, NtECMABoundary:
|
||||
// anchor found
|
||||
return node.T
|
||||
case NtAtomic, NtCapture:
|
||||
// For groups, continue exploring the sole child.
|
||||
node = node.Children[0]
|
||||
continue
|
||||
case NtConcatenate:
|
||||
// For concatenations, we expect primarily to explore its first (for leading) or last (for trailing) child,
|
||||
// but we can also skip over certain kinds of nodes (e.g. Empty), and thus iterate through its children backward
|
||||
// looking for the last we shouldn't skip.
|
||||
var child *RegexNode
|
||||
|
||||
if leading {
|
||||
for i := 0; i < len(node.Children); i++ {
|
||||
t := node.Children[i].T
|
||||
if t != NtEmpty && t != NtPosLook && t != NtNegLook {
|
||||
child = node.Children[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := len(node.Children) - 1; i >= 0; i-- {
|
||||
t := node.Children[i].T
|
||||
if t != NtEmpty && t != NtPosLook && t != NtNegLook {
|
||||
child = node.Children[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if child != nil {
|
||||
node = child
|
||||
continue
|
||||
}
|
||||
|
||||
case NtAlternate:
|
||||
// For alternations, every branch needs to lead or trail with the same anchor.
|
||||
|
||||
// Get the leading/trailing anchor of the first branch. If there isn't one, bail.
|
||||
anchor := findLeadingOrTrailingAnchor(node.Children[0], leading)
|
||||
if anchor == NtUnknown {
|
||||
return NtUnknown
|
||||
}
|
||||
|
||||
// Look at each subsequent branch and validate it has the same leading or trailing
|
||||
// anchor. If any doesn't, bail.
|
||||
for i := 1; i < len(node.Children); i++ {
|
||||
if findLeadingOrTrailingAnchor(node.Children[i], leading) != anchor {
|
||||
return NtUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// All branches have the same leading/trailing anchor. Return it.
|
||||
return anchor
|
||||
|
||||
}
|
||||
|
||||
// no anchor
|
||||
return NtUnknown
|
||||
}
|
||||
}
|
||||
+1491
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+10
-10
@@ -19,8 +19,8 @@ const (
|
||||
replaceWholeString = -4
|
||||
)
|
||||
|
||||
//ErrReplacementError is a general error during parsing the replacement text
|
||||
var ErrReplacementError = errors.New("Replacement pattern error.")
|
||||
// ErrReplacementError is a general error during parsing the replacement text
|
||||
var ErrReplacementError = errors.New("replacement pattern error")
|
||||
|
||||
// NewReplacerData will populate a reusable replacer data struct based on the given replacement string
|
||||
// and the capture group data from a regexp
|
||||
@@ -37,7 +37,7 @@ func NewReplacerData(rep string, caps map[int]int, capsize int, capnames map[str
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if concat.t != ntConcatenate {
|
||||
if concat.T != NtConcatenate {
|
||||
panic(ErrReplacementError)
|
||||
}
|
||||
|
||||
@@ -47,21 +47,21 @@ func NewReplacerData(rep string, caps map[int]int, capsize int, capnames map[str
|
||||
rules []int
|
||||
)
|
||||
|
||||
for _, child := range concat.children {
|
||||
switch child.t {
|
||||
case ntMulti:
|
||||
for _, child := range concat.Children {
|
||||
switch child.T {
|
||||
case NtMulti:
|
||||
child.writeStrToBuf(sb)
|
||||
|
||||
case ntOne:
|
||||
sb.WriteRune(child.ch)
|
||||
case NtOne:
|
||||
sb.WriteRune(child.Ch)
|
||||
|
||||
case ntRef:
|
||||
case NtRef:
|
||||
if sb.Len() > 0 {
|
||||
rules = append(rules, len(strings))
|
||||
strings = append(strings, sb.String())
|
||||
sb.Reset()
|
||||
}
|
||||
slot := child.m
|
||||
slot := child.M
|
||||
|
||||
if len(caps) > 0 && slot >= 0 {
|
||||
slot = caps[slot]
|
||||
+2362
File diff suppressed because it is too large
Load Diff
+6058
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+114
-114
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Write(tree *RegexTree) (*Code, error) {
|
||||
@@ -17,11 +16,6 @@ func Write(tree *RegexTree) (*Code, error) {
|
||||
|
||||
code, err := w.codeFromTree(tree)
|
||||
|
||||
if tree.options&Debug > 0 && code != nil {
|
||||
os.Stdout.WriteString(code.Dump())
|
||||
os.Stdout.WriteString("\n")
|
||||
}
|
||||
|
||||
return code, err
|
||||
}
|
||||
|
||||
@@ -41,8 +35,8 @@ type writer struct {
|
||||
}
|
||||
|
||||
const (
|
||||
beforeChild nodeType = 64
|
||||
afterChild = 128
|
||||
BeforeChild NodeType = 64
|
||||
AfterChild NodeType = 128
|
||||
//MaxPrefixSize is the largest number of runes we'll use for a BoyerMoyer prefix
|
||||
MaxPrefixSize = 50
|
||||
)
|
||||
@@ -58,20 +52,20 @@ const (
|
||||
// to just generate the code and grow the array as we go.
|
||||
func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) {
|
||||
var (
|
||||
curNode *regexNode
|
||||
curNode *RegexNode
|
||||
curChild int
|
||||
capsize int
|
||||
)
|
||||
// construct sparse capnum mapping if some numbers are unused
|
||||
|
||||
if tree.capnumlist == nil || tree.captop == len(tree.capnumlist) {
|
||||
capsize = tree.captop
|
||||
if tree.Capnumlist == nil || tree.Captop == len(tree.Capnumlist) {
|
||||
capsize = tree.Captop
|
||||
w.caps = nil
|
||||
} else {
|
||||
capsize = len(tree.capnumlist)
|
||||
w.caps = tree.caps
|
||||
for i := 0; i < len(tree.capnumlist); i++ {
|
||||
w.caps[tree.capnumlist[i]] = i
|
||||
capsize = len(tree.Capnumlist)
|
||||
w.caps = tree.Caps
|
||||
for i := 0; i < len(tree.Capnumlist); i++ {
|
||||
w.caps[tree.Capnumlist[i]] = i
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,18 +76,22 @@ func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) {
|
||||
w.emitted = make([]int, w.count)
|
||||
}
|
||||
|
||||
curNode = tree.root
|
||||
curNode = tree.Root
|
||||
curChild = 0
|
||||
|
||||
w.emit1(Lazybranch, 0)
|
||||
|
||||
for {
|
||||
if len(curNode.children) == 0 {
|
||||
w.emitFragment(curNode.t, curNode, 0)
|
||||
} else if curChild < len(curNode.children) {
|
||||
w.emitFragment(curNode.t|beforeChild, curNode, curChild)
|
||||
if len(curNode.Children) == 0 {
|
||||
if err := w.emitFragment(curNode.T, curNode, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if curChild < len(curNode.Children) {
|
||||
if err := w.emitFragment(curNode.T|BeforeChild, curNode, curChild); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
curNode = curNode.children[curChild]
|
||||
curNode = curNode.Children[curChild]
|
||||
|
||||
w.pushInt(curChild)
|
||||
curChild = 0
|
||||
@@ -105,9 +103,11 @@ func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) {
|
||||
}
|
||||
|
||||
curChild = w.popInt()
|
||||
curNode = curNode.next
|
||||
curNode = curNode.Parent
|
||||
|
||||
w.emitFragment(curNode.t|afterChild, curNode, curChild)
|
||||
if err := w.emitFragment(curNode.T|AfterChild, curNode, curChild); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curChild++
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) {
|
||||
|
||||
fcPrefix := getFirstCharsPrefix(tree)
|
||||
prefix := getPrefix(tree)
|
||||
rtl := (tree.options & RightToLeft) != 0
|
||||
rtl := (tree.Options & RightToLeft) != 0
|
||||
|
||||
var bmPrefix *BmPrefix
|
||||
//TODO: benchmark string prefixes
|
||||
@@ -138,47 +138,46 @@ func (w *writer) codeFromTree(tree *RegexTree) (*Code, error) {
|
||||
}
|
||||
|
||||
return &Code{
|
||||
Codes: w.emitted,
|
||||
Strings: w.stringtable,
|
||||
Sets: w.settable,
|
||||
TrackCount: w.trackcount,
|
||||
Caps: w.caps,
|
||||
Capsize: capsize,
|
||||
FcPrefix: fcPrefix,
|
||||
BmPrefix: bmPrefix,
|
||||
Anchors: getAnchors(tree),
|
||||
RightToLeft: rtl,
|
||||
Codes: w.emitted,
|
||||
Strings: w.stringtable,
|
||||
Sets: w.settable,
|
||||
TrackCount: w.trackcount,
|
||||
Caps: w.caps,
|
||||
Capsize: capsize,
|
||||
FcPrefix: fcPrefix,
|
||||
BmPrefix: bmPrefix,
|
||||
Anchors: getAnchors(tree),
|
||||
RightToLeft: rtl,
|
||||
FindOptimizations: tree.FindOptimizations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The main RegexCode generator. It does a depth-first walk
|
||||
// through the tree and calls EmitFragment to emits code before
|
||||
// and after each child of an interior node, and at each leaf.
|
||||
func (w *writer) emitFragment(nodetype nodeType, node *regexNode, curIndex int) error {
|
||||
func (w *writer) emitFragment(nodetype NodeType, node *RegexNode, curIndex int) error {
|
||||
bits := InstOp(0)
|
||||
|
||||
if nodetype <= ntRef {
|
||||
if (node.options & RightToLeft) != 0 {
|
||||
bits |= Rtl
|
||||
}
|
||||
if (node.options & IgnoreCase) != 0 {
|
||||
bits |= Ci
|
||||
}
|
||||
if (node.Options & RightToLeft) != 0 {
|
||||
bits |= Rtl
|
||||
}
|
||||
ntBits := nodeType(bits)
|
||||
if (node.Options & IgnoreCase) != 0 {
|
||||
bits |= Ci
|
||||
}
|
||||
|
||||
ntBits := NodeType(bits)
|
||||
|
||||
switch nodetype {
|
||||
case ntConcatenate | beforeChild, ntConcatenate | afterChild, ntEmpty:
|
||||
break
|
||||
case NtConcatenate | BeforeChild, NtConcatenate | AfterChild, NtEmpty:
|
||||
|
||||
case ntAlternate | beforeChild:
|
||||
if curIndex < len(node.children)-1 {
|
||||
case NtAlternate | BeforeChild:
|
||||
if curIndex < len(node.Children)-1 {
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Lazybranch, 0)
|
||||
}
|
||||
|
||||
case ntAlternate | afterChild:
|
||||
if curIndex < len(node.children)-1 {
|
||||
case NtAlternate | AfterChild:
|
||||
if curIndex < len(node.Children)-1 {
|
||||
lbPos := w.popInt()
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Goto, 0)
|
||||
@@ -188,32 +187,32 @@ func (w *writer) emitFragment(nodetype nodeType, node *regexNode, curIndex int)
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case ntTestref | beforeChild:
|
||||
case NtBackRefCond | BeforeChild:
|
||||
if curIndex == 0 {
|
||||
w.emit(Setjump)
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Lazybranch, 0)
|
||||
w.emit1(Testref, w.mapCapnum(node.m))
|
||||
w.emit1(Testref, w.mapCapnum(node.M))
|
||||
w.emit(Forejump)
|
||||
}
|
||||
|
||||
case ntTestref | afterChild:
|
||||
if curIndex == 0 {
|
||||
case NtBackRefCond | AfterChild:
|
||||
switch curIndex {
|
||||
case 0:
|
||||
branchpos := w.popInt()
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Goto, 0)
|
||||
w.patchJump(branchpos, w.curPos())
|
||||
w.emit(Forejump)
|
||||
if len(node.children) <= 1 {
|
||||
if len(node.Children) <= 1 {
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
}
|
||||
} else if curIndex == 1 {
|
||||
case 1:
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
}
|
||||
|
||||
case ntTestgroup | beforeChild:
|
||||
case NtExprCond | BeforeChild:
|
||||
if curIndex == 0 {
|
||||
w.emit(Setjump)
|
||||
w.emit(Setmark)
|
||||
@@ -221,143 +220,144 @@ func (w *writer) emitFragment(nodetype nodeType, node *regexNode, curIndex int)
|
||||
w.emit1(Lazybranch, 0)
|
||||
}
|
||||
|
||||
case ntTestgroup | afterChild:
|
||||
if curIndex == 0 {
|
||||
case NtExprCond | AfterChild:
|
||||
switch curIndex {
|
||||
case 0:
|
||||
w.emit(Getmark)
|
||||
w.emit(Forejump)
|
||||
} else if curIndex == 1 {
|
||||
Branchpos := w.popInt()
|
||||
case 1:
|
||||
branchpos := w.popInt()
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Goto, 0)
|
||||
w.patchJump(Branchpos, w.curPos())
|
||||
w.patchJump(branchpos, w.curPos())
|
||||
w.emit(Getmark)
|
||||
w.emit(Forejump)
|
||||
if len(node.children) <= 2 {
|
||||
if len(node.Children) <= 2 {
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
}
|
||||
} else if curIndex == 2 {
|
||||
case 2:
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
}
|
||||
|
||||
case ntLoop | beforeChild, ntLazyloop | beforeChild:
|
||||
case NtLoop | BeforeChild, NtLazyloop | BeforeChild:
|
||||
|
||||
if node.n < math.MaxInt32 || node.m > 1 {
|
||||
if node.m == 0 {
|
||||
if node.N < math.MaxInt32 || node.M > 1 {
|
||||
if node.M == 0 {
|
||||
w.emit1(Nullcount, 0)
|
||||
} else {
|
||||
w.emit1(Setcount, 1-node.m)
|
||||
w.emit1(Setcount, 1-node.M)
|
||||
}
|
||||
} else if node.m == 0 {
|
||||
} else if node.M == 0 {
|
||||
w.emit(Nullmark)
|
||||
} else {
|
||||
w.emit(Setmark)
|
||||
}
|
||||
|
||||
if node.m == 0 {
|
||||
if node.M == 0 {
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Goto, 0)
|
||||
}
|
||||
w.pushInt(w.curPos())
|
||||
|
||||
case ntLoop | afterChild, ntLazyloop | afterChild:
|
||||
case NtLoop | AfterChild, NtLazyloop | AfterChild:
|
||||
|
||||
startJumpPos := w.curPos()
|
||||
lazy := (nodetype - (ntLoop | afterChild))
|
||||
lazy := InstOp(nodetype - (NtLoop | AfterChild))
|
||||
|
||||
if node.n < math.MaxInt32 || node.m > 1 {
|
||||
if node.n == math.MaxInt32 {
|
||||
w.emit2(InstOp(Branchcount+lazy), w.popInt(), math.MaxInt32)
|
||||
if node.N < math.MaxInt32 || node.M > 1 {
|
||||
if node.N == math.MaxInt32 {
|
||||
w.emit2(Branchcount+lazy, w.popInt(), math.MaxInt32)
|
||||
} else {
|
||||
w.emit2(InstOp(Branchcount+lazy), w.popInt(), node.n-node.m)
|
||||
w.emit2(Branchcount+lazy, w.popInt(), node.N-node.M)
|
||||
}
|
||||
} else {
|
||||
w.emit1(InstOp(Branchmark+lazy), w.popInt())
|
||||
w.emit1(Branchmark+lazy, w.popInt())
|
||||
}
|
||||
|
||||
if node.m == 0 {
|
||||
if node.M == 0 {
|
||||
w.patchJump(w.popInt(), startJumpPos)
|
||||
}
|
||||
|
||||
case ntGroup | beforeChild, ntGroup | afterChild:
|
||||
case NtGroup | BeforeChild, NtGroup | AfterChild:
|
||||
|
||||
case ntCapture | beforeChild:
|
||||
case NtCapture | BeforeChild:
|
||||
w.emit(Setmark)
|
||||
|
||||
case ntCapture | afterChild:
|
||||
w.emit2(Capturemark, w.mapCapnum(node.m), w.mapCapnum(node.n))
|
||||
case NtCapture | AfterChild:
|
||||
w.emit2(Capturemark, w.mapCapnum(node.M), w.mapCapnum(node.N))
|
||||
|
||||
case ntRequire | beforeChild:
|
||||
case NtPosLook | BeforeChild:
|
||||
// NOTE: the following line causes lookahead/lookbehind to be
|
||||
// NON-BACKTRACKING. It can be commented out with (*)
|
||||
w.emit(Setjump)
|
||||
|
||||
w.emit(Setmark)
|
||||
|
||||
case ntRequire | afterChild:
|
||||
case NtPosLook | AfterChild:
|
||||
w.emit(Getmark)
|
||||
|
||||
// NOTE: the following line causes lookahead/lookbehind to be
|
||||
// NON-BACKTRACKING. It can be commented out with (*)
|
||||
w.emit(Forejump)
|
||||
|
||||
case ntPrevent | beforeChild:
|
||||
case NtNegLook | BeforeChild:
|
||||
w.emit(Setjump)
|
||||
w.pushInt(w.curPos())
|
||||
w.emit1(Lazybranch, 0)
|
||||
|
||||
case ntPrevent | afterChild:
|
||||
case NtNegLook | AfterChild:
|
||||
w.emit(Backjump)
|
||||
w.patchJump(w.popInt(), w.curPos())
|
||||
w.emit(Forejump)
|
||||
|
||||
case ntGreedy | beforeChild:
|
||||
case NtAtomic | BeforeChild:
|
||||
w.emit(Setjump)
|
||||
|
||||
case ntGreedy | afterChild:
|
||||
case NtAtomic | AfterChild:
|
||||
w.emit(Forejump)
|
||||
|
||||
case ntOne, ntNotone:
|
||||
w.emit1(InstOp(node.t|ntBits), int(node.ch))
|
||||
case NtOne, NtNotone:
|
||||
w.emit1(InstOp(node.T|ntBits), int(node.Ch))
|
||||
|
||||
case ntNotoneloop, ntNotonelazy, ntOneloop, ntOnelazy:
|
||||
if node.m > 0 {
|
||||
if node.t == ntOneloop || node.t == ntOnelazy {
|
||||
w.emit2(Onerep|bits, int(node.ch), node.m)
|
||||
case NtNotoneloop, NtNotoneloopatomic, NtNotonelazy, NtOneloop, NtOneloopatomic, NtOnelazy:
|
||||
if node.M > 0 {
|
||||
if node.T == NtOneloop || node.T == NtOnelazy || node.T == NtOneloopatomic {
|
||||
w.emit2(Onerep|bits, int(node.Ch), node.M)
|
||||
} else {
|
||||
w.emit2(Notonerep|bits, int(node.ch), node.m)
|
||||
w.emit2(Notonerep|bits, int(node.Ch), node.M)
|
||||
}
|
||||
}
|
||||
if node.n > node.m {
|
||||
if node.n == math.MaxInt32 {
|
||||
w.emit2(InstOp(node.t|ntBits), int(node.ch), math.MaxInt32)
|
||||
if node.N > node.M {
|
||||
if node.N == math.MaxInt32 {
|
||||
w.emit2(InstOp(node.T|ntBits), int(node.Ch), math.MaxInt32)
|
||||
} else {
|
||||
w.emit2(InstOp(node.t|ntBits), int(node.ch), node.n-node.m)
|
||||
w.emit2(InstOp(node.T|ntBits), int(node.Ch), node.N-node.M)
|
||||
}
|
||||
}
|
||||
|
||||
case ntSetloop, ntSetlazy:
|
||||
if node.m > 0 {
|
||||
w.emit2(Setrep|bits, w.setCode(node.set), node.m)
|
||||
case NtSetloop, NtSetlazy, NtSetloopatomic:
|
||||
if node.M > 0 {
|
||||
w.emit2(Setrep|bits, w.setCode(node.Set), node.M)
|
||||
}
|
||||
if node.n > node.m {
|
||||
if node.n == math.MaxInt32 {
|
||||
w.emit2(InstOp(node.t|ntBits), w.setCode(node.set), math.MaxInt32)
|
||||
if node.N > node.M {
|
||||
if node.N == math.MaxInt32 {
|
||||
w.emit2(InstOp(node.T|ntBits), w.setCode(node.Set), math.MaxInt32)
|
||||
} else {
|
||||
w.emit2(InstOp(node.t|ntBits), w.setCode(node.set), node.n-node.m)
|
||||
w.emit2(InstOp(node.T|ntBits), w.setCode(node.Set), node.N-node.M)
|
||||
}
|
||||
}
|
||||
|
||||
case ntMulti:
|
||||
w.emit1(InstOp(node.t|ntBits), w.stringCode(node.str))
|
||||
case NtMulti:
|
||||
w.emit1(InstOp(node.T|ntBits), w.stringCode(node.Str))
|
||||
|
||||
case ntSet:
|
||||
w.emit1(InstOp(node.t|ntBits), w.setCode(node.set))
|
||||
case NtSet:
|
||||
w.emit1(InstOp(node.T|ntBits), w.setCode(node.Set))
|
||||
|
||||
case ntRef:
|
||||
w.emit1(InstOp(node.t|ntBits), w.mapCapnum(node.m))
|
||||
case NtRef:
|
||||
w.emit1(InstOp(node.T|ntBits), w.mapCapnum(node.M))
|
||||
|
||||
case ntNothing, ntBol, ntEol, ntBoundary, ntNonboundary, ntECMABoundary, ntNonECMABoundary, ntBeginning, ntStart, ntEndZ, ntEnd:
|
||||
w.emit(InstOp(node.t))
|
||||
case NtNothing, NtBol, NtEol, NtBoundary, NtNonboundary, NtECMABoundary, NtNonECMABoundary, NtBeginning, NtStart, NtEndZ, NtEnd, NtUpdateBumpalong:
|
||||
w.emit(InstOp(node.T))
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unexpected opcode in regular expression generation: %v", nodetype)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user