mirror of
https://github.com/yuin/goldmark
synced 2025-03-04 23:04:52 +00:00
Add WithAttribute
This commit is contained in:
parent
3bf70b9428
commit
785421acb4
8 changed files with 216 additions and 42 deletions
18
README.md
18
README.md
|
|
@ -91,6 +91,7 @@ Parser and Renderer options
|
||||||
| `parser.WithInlineParsers` | A `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. |
|
| `parser.WithInlineParsers` | A `util.PrioritizedSlice` whose elements are `parser.InlineParser` | Parsers for parsing inline level elements. |
|
||||||
| `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
|
| `parser.WithParagraphTransformers` | A `util.PrioritizedSlice` whose elements are `parser.ParagraphTransformer` | Transformers for transforming paragraph nodes. |
|
||||||
| `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. |
|
| `parser.WithAutoHeadingID` | `-` | Enables auto heading ids. |
|
||||||
|
| `parser.WithAttribute` | `-` | Enables custom attributes. Currently only headings supports attributes. |
|
||||||
| `parser.WithFilterTags` | `...string` | HTML tag names forbidden in HTML blocks and Raw HTMLs. |
|
| `parser.WithFilterTags` | `...string` | HTML tag names forbidden in HTML blocks and Raw HTMLs. |
|
||||||
|
|
||||||
### HTML Renderer options
|
### HTML Renderer options
|
||||||
|
|
@ -120,6 +121,23 @@ Parser and Renderer options
|
||||||
- `extension.Footnote`
|
- `extension.Footnote`
|
||||||
- [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes)
|
- [PHP Markdown Extra: Footnotes](https://michelf.ca/projects/php-markdown/extra/#footnotes)
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
`parser.WithAttribute` option allows you to define attributes on some elements.
|
||||||
|
|
||||||
|
Currently only headings supports attributes.
|
||||||
|
|
||||||
|
#### Headings
|
||||||
|
|
||||||
|
```
|
||||||
|
## heading ## {#id .className attrName=attrValue class="class1 class2"}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
heading {#id .className attrName=attrValue}
|
||||||
|
============
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
Create extensions
|
Create extensions
|
||||||
--------------------
|
--------------------
|
||||||
**TODO**
|
**TODO**
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,9 @@ type Node interface {
|
||||||
// Attributes returns a list of attributes.
|
// Attributes returns a list of attributes.
|
||||||
// This may be a nil if there are no attributes.
|
// This may be a nil if there are no attributes.
|
||||||
Attributes() []Attribute
|
Attributes() []Attribute
|
||||||
|
|
||||||
|
// RemoveAttributes removes all attributes from this node.
|
||||||
|
RemoveAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// A BaseNode struct implements the Node interface.
|
// A BaseNode struct implements the Node interface.
|
||||||
|
|
@ -371,6 +374,11 @@ func (n *BaseNode) Attributes() []Attribute {
|
||||||
return n.attributes
|
return n.attributes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveAttributes implements Node.RemoveAttributes
|
||||||
|
func (n *BaseNode) RemoveAttributes() {
|
||||||
|
n.attributes = nil
|
||||||
|
}
|
||||||
|
|
||||||
// DumpHelper is a helper function to implement Node.Dump.
|
// DumpHelper is a helper function to implement Node.Dump.
|
||||||
// kv is pairs of an attribute name and an attribute value.
|
// kv is pairs of an attribute name and an attribute value.
|
||||||
// cb is a function called after wrote a name and attributes.
|
// cb is a function called after wrote a name and attributes.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import (
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/text"
|
"github.com/yuin/goldmark/text"
|
||||||
"github.com/yuin/goldmark/util"
|
"github.com/yuin/goldmark/util"
|
||||||
"regexp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings.
|
// A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings.
|
||||||
type HeadingConfig struct {
|
type HeadingConfig struct {
|
||||||
AutoHeadingID bool
|
AutoHeadingID bool
|
||||||
|
Attribute bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOption implements SetOptioner.
|
// SetOption implements SetOptioner.
|
||||||
|
|
@ -17,11 +17,14 @@ func (b *HeadingConfig) SetOption(name OptionName, value interface{}) {
|
||||||
switch name {
|
switch name {
|
||||||
case AutoHeadingID:
|
case AutoHeadingID:
|
||||||
b.AutoHeadingID = true
|
b.AutoHeadingID = true
|
||||||
|
case Attribute:
|
||||||
|
b.Attribute = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A HeadingOption interface sets options for heading parsers.
|
// A HeadingOption interface sets options for heading parsers.
|
||||||
type HeadingOption interface {
|
type HeadingOption interface {
|
||||||
|
Option
|
||||||
SetHeadingOption(*HeadingConfig)
|
SetHeadingOption(*HeadingConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +34,7 @@ var AutoHeadingID OptionName = "AutoHeadingID"
|
||||||
type withAutoHeadingID struct {
|
type withAutoHeadingID struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withAutoHeadingID) SetConfig(c *Config) {
|
func (o *withAutoHeadingID) SetParserOption(c *Config) {
|
||||||
c.Options[AutoHeadingID] = true
|
c.Options[AutoHeadingID] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,14 +44,22 @@ func (o *withAutoHeadingID) SetHeadingOption(p *HeadingConfig) {
|
||||||
|
|
||||||
// WithAutoHeadingID is a functional option that enables custom heading ids and
|
// WithAutoHeadingID is a functional option that enables custom heading ids and
|
||||||
// auto generated heading ids.
|
// auto generated heading ids.
|
||||||
func WithAutoHeadingID() interface {
|
func WithAutoHeadingID() HeadingOption {
|
||||||
Option
|
|
||||||
HeadingOption
|
|
||||||
} {
|
|
||||||
return &withAutoHeadingID{}
|
return &withAutoHeadingID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var atxHeadingRegexp = regexp.MustCompile(`^[ ]{0,3}(#{1,6})(?:\s+(.*?)\s*([\s]#+\s*)?)?\n?$`)
|
type withHeadingAttribute struct {
|
||||||
|
Option
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withHeadingAttribute) SetHeadingOption(p *HeadingConfig) {
|
||||||
|
p.Attribute = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeadingAttribute is a functional option that enables custom heading attributes.
|
||||||
|
func WithHeadingAttribute() HeadingOption {
|
||||||
|
return &withHeadingAttribute{WithAttribute()}
|
||||||
|
}
|
||||||
|
|
||||||
type atxHeadingParser struct {
|
type atxHeadingParser struct {
|
||||||
HeadingConfig
|
HeadingConfig
|
||||||
|
|
@ -79,22 +90,70 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context)
|
||||||
}
|
}
|
||||||
start := i + l
|
start := i + l
|
||||||
stop := len(line) - util.TrimRightSpaceLength(line)
|
stop := len(line) - util.TrimRightSpaceLength(line)
|
||||||
if stop <= start { // empty headings like '##[space]'
|
|
||||||
stop = start + 1
|
|
||||||
} else {
|
|
||||||
i = stop - 1
|
|
||||||
for ; line[i] == '#' && i >= start; i-- {
|
|
||||||
}
|
|
||||||
if i != stop-1 && !util.IsSpace(line[i]) {
|
|
||||||
i = stop - 1
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
stop = i
|
|
||||||
}
|
|
||||||
|
|
||||||
node := ast.NewHeading(level)
|
node := ast.NewHeading(level)
|
||||||
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
|
parsed := false
|
||||||
node.Lines().Append(text.NewSegment(segment.Start+start, segment.Start+stop))
|
if b.Attribute { // handles special case like ### heading ### {#id}
|
||||||
|
start--
|
||||||
|
closureOpen := -1
|
||||||
|
closureClose := -1
|
||||||
|
for i := start; i < stop; {
|
||||||
|
c := line[i]
|
||||||
|
if util.IsEscapedPunctuation(line, i) {
|
||||||
|
i += 2
|
||||||
|
} else if util.IsSpace(c) && i < stop-1 && line[i+1] == '#' {
|
||||||
|
closureOpen = i + 1
|
||||||
|
j := i + 1
|
||||||
|
for ; j < stop && line[j] == '#'; j++ {
|
||||||
|
}
|
||||||
|
closureClose = j
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if closureClose > 0 {
|
||||||
|
i := closureClose
|
||||||
|
for ; i < stop && util.IsSpace(line[i]); i++ {
|
||||||
|
}
|
||||||
|
if i < stop-1 || line[i] == '{' {
|
||||||
|
as := i + 1
|
||||||
|
for as < stop {
|
||||||
|
ai := util.FindAttributeIndex(line[as:], true)
|
||||||
|
if ai[0] < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
node.SetAttribute(line[as+ai[0]:as+ai[1]],
|
||||||
|
line[as+ai[2]:as+ai[3]])
|
||||||
|
as += ai[3]
|
||||||
|
}
|
||||||
|
if line[as] == '}' && (as > stop-2 || util.IsBlank(line[as:])) {
|
||||||
|
parsed = true
|
||||||
|
node.Lines().Append(text.NewSegment(segment.Start+start+1, segment.Start+closureOpen))
|
||||||
|
} else {
|
||||||
|
node.RemoveAttributes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !parsed {
|
||||||
|
stop := len(line) - util.TrimRightSpaceLength(line)
|
||||||
|
if stop <= start { // empty headings like '##[space]'
|
||||||
|
stop = start + 1
|
||||||
|
} else {
|
||||||
|
i = stop - 1
|
||||||
|
for ; line[i] == '#' && i >= start; i-- {
|
||||||
|
}
|
||||||
|
if i != stop-1 && !util.IsSpace(line[i]) {
|
||||||
|
i = stop - 1
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
stop = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
|
||||||
|
node.Lines().Append(text.NewSegment(segment.Start+start, segment.Start+stop))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return node, NoChildren
|
return node, NoChildren
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +166,12 @@ func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context)
|
||||||
if !b.AutoHeadingID {
|
if !b.AutoHeadingID {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
generateAutoHeadingID(node.(*ast.Heading), reader, pc)
|
if !b.Attribute {
|
||||||
|
_, ok := node.AttributeString("id")
|
||||||
|
if !ok {
|
||||||
|
generateAutoHeadingID(node.(*ast.Heading), reader, pc)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *atxHeadingParser) CanInterruptParagraph() bool {
|
func (b *atxHeadingParser) CanInterruptParagraph() bool {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ type withFilterTags struct {
|
||||||
value map[string]bool
|
value map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withFilterTags) SetConfig(c *Config) {
|
func (o *withFilterTags) SetParserOption(c *Config) {
|
||||||
c.Options[FilterTags] = o.value
|
c.Options[FilterTags] = o.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -375,12 +375,27 @@ func NewConfig() *Config {
|
||||||
|
|
||||||
// An Option interface is a functional option type for the Parser.
|
// An Option interface is a functional option type for the Parser.
|
||||||
type Option interface {
|
type Option interface {
|
||||||
SetConfig(*Config)
|
SetParserOption(*Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OptionName is a name of parser options.
|
// OptionName is a name of parser options.
|
||||||
type OptionName string
|
type OptionName string
|
||||||
|
|
||||||
|
// Attribute is an option name that spacify attributes of elements.
|
||||||
|
const Attribute OptionName = "Attribute"
|
||||||
|
|
||||||
|
type withAttribute struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *withAttribute) SetParserOption(c *Config) {
|
||||||
|
c.Options[Attribute] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAttribute is a functional option that enables custom attributes.
|
||||||
|
func WithAttribute() Option {
|
||||||
|
return &withAttribute{}
|
||||||
|
}
|
||||||
|
|
||||||
// A Parser interface parses Markdown text into AST nodes.
|
// A Parser interface parses Markdown text into AST nodes.
|
||||||
type Parser interface {
|
type Parser interface {
|
||||||
// Parse parses the given Markdown text into AST nodes.
|
// Parse parses the given Markdown text into AST nodes.
|
||||||
|
|
@ -552,7 +567,7 @@ type withBlockParsers struct {
|
||||||
value []util.PrioritizedValue
|
value []util.PrioritizedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withBlockParsers) SetConfig(c *Config) {
|
func (o *withBlockParsers) SetParserOption(c *Config) {
|
||||||
c.BlockParsers = append(c.BlockParsers, o.value...)
|
c.BlockParsers = append(c.BlockParsers, o.value...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -566,7 +581,7 @@ type withInlineParsers struct {
|
||||||
value []util.PrioritizedValue
|
value []util.PrioritizedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withInlineParsers) SetConfig(c *Config) {
|
func (o *withInlineParsers) SetParserOption(c *Config) {
|
||||||
c.InlineParsers = append(c.InlineParsers, o.value...)
|
c.InlineParsers = append(c.InlineParsers, o.value...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,7 +595,7 @@ type withParagraphTransformers struct {
|
||||||
value []util.PrioritizedValue
|
value []util.PrioritizedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withParagraphTransformers) SetConfig(c *Config) {
|
func (o *withParagraphTransformers) SetParserOption(c *Config) {
|
||||||
c.ParagraphTransformers = append(c.ParagraphTransformers, o.value...)
|
c.ParagraphTransformers = append(c.ParagraphTransformers, o.value...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -594,7 +609,7 @@ type withASTTransformers struct {
|
||||||
value []util.PrioritizedValue
|
value []util.PrioritizedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withASTTransformers) SetConfig(c *Config) {
|
func (o *withASTTransformers) SetParserOption(c *Config) {
|
||||||
c.ASTTransformers = append(c.ASTTransformers, o.value...)
|
c.ASTTransformers = append(c.ASTTransformers, o.value...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -609,7 +624,7 @@ type withOption struct {
|
||||||
value interface{}
|
value interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *withOption) SetConfig(c *Config) {
|
func (o *withOption) SetParserOption(c *Config) {
|
||||||
c.Options[o.name] = o.value
|
c.Options[o.name] = o.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -623,7 +638,7 @@ func WithOption(name OptionName, value interface{}) Option {
|
||||||
func NewParser(options ...Option) Parser {
|
func NewParser(options ...Option) Parser {
|
||||||
config := NewConfig()
|
config := NewConfig()
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
opt.SetConfig(config)
|
opt.SetParserOption(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &parser{
|
p := &parser{
|
||||||
|
|
@ -636,7 +651,7 @@ func NewParser(options ...Option) Parser {
|
||||||
|
|
||||||
func (p *parser) AddOptions(opts ...Option) {
|
func (p *parser) AddOptions(opts ...Option) {
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt.SetConfig(p.config)
|
opt.SetParserOption(p.config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,10 +94,27 @@ func (b *setextHeadingParser) Close(node ast.Node, reader text.Reader, pc Contex
|
||||||
tmp.Parent().RemoveChild(tmp.Parent(), tmp)
|
tmp.Parent().RemoveChild(tmp.Parent(), tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.AutoHeadingID {
|
if b.Attribute {
|
||||||
return
|
lastIndex := node.Lines().Len() - 1
|
||||||
|
lastLine := node.Lines().At(lastIndex)
|
||||||
|
line := lastLine.Value(reader.Source())
|
||||||
|
indicies := util.FindAttributeIndiciesReverse(line, true)
|
||||||
|
if indicies != nil {
|
||||||
|
for _, index := range indicies {
|
||||||
|
node.SetAttribute(line[index[0]:index[1]], line[index[2]:index[3]])
|
||||||
|
}
|
||||||
|
lastLine.Stop = lastLine.Start + indicies[0][0] - 1
|
||||||
|
lastLine.TrimRightSpace(reader.Source())
|
||||||
|
node.Lines().Set(lastIndex, lastLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.AutoHeadingID {
|
||||||
|
_, ok := node.AttributeString("id")
|
||||||
|
if !ok {
|
||||||
|
generateAutoHeadingID(heading, reader, pc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
generateAutoHeadingID(heading, reader, pc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *setextHeadingParser) CanInterruptParagraph() bool {
|
func (b *setextHeadingParser) CanInterruptParagraph() bool {
|
||||||
|
|
|
||||||
|
|
@ -206,12 +206,7 @@ func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node,
|
||||||
w.WriteString("<h")
|
w.WriteString("<h")
|
||||||
w.WriteByte("0123456"[n.Level])
|
w.WriteByte("0123456"[n.Level])
|
||||||
if n.Attributes() != nil {
|
if n.Attributes() != nil {
|
||||||
id, ok := n.Attribute(attrNameID)
|
r.RenderAttributes(w, node)
|
||||||
if ok {
|
|
||||||
w.WriteString(` id="`)
|
|
||||||
w.Write(id)
|
|
||||||
w.WriteByte('"')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.WriteByte('>')
|
w.WriteByte('>')
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -491,6 +486,17 @@ func (r *Renderer) renderText(w util.BufWriter, source []byte, node ast.Node, en
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderAttributes renders given node's attributes.
|
||||||
|
func (r *Renderer) RenderAttributes(w util.BufWriter, node ast.Node) {
|
||||||
|
for _, attr := range node.Attributes() {
|
||||||
|
w.WriteString(" ")
|
||||||
|
w.Write(attr.Name)
|
||||||
|
w.WriteString(`="`)
|
||||||
|
w.Write(attr.Value)
|
||||||
|
w.WriteByte('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A Writer interface wirtes textual contents to a writer.
|
// A Writer interface wirtes textual contents to a writer.
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
// Write writes the given source to writer with resolving references and unescaping
|
// Write writes the given source to writer with resolving references and unescaping
|
||||||
|
|
|
||||||
50
util/util.go
50
util/util.go
|
|
@ -54,6 +54,12 @@ func (b *CopyOnWriteBuffer) IsCopied() bool {
|
||||||
return b.copied
|
return b.copied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEscapedPunctuation returns true if caracter at a given index i
|
||||||
|
// is an escaped punctuation, otherwise false.
|
||||||
|
func IsEscapedPunctuation(source []byte, i int) bool {
|
||||||
|
return source[i] == '\\' && i < len(source)-1 && IsPunct(source[i+1])
|
||||||
|
}
|
||||||
|
|
||||||
// ReadWhile read the given source while pred is true.
|
// ReadWhile read the given source while pred is true.
|
||||||
func ReadWhile(source []byte, index [2]int, pred func(byte) bool) (int, bool) {
|
func ReadWhile(source []byte, index [2]int, pred func(byte) bool) (int, bool) {
|
||||||
j := index[0]
|
j := index[0]
|
||||||
|
|
@ -549,6 +555,46 @@ func URLEscape(v []byte, resolveReference bool) []byte {
|
||||||
return cob.Bytes()
|
return cob.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindAttributeIndiciesReverse searches attribute indicies from tail of the given
|
||||||
|
// bytes and returns indicies.
|
||||||
|
func FindAttributeIndiciesReverse(b []byte, canEscapeQuotes bool) [][4]int {
|
||||||
|
i := 0
|
||||||
|
retry:
|
||||||
|
var result [][4]int
|
||||||
|
as := -1
|
||||||
|
for i < len(b) {
|
||||||
|
if IsEscapedPunctuation(b, i) {
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b[i] == '{' {
|
||||||
|
i++
|
||||||
|
as = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if as < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for as < len(b) {
|
||||||
|
ai := FindAttributeIndex(b[as:], canEscapeQuotes)
|
||||||
|
if ai[0] < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i = as + ai[3]
|
||||||
|
if result == nil {
|
||||||
|
result = [][4]int{}
|
||||||
|
}
|
||||||
|
result = append(result, [4]int{as + ai[0], as + ai[1], as + ai[2], as + ai[3]})
|
||||||
|
as += ai[3]
|
||||||
|
}
|
||||||
|
if b[as] == '}' && (as > len(b)-2 || IsBlank(b[as:])) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
goto retry
|
||||||
|
}
|
||||||
|
|
||||||
// FindAttributeIndex searchs
|
// FindAttributeIndex searchs
|
||||||
// - #id
|
// - #id
|
||||||
// - .class
|
// - .class
|
||||||
|
|
@ -613,10 +659,10 @@ func FindHTMLAttributeIndex(b []byte, canEscapeQuotes bool) [4]int {
|
||||||
for ; i < l && IsSpace(b[i]); i++ {
|
for ; i < l && IsSpace(b[i]); i++ {
|
||||||
}
|
}
|
||||||
if i >= l {
|
if i >= l {
|
||||||
return result // empty attribute
|
return [4]int{-1, -1, -1, -1}
|
||||||
}
|
}
|
||||||
if b[i] != '=' {
|
if b[i] != '=' {
|
||||||
return result // empty attribute
|
return [4]int{-1, -1, -1, -1}
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
for ; i < l && IsSpace(b[i]); i++ {
|
for ; i < l && IsSpace(b[i]); i++ {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue