Merge pull request #2088 from umputun/deps/update-backend

Update backend dependencies to latest
This commit is contained in:
Umputun
2026-06-30 12:41:13 -05:00
committed by GitHub
266 changed files with 26653 additions and 13801 deletions
+8 -8
View File
@@ -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
)
+18 -83
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
@@ -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="\&#x27;(?:\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="&quot;"><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="(\-&gt;|==&gt;|\||::|@|\#|\$|\&amp;|!|!!|\./)"><token type="NameDecorator"/></rule>
<rule pattern="(&lt;:|:&gt;|:&lt;|&gt;:|&lt;\\|&lt;&gt;|&lt;|&gt;|ø|∞|\+|\-|\*|\~|=|\^|%|/|//|==&gt;|&lt;=&gt;|&lt;==&gt;|=&gt;&gt;|&lt;&lt;=&gt;&gt;|&lt;&lt;==&gt;&gt;|\-\-&gt;|&lt;\-&gt;|&lt;\-\-&gt;|=\||\|=|\-:|:\-|_|\.|\.\.|\\)"><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="\|\|\&gt;"><token type="LiteralStringInterpol"/><pop depth="1"/></rule>
<rule pattern="[^|]+"><usingself state="root"/></rule>
</state>
<state name="string-escape">
<rule pattern="(\\\\|\\n|\\t|\\&quot;)"><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="\&lt;\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
<rule pattern="&quot;"><token type="LiteralStringDouble"/><pop depth="1"/></rule>
<rule pattern="[^|&quot;]+"><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="\&lt;\|\|"><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="\&lt;\|\|"><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="\&lt;\|\|"><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="\&lt;\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
<rule pattern="\:\}"><token type="LiteralStringDouble"/><pop depth="1"/></rule>
<rule pattern="[^|&lt;:]+"><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="\&lt;\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
<rule pattern="\}"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
<rule pattern="[^|&lt;}]+"><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="\&lt;\|\|"><token type="LiteralStringInterpol"/><push state="inside-template"/></rule>
<rule pattern="\Z"><token type="LiteralStringSingle"/><pop depth="1"/></rule>
<rule pattern="[^|&lt;]+"><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
View File
@@ -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="/\*.*?\*/">
@@ -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="(~&gt;|&gt;=|&lt;=|!=|=|&lt;|&gt;)">
<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
View File
@@ -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.\[\]$&lt;&gt;]*\s+)+?)((?:[^\W\d]|\$)[\w$]*)(\s*)(\()">
<rule pattern="((?:(?:[^\W\d]|\$)[\w.\[\]$&lt;&gt;?]*\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" />
@@ -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>
File diff suppressed because one or more lines are too long
+6 -1
View File
@@ -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,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>
@@ -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="[,;{}\[\]()&lt;&gt;]">
@@ -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>
@@ -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>
@@ -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="[^&lt;&amp;@{}`\s]+">
<token type="Text"/>
</rule>
<rule pattern="&amp;\S*?;">
<token type="NameEntity"/>
</rule>
<rule pattern="\&lt;\!\[CDATA\[.*?\]\]\&gt;">
<token type="CommentPreproc"/>
</rule>
<rule pattern="&lt;!--">
<token type="Comment"/>
<push state="comment"/>
</rule>
<rule pattern="&lt;\?.*?\?&gt;">
<token type="CommentPreproc"/>
</rule>
<rule pattern="&lt;![^&gt;]*&gt;">
<token type="CommentPreproc"/>
</rule>
<rule pattern="(&lt;)(script)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameTag"/>
<token type="Text"/>
</bygroups>
<push state="script-content" state="tag"/>
</rule>
<rule pattern="(&lt;)(style)(\s*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameTag"/>
<token type="Text"/>
</bygroups>
<push state="style-content" state="tag"/>
</rule>
<rule pattern="(&lt;)([A-Za-z][\w:.-]*)">
<bygroups>
<token type="Punctuation"/>
<token type="NameTag"/>
</bygroups>
<push state="tag"/>
</rule>
<rule pattern="(&lt;/)([A-Za-z][\w:.-]*)(\s*)(&gt;)">
<bygroups>
<token type="Punctuation"/>
<token type="NameTag"/>
<token type="Text"/>
<token type="Punctuation"/>
</bygroups>
</rule>
<rule pattern="[@{}&lt;&amp;]">
<token type="Punctuation"/>
</rule>
</state>
<state name="script-content">
<rule pattern="(&lt;)(\s*)(/)(\s*)(script)(\s*)(&gt;)">
<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=".+?(?=&lt;\s*/\s*script\s*&gt;)">
<token type="Other"/>
</rule>
</state>
<state name="style-content">
<rule pattern="(&lt;)(\s*)(/)(\s*)(style)(\s*)(&gt;)">
<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=".+?(?=&lt;\s*/\s*style\s*&gt;)">
<using lexer="CSS"/>
</rule>
</state>
<state name="comment">
<rule pattern="[^-]+">
<token type="Comment"/>
</rule>
<rule pattern="--&gt;">
<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*)(&gt;)">
<bygroups>
<token type="Punctuation"/>
<token type="Text"/>
<token type="Punctuation"/>
</bygroups>
<pop depth="1"/>
</rule>
</state>
<state name="attr">
<rule pattern="&#34;.*?&#34;">
<token type="LiteralString"/>
<pop depth="1"/>
</rule>
<rule pattern="&#39;.*?&#39;">
<token type="LiteralString"/>
<pop depth="1"/>
</rule>
<rule pattern="[^\s&gt;]+">
<token type="LiteralString"/>
<pop depth="1"/>
</rule>
</state>
</rules>
</lexer>
+14 -4
View File
@@ -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="([^\{\}\[\]\?,\:\!\-\*&amp;\@].*)( )+(//.*)">
<bygroups>
<token type="Literal"/>
<token type="TextWhitespace"/>
<token type="Comment"/>
</bygroups>
</rule>
<rule pattern="[^\{\}\[\]\?,\:\!\-\*&amp;\@].*">
<token type="Literal"/>
</rule>
</state>
<state name="key">
<rule pattern="&#34;[^&#34;\n].*&#34;: ">
<rule pattern="&#34;[^&#34;\n#].*&#34;: ">
<token type="NameTag"/>
</rule>
<rule pattern="(-)( )([^&#34;\n{]*)(:)( )">
<rule pattern="(-)( )((?:(?!//)[^&#34;\n{#])*?)(:)( )">
<bygroups>
<token type="Punctuation"/>
<token type="TextWhitespace"/>
@@ -95,14 +105,14 @@
<token type="TextWhitespace"/>
</bygroups>
</rule>
<rule pattern="([^&#34;\n{]*)(:)( )">
<rule pattern="((?:(?!//)[^&#34;\n{#])*?)(:)( )">
<bygroups>
<token type="NameTag"/>
<token type="Punctuation"/>
<token type="TextWhitespace"/>
</bygroups>
</rule>
<rule pattern="([^&#34;\n{]*)(:)(\n)">
<rule pattern="((?:(?!//)[^&#34;\n{#])*?)(:)(\n)">
<bygroups>
<token type="NameTag"/>
<token type="Punctuation"/>
+2 -2
View File
@@ -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
View File
@@ -122,7 +122,7 @@ func (d *httpBodyContentTyper) Tokenise(options *TokeniseOptions, text string) (
if err != nil {
panic(err)
}
return EOF
return subIterator()
}
}
}
+64 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -9,6 +9,7 @@
"schedule:earlyMondays", // Run once a week.
'helpers:pinGitHubActionDigests',
],
"postUpdateOptions": ["gomodTidy"],
"packageRules": [
{
"matchPackageNames": ["golangci-lint"],
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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,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,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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,2 @@
{"name": "Alice"}
{"name": "Bob"}
+3 -3
View File
@@ -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
View File
@@ -1,7 +0,0 @@
language: go
arch:
- AMD64
- ppc64le
go:
- 1.9
- tip
-174
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
-865
View File
@@ -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
View File
@@ -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
View File
@@ -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 &regexNode{
t: t,
options: opt,
}
}
func newRegexNodeCh(t nodeType, opt RegexOptions, ch rune) *regexNode {
return &regexNode{
t: t,
options: opt,
ch: ch,
}
}
func newRegexNodeStr(t nodeType, opt RegexOptions, str []rune) *regexNode {
return &regexNode{
t: t,
options: opt,
str: str,
}
}
func newRegexNodeSet(t nodeType, opt RegexOptions, set *CharSet) *regexNode {
return &regexNode{
t: t,
options: opt,
set: set,
}
}
func newRegexNodeM(t nodeType, opt RegexOptions, m int) *regexNode {
return &regexNode{
t: t,
options: opt,
m: m,
}
}
func newRegexNodeMN(t nodeType, opt RegexOptions, m, n int) *regexNode {
return &regexNode{
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()
}
File diff suppressed because it is too large Load Diff
@@ -25,3 +25,5 @@ _testmain.go
*.out
.DS_Store
*.txt
benchmarks/
+283
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+394
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+67
View File
@@ -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
View File
@@ -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
}
File diff suppressed because it is too large Load Diff
+316
View File
@@ -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()
}
@@ -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)
@@ -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
View File
@@ -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
}
@@ -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
}
}
File diff suppressed because it is too large Load Diff
@@ -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]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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