[chore]: Bump github.com/tdewolff/minify/v2 from 2.20.9 to 2.20.12 (#2509)
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.9 to 2.20.12. - [Release notes](https://github.com/tdewolff/minify/releases) - [Commits](https://github.com/tdewolff/minify/compare/v2.20.9...v2.20.12) --- updated-dependencies: - dependency-name: github.com/tdewolff/minify/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
parent
72d0f46b0b
commit
f0c3533862
4
go.mod
4
go.mod
|
@ -47,7 +47,7 @@ require (
|
||||||
github.com/superseriousbusiness/activity v1.4.0-gts
|
github.com/superseriousbusiness/activity v1.4.0-gts
|
||||||
github.com/superseriousbusiness/exif-terminator v0.6.0
|
github.com/superseriousbusiness/exif-terminator v0.6.0
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
||||||
github.com/tdewolff/minify/v2 v2.20.9
|
github.com/tdewolff/minify/v2 v2.20.12
|
||||||
github.com/technologize/otel-go-contrib v1.1.0
|
github.com/technologize/otel-go-contrib v1.1.0
|
||||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||||
github.com/ulule/limiter/v3 v3.11.2
|
github.com/ulule/limiter/v3 v3.11.2
|
||||||
|
@ -160,7 +160,7 @@ require (
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
|
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
|
||||||
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB // indirect
|
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.7.6 // indirect
|
github.com/tdewolff/parse/v2 v2.7.7 // indirect
|
||||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
|
11
go.sum
11
go.sum
|
@ -511,12 +511,13 @@ github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB h1:8psprYSK
|
||||||
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB/go.mod h1:ymKGfy9kg4dIdraeZRAdobMS/flzLk3VcRPLpEWOAXg=
|
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB/go.mod h1:ymKGfy9kg4dIdraeZRAdobMS/flzLk3VcRPLpEWOAXg=
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
|
||||||
github.com/tdewolff/minify/v2 v2.20.9 h1:0RGsL+jBpm77obkuNCjNZ2eiN81CZzTnjeVmTqxCmYk=
|
github.com/tdewolff/minify/v2 v2.20.12 h1:ie5+91QGUUeEDbLkexhx2tlI9BQgwwnfY+/Qdj4BlQ4=
|
||||||
github.com/tdewolff/minify/v2 v2.20.9/go.mod h1:hZnNtFqXVQ5QIAR05tdgvS7h6E80jyRwHSGVmM4jbzQ=
|
github.com/tdewolff/minify/v2 v2.20.12/go.mod h1:8ktdncc9Rh41MkTX2KYaicHT9+VnpvIDjCyIVsr/nN8=
|
||||||
github.com/tdewolff/parse/v2 v2.7.6 h1:PGZH2b/itDSye9RatReRn4GBhsT+KFEMtAMjHRuY1h8=
|
github.com/tdewolff/parse/v2 v2.7.7 h1:V+50eFDH7Piw4IBwH8D8FtYeYbZp3T4SCtIvmBSIMyc=
|
||||||
github.com/tdewolff/parse/v2 v2.7.6/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
github.com/tdewolff/parse/v2 v2.7.7/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
|
||||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52 h1:gAQliwn+zJrkjAHVcBEYW/RFvd2St4yYimisvozAYlA=
|
|
||||||
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
|
||||||
|
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/technologize/otel-go-contrib v1.1.0 h1:gl9bxxJAgXFnKJzoprJOfbvNRE1k3Ky9O7ppVJDb9gg=
|
github.com/technologize/otel-go-contrib v1.1.0 h1:gl9bxxJAgXFnKJzoprJOfbvNRE1k3Ky9O7ppVJDb9gg=
|
||||||
github.com/technologize/otel-go-contrib v1.1.0/go.mod h1:dCN/wj2WyUO8aFZFdIN+6tfJHImjTML/8r2YVYAy3So=
|
github.com/technologize/otel-go-contrib v1.1.0/go.mod h1:dCN/wj2WyUO8aFZFdIN+6tfJHImjTML/8r2YVYAy3So=
|
||||||
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E=
|
||||||
|
|
|
@ -126,6 +126,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
}
|
}
|
||||||
w.Write(t.Data)
|
w.Write(t.Data)
|
||||||
}
|
}
|
||||||
|
omitSpace = false
|
||||||
case html.MathToken:
|
case html.MathToken:
|
||||||
if err := m.MinifyMimetype(mathMimeBytes, w, buffer.NewReader(t.Data), nil); err != nil {
|
if err := m.MinifyMimetype(mathMimeBytes, w, buffer.NewReader(t.Data), nil); err != nil {
|
||||||
if err != minify.ErrNotExist {
|
if err != minify.ErrNotExist {
|
||||||
|
@ -133,6 +134,7 @@ func (o *Minifier) Minify(m *minify.M, w io.Writer, r io.Reader, _ map[string]st
|
||||||
}
|
}
|
||||||
w.Write(t.Data)
|
w.Write(t.Data)
|
||||||
}
|
}
|
||||||
|
omitSpace = false
|
||||||
case html.TextToken:
|
case html.TextToken:
|
||||||
if t.HasTemplate {
|
if t.HasTemplate {
|
||||||
w.Write(t.Data)
|
w.Write(t.Data)
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
# CSS [![API reference](https://img.shields.io/badge/godoc-reference-5272B4)](https://pkg.go.dev/github.com/tdewolff/parse/v2/css?tab=doc)
|
||||||
|
|
||||||
|
This package is a CSS3 lexer and parser written in [Go][1]. Both follow the specification at [CSS Syntax Module Level 3](http://www.w3.org/TR/css-syntax-3/). The lexer takes an io.Reader and converts it into tokens until the EOF. The parser returns a parse tree of the full io.Reader input stream, but the low-level `Next` function can be used for stream parsing to returns grammar units until the EOF.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Run the following command
|
||||||
|
|
||||||
|
go get -u github.com/tdewolff/parse/v2/css
|
||||||
|
|
||||||
|
or add the following import and run project with `go get`
|
||||||
|
|
||||||
|
import "github.com/tdewolff/parse/v2/css"
|
||||||
|
|
||||||
|
## Lexer
|
||||||
|
### Usage
|
||||||
|
The following initializes a new Lexer with io.Reader `r`:
|
||||||
|
``` go
|
||||||
|
l := css.NewLexer(parse.NewInput(r))
|
||||||
|
```
|
||||||
|
|
||||||
|
To tokenize until EOF an error, use:
|
||||||
|
``` go
|
||||||
|
for {
|
||||||
|
tt, text := l.Next()
|
||||||
|
switch tt {
|
||||||
|
case css.ErrorToken:
|
||||||
|
// error or EOF set in l.Err()
|
||||||
|
return
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All tokens (see [CSS Syntax Module Level 3](http://www.w3.org/TR/css3-syntax/)):
|
||||||
|
``` go
|
||||||
|
ErrorToken // non-official token, returned when errors occur
|
||||||
|
IdentToken
|
||||||
|
FunctionToken // rgb( rgba( ...
|
||||||
|
AtKeywordToken // @abc
|
||||||
|
HashToken // #abc
|
||||||
|
StringToken
|
||||||
|
BadStringToken
|
||||||
|
URLToken // url(
|
||||||
|
BadURLToken
|
||||||
|
DelimToken // any unmatched character
|
||||||
|
NumberToken // 5
|
||||||
|
PercentageToken // 5%
|
||||||
|
DimensionToken // 5em
|
||||||
|
UnicodeRangeToken
|
||||||
|
IncludeMatchToken // ~=
|
||||||
|
DashMatchToken // |=
|
||||||
|
PrefixMatchToken // ^=
|
||||||
|
SuffixMatchToken // $=
|
||||||
|
SubstringMatchToken // *=
|
||||||
|
ColumnToken // ||
|
||||||
|
WhitespaceToken
|
||||||
|
CDOToken // <!--
|
||||||
|
CDCToken // -->
|
||||||
|
ColonToken
|
||||||
|
SemicolonToken
|
||||||
|
CommaToken
|
||||||
|
BracketToken // ( ) [ ] { }, all bracket tokens use this, Data() can distinguish between the brackets
|
||||||
|
CommentToken // non-official token
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2/css"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tokenize CSS3 from stdin.
|
||||||
|
func main() {
|
||||||
|
l := css.NewLexer(parse.NewInput(os.Stdin))
|
||||||
|
for {
|
||||||
|
tt, text := l.Next()
|
||||||
|
switch tt {
|
||||||
|
case css.ErrorToken:
|
||||||
|
if l.Err() != io.EOF {
|
||||||
|
fmt.Println("Error on line", l.Line(), ":", l.Err())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case css.IdentToken:
|
||||||
|
fmt.Println("Identifier", string(text))
|
||||||
|
case css.NumberToken:
|
||||||
|
fmt.Println("Number", string(text))
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parser
|
||||||
|
### Usage
|
||||||
|
The following creates a new Parser.
|
||||||
|
``` go
|
||||||
|
// true because this is the content of an inline style attribute
|
||||||
|
p := css.NewParser(parse.NewInput(bytes.NewBufferString("color: red;")), true)
|
||||||
|
```
|
||||||
|
|
||||||
|
To iterate over the stylesheet, use:
|
||||||
|
``` go
|
||||||
|
for {
|
||||||
|
gt, _, data := p.Next()
|
||||||
|
if gt == css.ErrorGrammar {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All grammar units returned by `Next`:
|
||||||
|
``` go
|
||||||
|
ErrorGrammar
|
||||||
|
AtRuleGrammar
|
||||||
|
EndAtRuleGrammar
|
||||||
|
RulesetGrammar
|
||||||
|
EndRulesetGrammar
|
||||||
|
DeclarationGrammar
|
||||||
|
TokenGrammar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
``` go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2/css"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// true because this is the content of an inline style attribute
|
||||||
|
p := css.NewParser(parse.NewInput(bytes.NewBufferString("color: red;")), true)
|
||||||
|
out := ""
|
||||||
|
for {
|
||||||
|
gt, _, data := p.Next()
|
||||||
|
if gt == css.ErrorGrammar {
|
||||||
|
break
|
||||||
|
} else if gt == css.AtRuleGrammar || gt == css.BeginAtRuleGrammar || gt == css.BeginRulesetGrammar || gt == css.DeclarationGrammar {
|
||||||
|
out += string(data)
|
||||||
|
if gt == css.DeclarationGrammar {
|
||||||
|
out += ":"
|
||||||
|
}
|
||||||
|
for _, val := range p.Values() {
|
||||||
|
out += string(val.Data)
|
||||||
|
}
|
||||||
|
if gt == css.BeginAtRuleGrammar || gt == css.BeginRulesetGrammar {
|
||||||
|
out += "{"
|
||||||
|
} else if gt == css.AtRuleGrammar || gt == css.DeclarationGrammar {
|
||||||
|
out += ";"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out += string(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(out)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
Released under the [MIT license](https://github.com/tdewolff/parse/blob/master/LICENSE.md).
|
||||||
|
|
||||||
|
[1]: http://golang.org/ "Go Language"
|
|
@ -0,0 +1,75 @@
|
||||||
|
package css
|
||||||
|
|
||||||
|
// generated by hasher -type=Hash -file=hash.go; DO NOT EDIT, except for adding more constants to the list and rerun go generate
|
||||||
|
|
||||||
|
// uses github.com/tdewolff/hasher
|
||||||
|
//go:generate hasher -type=Hash -file=hash.go
|
||||||
|
|
||||||
|
// Hash defines perfect hashes for a predefined list of strings
|
||||||
|
type Hash uint32
|
||||||
|
|
||||||
|
// Unique hash definitions to be used instead of strings
|
||||||
|
const (
|
||||||
|
Document Hash = 0x8 // document
|
||||||
|
Font_Face Hash = 0x809 // font-face
|
||||||
|
Keyframes Hash = 0x1109 // keyframes
|
||||||
|
Media Hash = 0x2105 // media
|
||||||
|
Page Hash = 0x2604 // page
|
||||||
|
Supports Hash = 0x1908 // supports
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the hash' name.
|
||||||
|
func (i Hash) String() string {
|
||||||
|
start := uint32(i >> 8)
|
||||||
|
n := uint32(i & 0xff)
|
||||||
|
if start+n > uint32(len(_Hash_text)) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return _Hash_text[start : start+n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToHash returns the hash whose name is s. It returns zero if there is no
|
||||||
|
// such hash. It is case sensitive.
|
||||||
|
func ToHash(s []byte) Hash {
|
||||||
|
if len(s) == 0 || len(s) > _Hash_maxLen {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
h := uint32(_Hash_hash0)
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
h ^= uint32(s[i])
|
||||||
|
h *= 16777619
|
||||||
|
}
|
||||||
|
if i := _Hash_table[h&uint32(len(_Hash_table)-1)]; int(i&0xff) == len(s) {
|
||||||
|
t := _Hash_text[i>>8 : i>>8+i&0xff]
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if t[i] != s[i] {
|
||||||
|
goto NEXT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
NEXT:
|
||||||
|
if i := _Hash_table[(h>>16)&uint32(len(_Hash_table)-1)]; int(i&0xff) == len(s) {
|
||||||
|
t := _Hash_text[i>>8 : i>>8+i&0xff]
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if t[i] != s[i] {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Hash_hash0 = 0x9acb0442
|
||||||
|
const _Hash_maxLen = 9
|
||||||
|
const _Hash_text = "documentfont-facekeyframesupportsmediapage"
|
||||||
|
|
||||||
|
var _Hash_table = [1 << 3]Hash{
|
||||||
|
0x1: 0x2604, // page
|
||||||
|
0x2: 0x2105, // media
|
||||||
|
0x3: 0x809, // font-face
|
||||||
|
0x5: 0x1109, // keyframes
|
||||||
|
0x6: 0x1908, // supports
|
||||||
|
0x7: 0x8, // document
|
||||||
|
}
|
|
@ -0,0 +1,698 @@
|
||||||
|
// Package css is a CSS3 lexer and parser following the specifications at http://www.w3.org/TR/css-syntax-3/.
|
||||||
|
package css
|
||||||
|
|
||||||
|
// TODO: \uFFFD replacement character for NULL bytes in strings for example, or atleast don't end the string early
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenType determines the type of token, eg. a number or a semicolon.
|
||||||
|
type TokenType uint32
|
||||||
|
|
||||||
|
// TokenType values.
|
||||||
|
const (
|
||||||
|
ErrorToken TokenType = iota // extra token when errors occur
|
||||||
|
IdentToken
|
||||||
|
FunctionToken // rgb( rgba( ...
|
||||||
|
AtKeywordToken // @abc
|
||||||
|
HashToken // #abc
|
||||||
|
StringToken
|
||||||
|
BadStringToken
|
||||||
|
URLToken
|
||||||
|
BadURLToken
|
||||||
|
DelimToken // any unmatched character
|
||||||
|
NumberToken // 5
|
||||||
|
PercentageToken // 5%
|
||||||
|
DimensionToken // 5em
|
||||||
|
UnicodeRangeToken // U+554A
|
||||||
|
IncludeMatchToken // ~=
|
||||||
|
DashMatchToken // |=
|
||||||
|
PrefixMatchToken // ^=
|
||||||
|
SuffixMatchToken // $=
|
||||||
|
SubstringMatchToken // *=
|
||||||
|
ColumnToken // ||
|
||||||
|
WhitespaceToken // space \t \r \n \f
|
||||||
|
CDOToken // <!--
|
||||||
|
CDCToken // -->
|
||||||
|
ColonToken // :
|
||||||
|
SemicolonToken // ;
|
||||||
|
CommaToken // ,
|
||||||
|
LeftBracketToken // [
|
||||||
|
RightBracketToken // ]
|
||||||
|
LeftParenthesisToken // (
|
||||||
|
RightParenthesisToken // )
|
||||||
|
LeftBraceToken // {
|
||||||
|
RightBraceToken // }
|
||||||
|
CommentToken // extra token for comments
|
||||||
|
EmptyToken
|
||||||
|
CustomPropertyNameToken
|
||||||
|
CustomPropertyValueToken
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of a TokenType.
|
||||||
|
func (tt TokenType) String() string {
|
||||||
|
switch tt {
|
||||||
|
case ErrorToken:
|
||||||
|
return "Error"
|
||||||
|
case IdentToken:
|
||||||
|
return "Ident"
|
||||||
|
case FunctionToken:
|
||||||
|
return "Function"
|
||||||
|
case AtKeywordToken:
|
||||||
|
return "AtKeyword"
|
||||||
|
case HashToken:
|
||||||
|
return "Hash"
|
||||||
|
case StringToken:
|
||||||
|
return "String"
|
||||||
|
case BadStringToken:
|
||||||
|
return "BadString"
|
||||||
|
case URLToken:
|
||||||
|
return "URL"
|
||||||
|
case BadURLToken:
|
||||||
|
return "BadURL"
|
||||||
|
case DelimToken:
|
||||||
|
return "Delim"
|
||||||
|
case NumberToken:
|
||||||
|
return "Number"
|
||||||
|
case PercentageToken:
|
||||||
|
return "Percentage"
|
||||||
|
case DimensionToken:
|
||||||
|
return "Dimension"
|
||||||
|
case UnicodeRangeToken:
|
||||||
|
return "UnicodeRange"
|
||||||
|
case IncludeMatchToken:
|
||||||
|
return "IncludeMatch"
|
||||||
|
case DashMatchToken:
|
||||||
|
return "DashMatch"
|
||||||
|
case PrefixMatchToken:
|
||||||
|
return "PrefixMatch"
|
||||||
|
case SuffixMatchToken:
|
||||||
|
return "SuffixMatch"
|
||||||
|
case SubstringMatchToken:
|
||||||
|
return "SubstringMatch"
|
||||||
|
case ColumnToken:
|
||||||
|
return "Column"
|
||||||
|
case WhitespaceToken:
|
||||||
|
return "Whitespace"
|
||||||
|
case CDOToken:
|
||||||
|
return "CDO"
|
||||||
|
case CDCToken:
|
||||||
|
return "CDC"
|
||||||
|
case ColonToken:
|
||||||
|
return "Colon"
|
||||||
|
case SemicolonToken:
|
||||||
|
return "Semicolon"
|
||||||
|
case CommaToken:
|
||||||
|
return "Comma"
|
||||||
|
case LeftBracketToken:
|
||||||
|
return "LeftBracket"
|
||||||
|
case RightBracketToken:
|
||||||
|
return "RightBracket"
|
||||||
|
case LeftParenthesisToken:
|
||||||
|
return "LeftParenthesis"
|
||||||
|
case RightParenthesisToken:
|
||||||
|
return "RightParenthesis"
|
||||||
|
case LeftBraceToken:
|
||||||
|
return "LeftBrace"
|
||||||
|
case RightBraceToken:
|
||||||
|
return "RightBrace"
|
||||||
|
case CommentToken:
|
||||||
|
return "Comment"
|
||||||
|
case EmptyToken:
|
||||||
|
return "Empty"
|
||||||
|
case CustomPropertyNameToken:
|
||||||
|
return "CustomPropertyName"
|
||||||
|
case CustomPropertyValueToken:
|
||||||
|
return "CustomPropertyValue"
|
||||||
|
}
|
||||||
|
return "Invalid(" + strconv.Itoa(int(tt)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Lexer is the state for the lexer.
|
||||||
|
type Lexer struct {
|
||||||
|
r *parse.Input
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLexer returns a new Lexer for a given io.Reader.
|
||||||
|
func NewLexer(r *parse.Input) *Lexer {
|
||||||
|
return &Lexer{
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error encountered during lexing, this is often io.EOF but also other errors can be returned.
|
||||||
|
func (l *Lexer) Err() error {
|
||||||
|
return l.r.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next Token. It returns ErrorToken when an error was encountered. Using Err() one can retrieve the error message.
|
||||||
|
func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
|
switch l.r.Peek(0) {
|
||||||
|
case ' ', '\t', '\n', '\r', '\f':
|
||||||
|
l.r.Move(1)
|
||||||
|
for l.consumeWhitespace() {
|
||||||
|
}
|
||||||
|
return WhitespaceToken, l.r.Shift()
|
||||||
|
case ':':
|
||||||
|
l.r.Move(1)
|
||||||
|
return ColonToken, l.r.Shift()
|
||||||
|
case ';':
|
||||||
|
l.r.Move(1)
|
||||||
|
return SemicolonToken, l.r.Shift()
|
||||||
|
case ',':
|
||||||
|
l.r.Move(1)
|
||||||
|
return CommaToken, l.r.Shift()
|
||||||
|
case '(', ')', '[', ']', '{', '}':
|
||||||
|
if t := l.consumeBracket(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '#':
|
||||||
|
if l.consumeHashToken() {
|
||||||
|
return HashToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '"', '\'':
|
||||||
|
if t := l.consumeString(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '.', '+':
|
||||||
|
if t := l.consumeNumeric(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
if t := l.consumeNumeric(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
} else if t := l.consumeIdentlike(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
} else if l.consumeCDCToken() {
|
||||||
|
return CDCToken, l.r.Shift()
|
||||||
|
} else if l.consumeCustomVariableToken() {
|
||||||
|
return CustomPropertyNameToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '@':
|
||||||
|
if l.consumeAtKeywordToken() {
|
||||||
|
return AtKeywordToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '$', '*', '^', '~':
|
||||||
|
if t := l.consumeMatch(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '/':
|
||||||
|
if l.consumeComment() {
|
||||||
|
return CommentToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '<':
|
||||||
|
if l.consumeCDOToken() {
|
||||||
|
return CDOToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '\\':
|
||||||
|
if t := l.consumeIdentlike(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case 'u', 'U':
|
||||||
|
if l.consumeUnicodeRangeToken() {
|
||||||
|
return UnicodeRangeToken, l.r.Shift()
|
||||||
|
} else if t := l.consumeIdentlike(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
case '|':
|
||||||
|
if t := l.consumeMatch(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
} else if l.consumeColumnToken() {
|
||||||
|
return ColumnToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
case 0:
|
||||||
|
if l.r.Err() != nil {
|
||||||
|
return ErrorToken, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if t := l.consumeNumeric(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
} else if t := l.consumeIdentlike(); t != ErrorToken {
|
||||||
|
return t, l.r.Shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// can't be rune because consumeIdentlike consumes that as an identifier
|
||||||
|
l.r.Move(1)
|
||||||
|
return DelimToken, l.r.Shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/*
|
||||||
|
The following functions follow the railroad diagrams in http://www.w3.org/TR/css3-syntax/
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (l *Lexer) consumeByte(c byte) bool {
|
||||||
|
if l.r.Peek(0) == c {
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeComment() bool {
|
||||||
|
if l.r.Peek(0) != '/' || l.r.Peek(1) != '*' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
l.r.Move(2)
|
||||||
|
for {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == 0 && l.r.Err() != nil {
|
||||||
|
break
|
||||||
|
} else if c == '*' && l.r.Peek(1) == '/' {
|
||||||
|
l.r.Move(2)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeNewline() bool {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == '\n' || c == '\f' {
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
} else if c == '\r' {
|
||||||
|
if l.r.Peek(1) == '\n' {
|
||||||
|
l.r.Move(2)
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeWhitespace() bool {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' {
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeDigit() bool {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c >= '0' && c <= '9' {
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeHexDigit() bool {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') {
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeEscape() bool {
|
||||||
|
if l.r.Peek(0) != '\\' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mark := l.r.Pos()
|
||||||
|
l.r.Move(1)
|
||||||
|
if l.consumeNewline() {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
} else if l.consumeHexDigit() {
|
||||||
|
for k := 1; k < 6; k++ {
|
||||||
|
if !l.consumeHexDigit() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.consumeWhitespace()
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c >= 0xC0 {
|
||||||
|
_, n := l.r.PeekRune(0)
|
||||||
|
l.r.Move(n)
|
||||||
|
return true
|
||||||
|
} else if c == 0 && l.r.Err() != nil {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.r.Move(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeIdentToken() bool {
|
||||||
|
mark := l.r.Pos()
|
||||||
|
if l.r.Peek(0) == '-' {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c >= 0x80) {
|
||||||
|
if c != '\\' || !l.consumeEscape() {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c >= 0x80) {
|
||||||
|
if c != '\\' || !l.consumeEscape() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// support custom variables, https://www.w3.org/TR/css-variables-1/
|
||||||
|
func (l *Lexer) consumeCustomVariableToken() bool {
|
||||||
|
// expect to be on a '-'
|
||||||
|
l.r.Move(1)
|
||||||
|
if l.r.Peek(0) != '-' {
|
||||||
|
l.r.Move(-1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !l.consumeIdentToken() {
|
||||||
|
l.r.Move(-1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeAtKeywordToken() bool {
|
||||||
|
// expect to be on an '@'
|
||||||
|
l.r.Move(1)
|
||||||
|
if !l.consumeIdentToken() {
|
||||||
|
l.r.Move(-1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeHashToken() bool {
|
||||||
|
// expect to be on a '#'
|
||||||
|
mark := l.r.Pos()
|
||||||
|
l.r.Move(1)
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c >= 0x80) {
|
||||||
|
if c != '\\' || !l.consumeEscape() {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c >= 0x80) {
|
||||||
|
if c != '\\' || !l.consumeEscape() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeNumberToken() bool {
|
||||||
|
mark := l.r.Pos()
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == '+' || c == '-' {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
firstDigit := l.consumeDigit()
|
||||||
|
if firstDigit {
|
||||||
|
for l.consumeDigit() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if l.r.Peek(0) == '.' {
|
||||||
|
l.r.Move(1)
|
||||||
|
if l.consumeDigit() {
|
||||||
|
for l.consumeDigit() {
|
||||||
|
}
|
||||||
|
} else if firstDigit {
|
||||||
|
// . could belong to the next token
|
||||||
|
l.r.Move(-1)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if !firstDigit {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mark = l.r.Pos()
|
||||||
|
c = l.r.Peek(0)
|
||||||
|
if c == 'e' || c == 'E' {
|
||||||
|
l.r.Move(1)
|
||||||
|
c = l.r.Peek(0)
|
||||||
|
if c == '+' || c == '-' {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
if !l.consumeDigit() {
|
||||||
|
// e could belong to next token
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for l.consumeDigit() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeUnicodeRangeToken() bool {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if (c != 'u' && c != 'U') || l.r.Peek(1) != '+' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mark := l.r.Pos()
|
||||||
|
l.r.Move(2)
|
||||||
|
|
||||||
|
// consume up to 6 hexDigits
|
||||||
|
k := 0
|
||||||
|
for l.consumeHexDigit() {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
|
||||||
|
// either a minus or a question mark or the end is expected
|
||||||
|
if l.consumeByte('-') {
|
||||||
|
if k == 0 || 6 < k {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume another up to 6 hexDigits
|
||||||
|
if l.consumeHexDigit() {
|
||||||
|
k = 1
|
||||||
|
for l.consumeHexDigit() {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if l.consumeByte('?') {
|
||||||
|
// could be filled up to 6 characters with question marks or else regular hexDigits
|
||||||
|
k++
|
||||||
|
for l.consumeByte('?') {
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if k == 0 || 6 < k {
|
||||||
|
l.r.Rewind(mark)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeColumnToken() bool {
|
||||||
|
if l.r.Peek(0) == '|' && l.r.Peek(1) == '|' {
|
||||||
|
l.r.Move(2)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeCDOToken() bool {
|
||||||
|
if l.r.Peek(0) == '<' && l.r.Peek(1) == '!' && l.r.Peek(2) == '-' && l.r.Peek(3) == '-' {
|
||||||
|
l.r.Move(4)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeCDCToken() bool {
|
||||||
|
if l.r.Peek(0) == '-' && l.r.Peek(1) == '-' && l.r.Peek(2) == '>' {
|
||||||
|
l.r.Move(3)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// consumeMatch consumes any MatchToken.
|
||||||
|
func (l *Lexer) consumeMatch() TokenType {
|
||||||
|
if l.r.Peek(1) == '=' {
|
||||||
|
switch l.r.Peek(0) {
|
||||||
|
case '~':
|
||||||
|
l.r.Move(2)
|
||||||
|
return IncludeMatchToken
|
||||||
|
case '|':
|
||||||
|
l.r.Move(2)
|
||||||
|
return DashMatchToken
|
||||||
|
case '^':
|
||||||
|
l.r.Move(2)
|
||||||
|
return PrefixMatchToken
|
||||||
|
case '$':
|
||||||
|
l.r.Move(2)
|
||||||
|
return SuffixMatchToken
|
||||||
|
case '*':
|
||||||
|
l.r.Move(2)
|
||||||
|
return SubstringMatchToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ErrorToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeBracket consumes any bracket token.
|
||||||
|
func (l *Lexer) consumeBracket() TokenType {
|
||||||
|
switch l.r.Peek(0) {
|
||||||
|
case '(':
|
||||||
|
l.r.Move(1)
|
||||||
|
return LeftParenthesisToken
|
||||||
|
case ')':
|
||||||
|
l.r.Move(1)
|
||||||
|
return RightParenthesisToken
|
||||||
|
case '[':
|
||||||
|
l.r.Move(1)
|
||||||
|
return LeftBracketToken
|
||||||
|
case ']':
|
||||||
|
l.r.Move(1)
|
||||||
|
return RightBracketToken
|
||||||
|
case '{':
|
||||||
|
l.r.Move(1)
|
||||||
|
return LeftBraceToken
|
||||||
|
case '}':
|
||||||
|
l.r.Move(1)
|
||||||
|
return RightBraceToken
|
||||||
|
}
|
||||||
|
return ErrorToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeNumeric consumes NumberToken, PercentageToken or DimensionToken.
|
||||||
|
func (l *Lexer) consumeNumeric() TokenType {
|
||||||
|
if l.consumeNumberToken() {
|
||||||
|
if l.consumeByte('%') {
|
||||||
|
return PercentageToken
|
||||||
|
} else if l.consumeIdentToken() {
|
||||||
|
return DimensionToken
|
||||||
|
}
|
||||||
|
return NumberToken
|
||||||
|
}
|
||||||
|
return ErrorToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeString consumes a string and may return BadStringToken when a newline is encountered.
|
||||||
|
func (l *Lexer) consumeString() TokenType {
|
||||||
|
// assume to be on " or '
|
||||||
|
delim := l.r.Peek(0)
|
||||||
|
l.r.Move(1)
|
||||||
|
for {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == 0 && l.r.Err() != nil {
|
||||||
|
break
|
||||||
|
} else if c == '\n' || c == '\r' || c == '\f' {
|
||||||
|
l.r.Move(1)
|
||||||
|
return BadStringToken
|
||||||
|
} else if c == delim {
|
||||||
|
l.r.Move(1)
|
||||||
|
break
|
||||||
|
} else if c == '\\' {
|
||||||
|
if !l.consumeEscape() {
|
||||||
|
// either newline or EOF after backslash
|
||||||
|
l.r.Move(1)
|
||||||
|
l.consumeNewline()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StringToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lexer) consumeUnquotedURL() bool {
|
||||||
|
for {
|
||||||
|
c := l.r.Peek(0)
|
||||||
|
if c == 0 && l.r.Err() != nil || c == ')' {
|
||||||
|
break
|
||||||
|
} else if c == '"' || c == '\'' || c == '(' || c == '\\' || c == ' ' || c <= 0x1F || c == 0x7F {
|
||||||
|
if c != '\\' || !l.consumeEscape() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeRemnantsBadUrl consumes bytes of a BadUrlToken so that normal tokenization may continue.
|
||||||
|
func (l *Lexer) consumeRemnantsBadURL() {
|
||||||
|
for {
|
||||||
|
if l.consumeByte(')') || l.r.Err() != nil {
|
||||||
|
break
|
||||||
|
} else if !l.consumeEscape() {
|
||||||
|
l.r.Move(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeIdentlike consumes IdentToken, FunctionToken or UrlToken.
|
||||||
|
func (l *Lexer) consumeIdentlike() TokenType {
|
||||||
|
if l.consumeIdentToken() {
|
||||||
|
if l.r.Peek(0) != '(' {
|
||||||
|
return IdentToken
|
||||||
|
} else if !parse.EqualFold(bytes.Replace(l.r.Lexeme(), []byte{'\\'}, nil, -1), []byte{'u', 'r', 'l'}) {
|
||||||
|
l.r.Move(1)
|
||||||
|
return FunctionToken
|
||||||
|
}
|
||||||
|
l.r.Move(1)
|
||||||
|
|
||||||
|
// consume url
|
||||||
|
for l.consumeWhitespace() {
|
||||||
|
}
|
||||||
|
if c := l.r.Peek(0); c == '"' || c == '\'' {
|
||||||
|
if l.consumeString() == BadStringToken {
|
||||||
|
l.consumeRemnantsBadURL()
|
||||||
|
return BadURLToken
|
||||||
|
}
|
||||||
|
} else if !l.consumeUnquotedURL() && !l.consumeWhitespace() { // if unquoted URL fails due to encountering whitespace, continue
|
||||||
|
l.consumeRemnantsBadURL()
|
||||||
|
return BadURLToken
|
||||||
|
}
|
||||||
|
for l.consumeWhitespace() {
|
||||||
|
}
|
||||||
|
if !l.consumeByte(')') && l.r.Err() != io.EOF {
|
||||||
|
l.consumeRemnantsBadURL()
|
||||||
|
return BadURLToken
|
||||||
|
}
|
||||||
|
return URLToken
|
||||||
|
}
|
||||||
|
return ErrorToken
|
||||||
|
}
|
|
@ -0,0 +1,493 @@
|
||||||
|
package css
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2"
|
||||||
|
"github.com/tdewolff/parse/v2/buffer"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wsBytes = []byte(" ")
|
||||||
|
var endBytes = []byte("}")
|
||||||
|
var emptyBytes = []byte("")
|
||||||
|
|
||||||
|
// GrammarType determines the type of grammar.
|
||||||
|
type GrammarType uint32
|
||||||
|
|
||||||
|
// GrammarType values.
|
||||||
|
const (
|
||||||
|
ErrorGrammar GrammarType = iota // extra token when errors occur
|
||||||
|
CommentGrammar
|
||||||
|
AtRuleGrammar
|
||||||
|
BeginAtRuleGrammar
|
||||||
|
EndAtRuleGrammar
|
||||||
|
QualifiedRuleGrammar
|
||||||
|
BeginRulesetGrammar
|
||||||
|
EndRulesetGrammar
|
||||||
|
DeclarationGrammar
|
||||||
|
TokenGrammar
|
||||||
|
CustomPropertyGrammar
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of a GrammarType.
|
||||||
|
func (tt GrammarType) String() string {
|
||||||
|
switch tt {
|
||||||
|
case ErrorGrammar:
|
||||||
|
return "Error"
|
||||||
|
case CommentGrammar:
|
||||||
|
return "Comment"
|
||||||
|
case AtRuleGrammar:
|
||||||
|
return "AtRule"
|
||||||
|
case BeginAtRuleGrammar:
|
||||||
|
return "BeginAtRule"
|
||||||
|
case EndAtRuleGrammar:
|
||||||
|
return "EndAtRule"
|
||||||
|
case QualifiedRuleGrammar:
|
||||||
|
return "QualifiedRule"
|
||||||
|
case BeginRulesetGrammar:
|
||||||
|
return "BeginRuleset"
|
||||||
|
case EndRulesetGrammar:
|
||||||
|
return "EndRuleset"
|
||||||
|
case DeclarationGrammar:
|
||||||
|
return "Declaration"
|
||||||
|
case TokenGrammar:
|
||||||
|
return "Token"
|
||||||
|
case CustomPropertyGrammar:
|
||||||
|
return "CustomProperty"
|
||||||
|
}
|
||||||
|
return "Invalid(" + strconv.Itoa(int(tt)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// State is the state function the parser currently is in.
|
||||||
|
type State func(*Parser) GrammarType
|
||||||
|
|
||||||
|
// Token is a single TokenType and its associated data.
|
||||||
|
type Token struct {
|
||||||
|
TokenType
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Token) String() string {
|
||||||
|
return t.TokenType.String() + "('" + string(t.Data) + "')"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser is the state for the parser.
|
||||||
|
type Parser struct {
|
||||||
|
l *Lexer
|
||||||
|
state []State
|
||||||
|
err string
|
||||||
|
errPos int
|
||||||
|
|
||||||
|
buf []Token
|
||||||
|
level int
|
||||||
|
|
||||||
|
data []byte
|
||||||
|
tt TokenType
|
||||||
|
keepWS bool
|
||||||
|
prevWS bool
|
||||||
|
prevEnd bool
|
||||||
|
prevComment bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser returns a new CSS parser from an io.Reader. isInline specifies whether this is an inline style attribute.
|
||||||
|
func NewParser(r *parse.Input, isInline bool) *Parser {
|
||||||
|
l := NewLexer(r)
|
||||||
|
p := &Parser{
|
||||||
|
l: l,
|
||||||
|
state: make([]State, 0, 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
if isInline {
|
||||||
|
p.state = append(p.state, (*Parser).parseDeclarationList)
|
||||||
|
} else {
|
||||||
|
p.state = append(p.state, (*Parser).parseStylesheet)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasParseError returns true if there is a parse error (and not a read error).
|
||||||
|
func (p *Parser) HasParseError() bool {
|
||||||
|
return p.err != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns the error encountered during parsing, this is often io.EOF but also other errors can be returned.
|
||||||
|
func (p *Parser) Err() error {
|
||||||
|
if p.err != "" {
|
||||||
|
r := buffer.NewReader(p.l.r.Bytes())
|
||||||
|
return parse.NewError(r, p.errPos, p.err)
|
||||||
|
}
|
||||||
|
return p.l.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next Grammar. It returns ErrorGrammar when an error was encountered. Using Err() one can retrieve the error message.
|
||||||
|
func (p *Parser) Next() (GrammarType, TokenType, []byte) {
|
||||||
|
p.err = ""
|
||||||
|
|
||||||
|
if p.prevEnd {
|
||||||
|
p.tt, p.data = RightBraceToken, endBytes
|
||||||
|
p.prevEnd = false
|
||||||
|
} else {
|
||||||
|
p.tt, p.data = p.popToken(true)
|
||||||
|
}
|
||||||
|
gt := p.state[len(p.state)-1](p)
|
||||||
|
return gt, p.tt, p.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset return offset for current Grammar
|
||||||
|
func (p *Parser) Offset() int {
|
||||||
|
return p.l.r.Offset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns a slice of Tokens for the last Grammar. Only AtRuleGrammar, BeginAtRuleGrammar, BeginRulesetGrammar and Declaration will return the at-rule components, ruleset selector and declaration values respectively.
|
||||||
|
func (p *Parser) Values() []Token {
|
||||||
|
return p.buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) popToken(allowComment bool) (TokenType, []byte) {
|
||||||
|
p.prevWS = false
|
||||||
|
p.prevComment = false
|
||||||
|
tt, data := p.l.Next()
|
||||||
|
for !p.keepWS && tt == WhitespaceToken || tt == CommentToken {
|
||||||
|
if tt == WhitespaceToken {
|
||||||
|
p.prevWS = true
|
||||||
|
} else {
|
||||||
|
p.prevComment = true
|
||||||
|
if allowComment && len(p.state) == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tt, data = p.l.Next()
|
||||||
|
}
|
||||||
|
return tt, data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) initBuf() {
|
||||||
|
p.buf = p.buf[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) pushBuf(tt TokenType, data []byte) {
|
||||||
|
p.buf = append(p.buf, Token{tt, data})
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func (p *Parser) parseStylesheet() GrammarType {
|
||||||
|
if p.tt == CDOToken || p.tt == CDCToken {
|
||||||
|
return TokenGrammar
|
||||||
|
} else if p.tt == AtKeywordToken {
|
||||||
|
return p.parseAtRule()
|
||||||
|
} else if p.tt == CommentToken {
|
||||||
|
return CommentGrammar
|
||||||
|
} else if p.tt == ErrorToken {
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
return p.parseQualifiedRule()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseDeclarationList() GrammarType {
|
||||||
|
if p.tt == CommentToken {
|
||||||
|
p.tt, p.data = p.popToken(false)
|
||||||
|
}
|
||||||
|
for p.tt == SemicolonToken {
|
||||||
|
p.tt, p.data = p.popToken(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IE hack: *color:red;
|
||||||
|
if p.tt == DelimToken && p.data[0] == '*' {
|
||||||
|
tt, data := p.popToken(false)
|
||||||
|
p.tt = tt
|
||||||
|
p.data = append(p.data, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.tt == ErrorToken {
|
||||||
|
return ErrorGrammar
|
||||||
|
} else if p.tt == AtKeywordToken {
|
||||||
|
return p.parseAtRule()
|
||||||
|
} else if p.tt == IdentToken || p.tt == DelimToken {
|
||||||
|
return p.parseDeclaration()
|
||||||
|
} else if p.tt == CustomPropertyNameToken {
|
||||||
|
return p.parseCustomProperty()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse error
|
||||||
|
p.initBuf()
|
||||||
|
p.l.r.Move(-len(p.data))
|
||||||
|
p.err, p.errPos = fmt.Sprintf("unexpected token '%s' in declaration", string(p.data)), p.l.r.Offset()
|
||||||
|
p.l.r.Move(len(p.data))
|
||||||
|
|
||||||
|
if p.tt == RightBraceToken {
|
||||||
|
// right brace token will occur when we've had a decl error that ended in a right brace token
|
||||||
|
// as these are not handled by decl error, we handle it here explicitly. Normally its used to end eg. the qual rule.
|
||||||
|
p.pushBuf(p.tt, p.data)
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
return p.parseDeclarationError(p.tt, p.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func (p *Parser) parseAtRule() GrammarType {
|
||||||
|
p.initBuf()
|
||||||
|
p.data = parse.ToLower(parse.Copy(p.data))
|
||||||
|
atRuleName := p.data
|
||||||
|
if len(atRuleName) > 0 && atRuleName[1] == '-' {
|
||||||
|
if i := bytes.IndexByte(atRuleName[2:], '-'); i != -1 {
|
||||||
|
atRuleName = atRuleName[i+2:] // skip vendor specific prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atRule := ToHash(atRuleName[1:])
|
||||||
|
|
||||||
|
first := true
|
||||||
|
skipWS := false
|
||||||
|
for {
|
||||||
|
tt, data := p.popToken(false)
|
||||||
|
if tt == LeftBraceToken && p.level == 0 {
|
||||||
|
if atRule == Font_Face || atRule == Page {
|
||||||
|
p.state = append(p.state, (*Parser).parseAtRuleDeclarationList)
|
||||||
|
} else if atRule == Document || atRule == Keyframes || atRule == Media || atRule == Supports {
|
||||||
|
p.state = append(p.state, (*Parser).parseAtRuleRuleList)
|
||||||
|
} else {
|
||||||
|
p.state = append(p.state, (*Parser).parseAtRuleUnknown)
|
||||||
|
}
|
||||||
|
return BeginAtRuleGrammar
|
||||||
|
} else if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken {
|
||||||
|
p.prevEnd = (tt == RightBraceToken)
|
||||||
|
return AtRuleGrammar
|
||||||
|
} else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken {
|
||||||
|
if p.level == 0 {
|
||||||
|
// TODO: buggy
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
if 1 < len(p.state) {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
}
|
||||||
|
p.err, p.errPos = "unexpected ending in at rule", p.l.r.Offset()
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
if tt == LeftParenthesisToken || tt == LeftBracketToken {
|
||||||
|
p.prevWS = false
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
if len(data) == 1 && (data[0] == ',' || data[0] == ':') {
|
||||||
|
skipWS = true
|
||||||
|
} else if p.prevWS && !skipWS && tt != RightParenthesisToken {
|
||||||
|
p.pushBuf(WhitespaceToken, wsBytes)
|
||||||
|
} else {
|
||||||
|
skipWS = false
|
||||||
|
}
|
||||||
|
if tt == LeftParenthesisToken {
|
||||||
|
skipWS = true
|
||||||
|
}
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseAtRuleRuleList() GrammarType {
|
||||||
|
if p.tt == RightBraceToken || p.tt == ErrorToken {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
return EndAtRuleGrammar
|
||||||
|
} else if p.tt == AtKeywordToken {
|
||||||
|
return p.parseAtRule()
|
||||||
|
} else {
|
||||||
|
return p.parseQualifiedRule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseAtRuleDeclarationList() GrammarType {
|
||||||
|
for p.tt == SemicolonToken {
|
||||||
|
p.tt, p.data = p.popToken(false)
|
||||||
|
}
|
||||||
|
if p.tt == RightBraceToken || p.tt == ErrorToken {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
return EndAtRuleGrammar
|
||||||
|
}
|
||||||
|
return p.parseDeclarationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseAtRuleUnknown() GrammarType {
|
||||||
|
p.keepWS = true
|
||||||
|
if p.tt == RightBraceToken && p.level == 0 || p.tt == ErrorToken {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
p.keepWS = false
|
||||||
|
return EndAtRuleGrammar
|
||||||
|
}
|
||||||
|
if p.tt == LeftParenthesisToken || p.tt == LeftBraceToken || p.tt == LeftBracketToken || p.tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if p.tt == RightParenthesisToken || p.tt == RightBraceToken || p.tt == RightBracketToken {
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
return TokenGrammar
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseQualifiedRule() GrammarType {
|
||||||
|
p.initBuf()
|
||||||
|
first := true
|
||||||
|
inAttrSel := false
|
||||||
|
skipWS := true
|
||||||
|
var tt TokenType
|
||||||
|
var data []byte
|
||||||
|
for {
|
||||||
|
if first {
|
||||||
|
tt, data = p.tt, p.data
|
||||||
|
p.tt = WhitespaceToken
|
||||||
|
p.data = emptyBytes
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
tt, data = p.popToken(false)
|
||||||
|
}
|
||||||
|
if tt == LeftBraceToken && p.level == 0 {
|
||||||
|
p.state = append(p.state, (*Parser).parseQualifiedRuleDeclarationList)
|
||||||
|
return BeginRulesetGrammar
|
||||||
|
} else if tt == ErrorToken {
|
||||||
|
p.err, p.errPos = "unexpected ending in qualified rule", p.l.r.Offset()
|
||||||
|
return ErrorGrammar
|
||||||
|
} else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken {
|
||||||
|
if p.level == 0 {
|
||||||
|
// TODO: buggy
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
if 1 < len(p.state) {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
}
|
||||||
|
p.err, p.errPos = "unexpected ending in qualified rule", p.l.r.Offset()
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
if len(data) == 1 && (data[0] == ',' || data[0] == '>' || data[0] == '+' || data[0] == '~') {
|
||||||
|
if data[0] == ',' {
|
||||||
|
return QualifiedRuleGrammar
|
||||||
|
}
|
||||||
|
skipWS = true
|
||||||
|
} else if p.prevWS && !skipWS && !inAttrSel {
|
||||||
|
p.pushBuf(WhitespaceToken, wsBytes)
|
||||||
|
} else {
|
||||||
|
skipWS = false
|
||||||
|
}
|
||||||
|
if tt == LeftBracketToken {
|
||||||
|
inAttrSel = true
|
||||||
|
} else if tt == RightBracketToken {
|
||||||
|
inAttrSel = false
|
||||||
|
}
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseQualifiedRuleDeclarationList() GrammarType {
|
||||||
|
for p.tt == SemicolonToken {
|
||||||
|
p.tt, p.data = p.popToken(false)
|
||||||
|
}
|
||||||
|
if p.tt == RightBraceToken || p.tt == ErrorToken {
|
||||||
|
p.state = p.state[:len(p.state)-1]
|
||||||
|
return EndRulesetGrammar
|
||||||
|
}
|
||||||
|
return p.parseDeclarationList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseDeclaration() GrammarType {
|
||||||
|
p.initBuf()
|
||||||
|
p.data = parse.ToLower(parse.Copy(p.data))
|
||||||
|
|
||||||
|
ttName, dataName := p.tt, p.data
|
||||||
|
tt, data := p.popToken(false)
|
||||||
|
if tt != ColonToken {
|
||||||
|
p.l.r.Move(-len(data))
|
||||||
|
p.err, p.errPos = "expected colon in declaration", p.l.r.Offset()
|
||||||
|
p.l.r.Move(len(data))
|
||||||
|
p.pushBuf(ttName, dataName)
|
||||||
|
return p.parseDeclarationError(tt, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
skipWS := true
|
||||||
|
for {
|
||||||
|
tt, data := p.popToken(false)
|
||||||
|
if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken {
|
||||||
|
p.prevEnd = (tt == RightBraceToken)
|
||||||
|
return DeclarationGrammar
|
||||||
|
} else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken {
|
||||||
|
if p.level == 0 {
|
||||||
|
// TODO: buggy
|
||||||
|
p.err, p.errPos = "unexpected ending in declaration", p.l.r.Offset()
|
||||||
|
p.pushBuf(ttName, dataName)
|
||||||
|
p.pushBuf(ColonToken, []byte{':'})
|
||||||
|
return p.parseDeclarationError(tt, data)
|
||||||
|
}
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
if len(data) == 1 && (data[0] == ',' || data[0] == '/' || data[0] == ':' || data[0] == '!' || data[0] == '=') {
|
||||||
|
skipWS = true
|
||||||
|
} else if (p.prevWS || p.prevComment) && !skipWS {
|
||||||
|
p.pushBuf(WhitespaceToken, wsBytes)
|
||||||
|
} else {
|
||||||
|
skipWS = false
|
||||||
|
}
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseDeclarationError(tt TokenType, data []byte) GrammarType {
|
||||||
|
// we're on the offending (tt,data), keep popping tokens till we reach ;, }, or EOF
|
||||||
|
p.tt, p.data = tt, data
|
||||||
|
for {
|
||||||
|
if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken {
|
||||||
|
p.prevEnd = (tt == RightBraceToken)
|
||||||
|
if tt == SemicolonToken {
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
}
|
||||||
|
return ErrorGrammar
|
||||||
|
} else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken {
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.prevWS {
|
||||||
|
p.pushBuf(WhitespaceToken, wsBytes)
|
||||||
|
}
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
|
||||||
|
tt, data = p.popToken(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseCustomProperty() GrammarType {
|
||||||
|
p.initBuf()
|
||||||
|
if tt, data := p.popToken(false); tt != ColonToken {
|
||||||
|
p.l.r.Move(-len(data))
|
||||||
|
p.err, p.errPos = "expected colon in custom property", p.l.r.Offset()
|
||||||
|
p.l.r.Move(len(data))
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
val := []byte{}
|
||||||
|
for {
|
||||||
|
tt, data := p.l.Next()
|
||||||
|
if (tt == SemicolonToken || tt == RightBraceToken) && p.level == 0 || tt == ErrorToken {
|
||||||
|
p.prevEnd = (tt == RightBraceToken)
|
||||||
|
p.pushBuf(CustomPropertyValueToken, val)
|
||||||
|
return CustomPropertyGrammar
|
||||||
|
} else if tt == LeftParenthesisToken || tt == LeftBraceToken || tt == LeftBracketToken || tt == FunctionToken {
|
||||||
|
p.level++
|
||||||
|
} else if tt == RightParenthesisToken || tt == RightBraceToken || tt == RightBracketToken {
|
||||||
|
if p.level == 0 {
|
||||||
|
// TODO: buggy
|
||||||
|
p.pushBuf(tt, data)
|
||||||
|
p.err, p.errPos = "unexpected ending in custom property", p.l.r.Offset()
|
||||||
|
return ErrorGrammar
|
||||||
|
}
|
||||||
|
p.level--
|
||||||
|
}
|
||||||
|
val = append(val, data...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package css
|
||||||
|
|
||||||
|
import "github.com/tdewolff/parse/v2"
|
||||||
|
|
||||||
|
// IsIdent returns true if the bytes are a valid identifier.
|
||||||
|
func IsIdent(b []byte) bool {
|
||||||
|
l := NewLexer(parse.NewInputBytes(b))
|
||||||
|
l.consumeIdentToken()
|
||||||
|
l.r.Restore()
|
||||||
|
return l.r.Pos() == len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsURLUnquoted returns true if the bytes are a valid unquoted URL.
|
||||||
|
func IsURLUnquoted(b []byte) bool {
|
||||||
|
l := NewLexer(parse.NewInputBytes(b))
|
||||||
|
l.consumeUnquotedURL()
|
||||||
|
l.r.Restore()
|
||||||
|
return l.r.Pos() == len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSL2RGB converts HSL to RGB with all of range [0,1]
|
||||||
|
// from http://www.w3.org/TR/css3-color/#hsl-color
|
||||||
|
func HSL2RGB(h, s, l float64) (float64, float64, float64) {
|
||||||
|
m2 := l * (s + 1)
|
||||||
|
if l > 0.5 {
|
||||||
|
m2 = l + s - l*s
|
||||||
|
}
|
||||||
|
m1 := l*2 - m2
|
||||||
|
return hue2rgb(m1, m2, h+1.0/3.0), hue2rgb(m1, m2, h), hue2rgb(m1, m2, h-1.0/3.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hue2rgb(m1, m2, h float64) float64 {
|
||||||
|
if h < 0.0 {
|
||||||
|
h += 1.0
|
||||||
|
}
|
||||||
|
if h > 1.0 {
|
||||||
|
h -= 1.0
|
||||||
|
}
|
||||||
|
if h*6.0 < 1.0 {
|
||||||
|
return m1 + (m2-m1)*h*6.0
|
||||||
|
} else if h*2.0 < 1.0 {
|
||||||
|
return m2
|
||||||
|
} else if h*3.0 < 2.0 {
|
||||||
|
return m1 + (m2-m1)*(2.0/3.0-h)*6.0
|
||||||
|
}
|
||||||
|
return m1
|
||||||
|
}
|
|
@ -166,6 +166,7 @@ func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
isEndTag := c == '/' && l.r.Peek(2) != '>' && (l.r.Peek(2) != 0 || l.r.PeekErr(2) == nil)
|
isEndTag := c == '/' && l.r.Peek(2) != '>' && (l.r.Peek(2) != 0 || l.r.PeekErr(2) == nil)
|
||||||
if !isEndTag && (c < 'a' || 'z' < c) && (c < 'A' || 'Z' < c) && c != '!' && c != '?' {
|
if !isEndTag && (c < 'a' || 'z' < c) && (c < 'A' || 'Z' < c) && c != '!' && c != '?' {
|
||||||
// not a tag
|
// not a tag
|
||||||
|
l.r.Move(1)
|
||||||
} else if 0 < l.r.Pos() {
|
} else if 0 < l.r.Pos() {
|
||||||
// return currently buffered texttoken so that we can return tag next iteration
|
// return currently buffered texttoken so that we can return tag next iteration
|
||||||
l.text = l.r.Shift()
|
l.text = l.r.Shift()
|
||||||
|
@ -202,10 +203,11 @@ func (l *Lexer) Next() (TokenType, []byte) {
|
||||||
return TextToken, l.text
|
return TextToken, l.text
|
||||||
}
|
}
|
||||||
return ErrorToken, nil
|
return ErrorToken, nil
|
||||||
}
|
} else {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -539,19 +541,19 @@ func (l *Lexer) shiftXML(rawTag Hash) []byte {
|
||||||
|
|
||||||
func (l *Lexer) moveTemplate() {
|
func (l *Lexer) moveTemplate() {
|
||||||
for {
|
for {
|
||||||
if c := l.r.Peek(0); l.at(l.tmplEnd...) || c == 0 && l.r.Err() != nil {
|
if c := l.r.Peek(0); c == 0 && l.r.Err() != nil {
|
||||||
if c != 0 {
|
return
|
||||||
|
} else if l.at(l.tmplEnd...) {
|
||||||
l.r.Move(len(l.tmplEnd))
|
l.r.Move(len(l.tmplEnd))
|
||||||
}
|
return
|
||||||
break
|
|
||||||
} else if c == '"' || c == '\'' {
|
} else if c == '"' || c == '\'' {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
escape := false
|
escape := false
|
||||||
for {
|
for {
|
||||||
if c2 := l.r.Peek(0); !escape && c2 == c || c2 == 0 && l.r.Err() != nil {
|
if c2 := l.r.Peek(0); c2 == 0 && l.r.Err() != nil {
|
||||||
if c2 != 0 {
|
return
|
||||||
|
} else if !escape && c2 == c {
|
||||||
l.r.Move(1)
|
l.r.Move(1)
|
||||||
}
|
|
||||||
break
|
break
|
||||||
} else if c2 == '\\' {
|
} else if c2 == '\\' {
|
||||||
escape = !escape
|
escape = !escape
|
||||||
|
|
|
@ -0,0 +1,403 @@
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tdewolff/parse/v2"
|
||||||
|
"github.com/tdewolff/parse/v2/css"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AST struct {
|
||||||
|
Children []*Tag
|
||||||
|
Text []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *AST) String() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for i, child := range ast.Children {
|
||||||
|
if i != 0 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
sb.WriteString(child.ASTString())
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attr struct {
|
||||||
|
Key, Val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attr *Attr) String() string {
|
||||||
|
return fmt.Sprintf(`%s="%s"`, string(attr.Key), string(attr.Val))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Root *AST
|
||||||
|
Parent *Tag
|
||||||
|
Prev, Next *Tag
|
||||||
|
Children []*Tag
|
||||||
|
Index int
|
||||||
|
|
||||||
|
Name []byte
|
||||||
|
Attrs []Attr
|
||||||
|
textStart, textEnd int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) getAttr(key []byte) ([]byte, bool) {
|
||||||
|
for _, attr := range tag.Attrs {
|
||||||
|
if bytes.Equal(key, attr.Key) {
|
||||||
|
return attr.Val, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) GetAttr(key string) (string, bool) {
|
||||||
|
val, ok := tag.getAttr([]byte(key))
|
||||||
|
return string(val), ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) Text() string {
|
||||||
|
return string(tag.Root.Text[tag.textStart:tag.textEnd])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) String() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString("<")
|
||||||
|
sb.Write(tag.Name)
|
||||||
|
for _, attr := range tag.Attrs {
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(attr.String())
|
||||||
|
}
|
||||||
|
sb.WriteString(">")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) ASTString() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.WriteString(tag.String())
|
||||||
|
for _, child := range tag.Children {
|
||||||
|
sb.WriteString("\n ")
|
||||||
|
s := child.ASTString()
|
||||||
|
s = strings.ReplaceAll(s, "\n", "\n ")
|
||||||
|
sb.WriteString(s)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Parse(r *parse.Input) (*AST, error) {
|
||||||
|
ast := &AST{}
|
||||||
|
root := &Tag{}
|
||||||
|
cur := root
|
||||||
|
|
||||||
|
l := NewLexer(r)
|
||||||
|
for {
|
||||||
|
tt, data := l.Next()
|
||||||
|
switch tt {
|
||||||
|
case ErrorToken:
|
||||||
|
if err := l.Err(); err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ast.Children = root.Children
|
||||||
|
return ast, nil
|
||||||
|
case TextToken:
|
||||||
|
ast.Text = append(ast.Text, data...)
|
||||||
|
case StartTagToken:
|
||||||
|
child := &Tag{
|
||||||
|
Root: ast,
|
||||||
|
Parent: cur,
|
||||||
|
Index: len(cur.Children),
|
||||||
|
Name: l.Text(),
|
||||||
|
textStart: len(ast.Text),
|
||||||
|
}
|
||||||
|
if 0 < len(cur.Children) {
|
||||||
|
child.Prev = cur.Children[len(cur.Children)-1]
|
||||||
|
child.Prev.Next = child
|
||||||
|
}
|
||||||
|
cur.Children = append(cur.Children, child)
|
||||||
|
cur = child
|
||||||
|
case AttributeToken:
|
||||||
|
val := l.AttrVal()
|
||||||
|
if 0 < len(val) && (val[0] == '"' || val[0] == '\'') {
|
||||||
|
val = val[1 : len(val)-1]
|
||||||
|
}
|
||||||
|
cur.Attrs = append(cur.Attrs, Attr{l.AttrKey(), val})
|
||||||
|
case StartTagCloseToken:
|
||||||
|
if voidTags[string(cur.Name)] {
|
||||||
|
cur.textEnd = len(ast.Text)
|
||||||
|
cur = cur.Parent
|
||||||
|
}
|
||||||
|
case EndTagToken, StartTagVoidToken:
|
||||||
|
start := cur
|
||||||
|
for start != root && !bytes.Equal(l.Text(), start.Name) {
|
||||||
|
start = start.Parent
|
||||||
|
}
|
||||||
|
if start == root {
|
||||||
|
// ignore
|
||||||
|
} else {
|
||||||
|
parent := start.Parent
|
||||||
|
for cur != parent {
|
||||||
|
cur.textEnd = len(ast.Text)
|
||||||
|
cur = cur.Parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *AST) Query(s string) (*Tag, error) {
|
||||||
|
sel, err := ParseSelector(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range ast.Children {
|
||||||
|
if match := child.query(sel); match != nil {
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) query(sel selector) *Tag {
|
||||||
|
if sel.AppliesTo(tag) {
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
for _, child := range tag.Children {
|
||||||
|
if match := child.query(sel); match != nil {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ast *AST) QueryAll(s string) ([]*Tag, error) {
|
||||||
|
sel, err := ParseSelector(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := []*Tag{}
|
||||||
|
for _, child := range ast.Children {
|
||||||
|
child.queryAll(&matches, sel)
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tag *Tag) queryAll(matches *[]*Tag, sel selector) {
|
||||||
|
if sel.AppliesTo(tag) {
|
||||||
|
*matches = append(*matches, tag)
|
||||||
|
}
|
||||||
|
for _, child := range tag.Children {
|
||||||
|
child.queryAll(matches, sel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type attrSelector struct {
|
||||||
|
op byte // empty, =, ~, |
|
||||||
|
attr []byte
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sel attrSelector) AppliesTo(tag *Tag) bool {
|
||||||
|
val, ok := tag.getAttr(sel.attr)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sel.op {
|
||||||
|
case 0:
|
||||||
|
return true
|
||||||
|
case '=':
|
||||||
|
return bytes.Equal(val, sel.val)
|
||||||
|
case '~':
|
||||||
|
if 0 < len(sel.val) {
|
||||||
|
vals := bytes.Split(val, []byte(" "))
|
||||||
|
for _, val := range vals {
|
||||||
|
if bytes.Equal(val, sel.val) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case '|':
|
||||||
|
return bytes.Equal(val, sel.val) || bytes.HasPrefix(val, append(sel.val, '-'))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attr attrSelector) String() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Write(attr.attr)
|
||||||
|
if attr.op != 0 {
|
||||||
|
sb.WriteByte(attr.op)
|
||||||
|
if attr.op != '=' {
|
||||||
|
sb.WriteByte('=')
|
||||||
|
}
|
||||||
|
sb.WriteByte('"')
|
||||||
|
sb.Write(attr.val)
|
||||||
|
sb.WriteByte('"')
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type selectorNode struct {
|
||||||
|
typ []byte // is * for universal
|
||||||
|
attrs []attrSelector
|
||||||
|
op byte // space or >, last is NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sel selectorNode) AppliesTo(tag *Tag) bool {
|
||||||
|
if 0 < len(sel.typ) && !bytes.Equal(sel.typ, []byte("*")) && !bytes.Equal(sel.typ, tag.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, attr := range sel.attrs {
|
||||||
|
if !attr.AppliesTo(tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sel selectorNode) String() string {
|
||||||
|
sb := strings.Builder{}
|
||||||
|
sb.Write(sel.typ)
|
||||||
|
for _, attr := range sel.attrs {
|
||||||
|
if bytes.Equal(attr.attr, []byte("id")) && attr.op == '=' {
|
||||||
|
sb.WriteByte('#')
|
||||||
|
sb.Write(attr.val)
|
||||||
|
} else if bytes.Equal(attr.attr, []byte("class")) && attr.op == '~' {
|
||||||
|
sb.WriteByte('.')
|
||||||
|
sb.Write(attr.val)
|
||||||
|
} else {
|
||||||
|
sb.WriteByte('[')
|
||||||
|
sb.WriteString(attr.String())
|
||||||
|
sb.WriteByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sel.op != 0 {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteByte(sel.op)
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
tt css.TokenType
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type selector []selectorNode
|
||||||
|
|
||||||
|
func ParseSelector(s string) (selector, error) {
|
||||||
|
ts := []token{}
|
||||||
|
l := css.NewLexer(parse.NewInputString(s))
|
||||||
|
for {
|
||||||
|
tt, data := l.Next()
|
||||||
|
if tt == css.ErrorToken {
|
||||||
|
if err := l.Err(); err != io.EOF {
|
||||||
|
return selector{}, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ts = append(ts, token{
|
||||||
|
tt: tt,
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sel := selector{}
|
||||||
|
node := selectorNode{}
|
||||||
|
for i := 0; i < len(ts); i++ {
|
||||||
|
t := ts[i]
|
||||||
|
if 0 < i && (t.tt == css.WhitespaceToken || t.tt == css.DelimToken && t.data[0] == '>') {
|
||||||
|
if t.tt == css.DelimToken {
|
||||||
|
node.op = '>'
|
||||||
|
} else {
|
||||||
|
node.op = ' '
|
||||||
|
}
|
||||||
|
sel = append(sel, node)
|
||||||
|
node = selectorNode{}
|
||||||
|
} else if t.tt == css.IdentToken || t.tt == css.DelimToken && t.data[0] == '*' {
|
||||||
|
node.typ = t.data
|
||||||
|
} else if t.tt == css.DelimToken && (t.data[0] == '.' || t.data[0] == '#') && i+1 < len(ts) && ts[i+1].tt == css.IdentToken {
|
||||||
|
if t.data[0] == '#' {
|
||||||
|
node.attrs = append(node.attrs, attrSelector{op: '=', attr: []byte("id"), val: ts[i+1].data})
|
||||||
|
} else {
|
||||||
|
node.attrs = append(node.attrs, attrSelector{op: '~', attr: []byte("class"), val: ts[i+1].data})
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
} else if t.tt == css.DelimToken && t.data[0] == '[' && i+2 < len(ts) && ts[i+1].tt == css.IdentToken && ts[i+2].tt == css.DelimToken {
|
||||||
|
if ts[i+2].data[0] == ']' {
|
||||||
|
node.attrs = append(node.attrs, attrSelector{op: 0, attr: ts[i+1].data})
|
||||||
|
i += 2
|
||||||
|
} else if i+4 < len(ts) && ts[i+3].tt == css.IdentToken && ts[i+4].tt == css.DelimToken && ts[i+4].data[0] == ']' {
|
||||||
|
node.attrs = append(node.attrs, attrSelector{op: ts[i+2].data[0], attr: ts[i+1].data, val: ts[i+3].data})
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sel = append(sel, node)
|
||||||
|
return sel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sels selector) AppliesTo(tag *Tag) bool {
|
||||||
|
if len(sels) == 0 {
|
||||||
|
return true
|
||||||
|
} else if !sels[len(sels)-1].AppliesTo(tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = tag.Parent
|
||||||
|
isel := len(sels) - 2
|
||||||
|
for 0 <= isel && tag != nil {
|
||||||
|
switch sels[isel].op {
|
||||||
|
case ' ':
|
||||||
|
for tag != nil {
|
||||||
|
if sels[isel].AppliesTo(tag) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tag = tag.Parent
|
||||||
|
}
|
||||||
|
case '>':
|
||||||
|
if !sels[isel].AppliesTo(tag) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tag = tag.Parent
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
isel--
|
||||||
|
}
|
||||||
|
return len(sels) != 0 && isel == -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sels selector) String() string {
|
||||||
|
if len(sels) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sb := strings.Builder{}
|
||||||
|
for _, sel := range sels {
|
||||||
|
sb.WriteString(sel.String())
|
||||||
|
}
|
||||||
|
return sb.String()[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var voidTags = map[string]bool{
|
||||||
|
"area": true,
|
||||||
|
"base": true,
|
||||||
|
"br": true,
|
||||||
|
"col": true,
|
||||||
|
"embed": true,
|
||||||
|
"hr": true,
|
||||||
|
"img": true,
|
||||||
|
"input": true,
|
||||||
|
"link": true,
|
||||||
|
"meta": true,
|
||||||
|
"source": true,
|
||||||
|
"track": true,
|
||||||
|
"wbr": true,
|
||||||
|
}
|
|
@ -672,14 +672,15 @@ github.com/superseriousbusiness/oauth2/v4/generates
|
||||||
github.com/superseriousbusiness/oauth2/v4/manage
|
github.com/superseriousbusiness/oauth2/v4/manage
|
||||||
github.com/superseriousbusiness/oauth2/v4/models
|
github.com/superseriousbusiness/oauth2/v4/models
|
||||||
github.com/superseriousbusiness/oauth2/v4/server
|
github.com/superseriousbusiness/oauth2/v4/server
|
||||||
# github.com/tdewolff/minify/v2 v2.20.9
|
# github.com/tdewolff/minify/v2 v2.20.12
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/tdewolff/minify/v2
|
github.com/tdewolff/minify/v2
|
||||||
github.com/tdewolff/minify/v2/html
|
github.com/tdewolff/minify/v2/html
|
||||||
# github.com/tdewolff/parse/v2 v2.7.6
|
# github.com/tdewolff/parse/v2 v2.7.7
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/tdewolff/parse/v2
|
github.com/tdewolff/parse/v2
|
||||||
github.com/tdewolff/parse/v2/buffer
|
github.com/tdewolff/parse/v2/buffer
|
||||||
|
github.com/tdewolff/parse/v2/css
|
||||||
github.com/tdewolff/parse/v2/html
|
github.com/tdewolff/parse/v2/html
|
||||||
github.com/tdewolff/parse/v2/strconv
|
github.com/tdewolff/parse/v2/strconv
|
||||||
# github.com/technologize/otel-go-contrib v1.1.0
|
# github.com/technologize/otel-go-contrib v1.1.0
|
||||||
|
|
Loading…
Reference in New Issue