diff --git a/_test/options.txt b/_test/options.txt index e24e360..377b0d0 100644 --- a/_test/options.txt +++ b/_test/options.txt @@ -8,25 +8,25 @@ ## Title3 ## {#id_3 .class-3} -## Title4 ## {attr3=value3} +## Title4 ## {data-attr3=value3} -## Title5 ## {#id_5 attr5=value5} +## Title5 ## {#id_5 data-attr5=value5} -## Title6 ## {#id_6 .class6 attr6=value6} +## Title6 ## {#id_6 .class6 data-attr6=value6} -## Title7 ## {#id_7 attr7="value \"7"} +## Title7 ## {#id_7 data-attr7="value \"7"} -## Title8 {#id .className attrName=attrValue class="class1 class2"} +## Title8 {#id .className data-attrName=attrValue class="class1 class2"} //- - - - - - - - -//
\n") + if n.Attributes() != nil { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("\n") + } } else { _, _ = w.WriteString("\n") } @@ -278,6 +314,12 @@ func (r *Renderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Nod return ast.WalkContinue, nil } +// ListAttributeFilter defines attribute names which list elements can have. +var ListAttributeFilter = GlobalAttributeFilter.Extend( + []byte("start"), + []byte("reversed"), +) + func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.List) tag := "ul" @@ -288,10 +330,12 @@ func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, en _ = w.WriteByte('<') _, _ = w.WriteString(tag) if n.IsOrdered() && n.Start != 1 { - fmt.Fprintf(w, " start=\"%d\">\n", n.Start) - } else { - _, _ = w.WriteString(">\n") + fmt.Fprintf(w, " start=\"%d\"", n.Start) } + if n.Attributes() != nil { + RenderAttributes(w, n, ListAttributeFilter) + } + _, _ = w.WriteString(">\n") } else { _, _ = w.WriteString("") _, _ = w.WriteString(tag) @@ -300,9 +344,20 @@ func (r *Renderer) renderList(w util.BufWriter, source []byte, node ast.Node, en return ast.WalkContinue, nil } +// ListItemAttributeFilter defines attribute names which list item elements can have. +var ListItemAttributeFilter = GlobalAttributeFilter.Extend( + []byte("value"), +) + func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _, _ = w.WriteString("- ") + if n.Attributes() != nil { + _, _ = w.WriteString("
- ') + } else { + _, _ = w.WriteString("
- ") + } fc := n.FirstChild() if fc != nil { if _, ok := fc.(*ast.TextBlock); !ok { @@ -315,9 +370,18 @@ func (r *Renderer) renderListItem(w util.BufWriter, source []byte, n ast.Node, e return ast.WalkContinue, nil } +// ParagraphAttributeFilter defines attribute names which paragraph elements can have. +var ParagraphAttributeFilter = GlobalAttributeFilter + func (r *Renderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _, _ = w.WriteString("
") + if n.Attributes() != nil { + _, _ = w.WriteString("
') + } else { + _, _ = w.WriteString("
") + } } else { _, _ = w.WriteString("
\n") } @@ -333,18 +397,41 @@ func (r *Renderer) renderTextBlock(w util.BufWriter, source []byte, n ast.Node, return ast.WalkContinue, nil } +// ThematicAttributeFilter defines attribute names which hr elements can have. +var ThematicAttributeFilter = GlobalAttributeFilter.Extend( + []byte("align"), + []byte("color"), +) + func (r *Renderer) renderThematicBreak(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil } + _, _ = w.WriteString("
\n") + _, _ = w.WriteString(" />\n") } else { - _, _ = w.WriteString("
\n") + _, _ = w.WriteString(">\n") } return ast.WalkContinue, nil } +// LinkAttributeFilter defines attribute names which link elements can have. +var LinkAttributeFilter = GlobalAttributeFilter.Extend( + []byte("download"), + // []byte("href"), + []byte("hreflang"), + []byte("media"), + []byte("ping"), + []byte("referrerpolicy"), + []byte("rel"), + []byte("shape"), + []byte("target"), +) + func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.AutoLink) if !entering { @@ -357,15 +444,30 @@ func (r *Renderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node _, _ = w.WriteString("mailto:") } _, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false))) - _, _ = w.WriteString(`">`) + if n.Attributes() != nil { + _ = w.WriteByte('"') + RenderAttributes(w, n, LinkAttributeFilter) + _ = w.WriteByte('>') + } else { + _, _ = w.WriteString(`">`) + } _, _ = w.Write(util.EscapeHTML(label)) _, _ = w.WriteString(``) return ast.WalkContinue, nil } +// CodeAttributeFilter defines attribute names which code elements can have. +var CodeAttributeFilter = GlobalAttributeFilter + func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _, _ = w.WriteString("") + if n.Attributes() != nil { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("") + } for c := n.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*ast.Text).Segment value := segment.Value(source) @@ -384,6 +486,9 @@ func (r *Renderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, e return ast.WalkContinue, nil } +// EmphasisAttributeFilter defines attribute names which emphasis elements can have. +var EmphasisAttributeFilter = GlobalAttributeFilter + func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Emphasis) tag := "em" @@ -393,6 +498,9 @@ func (r *Renderer) renderEmphasis(w util.BufWriter, source []byte, node ast.Node if entering { _ = w.WriteByte('<') _, _ = w.WriteString(tag) + if n.Attributes() != nil { + RenderAttributes(w, n, EmphasisAttributeFilter) + } _ = w.WriteByte('>') } else { _, _ = w.WriteString("") @@ -415,12 +523,34 @@ func (r *Renderer) renderLink(w util.BufWriter, source []byte, node ast.Node, en r.Writer.Write(w, n.Title) _ = w.WriteByte('"') } + if n.Attributes() != nil { + RenderAttributes(w, n, LinkAttributeFilter) + } _ = w.WriteByte('>') } else { _, _ = w.WriteString("") } return ast.WalkContinue, nil } + +// ImageAttributeFilter defines attribute names which image elements can have. +var ImageAttributeFilter = GlobalAttributeFilter.Extend( + []byte("align"), + []byte("border"), + []byte("crossorigin"), + []byte("decoding"), + []byte("height"), + []byte("importance"), + []byte("intrinsicsize"), + []byte("ismap"), + []byte("loading"), + []byte("referrerpolicy"), + []byte("sizes"), + []byte("srcset"), + []byte("usemap"), + []byte("width"), +) + func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if !entering { return ast.WalkContinue, nil @@ -438,6 +568,9 @@ func (r *Renderer) renderImage(w util.BufWriter, source []byte, node ast.Node, e r.Writer.Write(w, n.Title) _ = w.WriteByte('"') } + if n.Attributes() != nil { + RenderAttributes(w, n, ImageAttributeFilter) + } if r.XHTML { _, _ = w.WriteString(" />") } else { @@ -503,13 +636,22 @@ func (r *Renderer) renderString(w util.BufWriter, source []byte, node ast.Node, return ast.WalkContinue, nil } -// RenderAttributes renders given node's attributes. -func (r *Renderer) RenderAttributes(w util.BufWriter, node ast.Node) { +var dataPrefix = []byte("data-") +// RenderAttributes renders given node's attributes. +// You can specify attribute names to render by the filter. +// If filter is nil, RenderAttributes renders all attributes. +func RenderAttributes(w util.BufWriter, node ast.Node, filter util.BytesFilter) { for _, attr := range node.Attributes() { + if filter != nil && !filter.Contains(attr.Name) { + if !bytes.HasPrefix(attr.Name, dataPrefix) { + continue + } + } _, _ = w.WriteString(" ") _, _ = w.Write(attr.Name) _, _ = w.WriteString(`="`) + // TODO: convert numeric values to strings _, _ = w.Write(util.EscapeHTML(attr.Value.([]byte))) _ = w.WriteByte('"') } diff --git a/util/util.go b/util/util.go index 74f991b..00cdbbb 100644 --- a/util/util.go +++ b/util/util.go @@ -798,3 +798,97 @@ func (s PrioritizedSlice) Remove(v interface{}) PrioritizedSlice { func Prioritized(v interface{}, priority int) PrioritizedValue { return PrioritizedValue{v, priority} } + +func bytesHash(b []byte) uint64 { + var hash uint64 = 5381 + for _, c := range b { + hash = ((hash << 5) + hash) + uint64(c) + } + return hash +} + +// BytesFilter is a efficient data structure for checking whether bytes exist or not. +// BytesFilter is thread-safe. +type BytesFilter interface { + // Add adds given bytes to this set. + Add([]byte) + + // Contains return true if this set contains given bytes, otherwise false. + Contains([]byte) bool + + // Extend copies this filter and adds given bytes to new filter. + Extend(...[]byte) BytesFilter +} + +type bytesFilter struct { + chars [256]uint8 + threshold int + slots [][][]byte +} + +func NewBytesFilter(elements ...[]byte) BytesFilter { + s := &bytesFilter{ + threshold: 3, + slots: make([][][]byte, 64), + } + for _, element := range elements { + s.Add(element) + } + return s +} + +func (s *bytesFilter) Add(b []byte) { + l := len(b) + m := s.threshold + if l < s.threshold { + m = l + } + for i := 0; i < m; i++ { + s.chars[b[i]] |= 1 << i + } + h := bytesHash(b) % uint64(len(s.slots)) + slot := s.slots[h] + if slot == nil { + slot = [][]byte{} + } + s.slots[h] = append(slot, b) +} + +func (s *bytesFilter) Extend(bs ...[]byte) BytesFilter { + newFilter := NewBytesFilter().(*bytesFilter) + newFilter.chars = s.chars + newFilter.threshold = s.threshold + for k, v := range s.slots { + newSlot := make([][]byte, len(v)) + copy(newSlot, v) + newFilter.slots[k] = v + } + for _, b := range bs { + newFilter.Add(b) + } + return newFilter +} + +func (s *bytesFilter) Contains(b []byte) bool { + l := len(b) + m := s.threshold + if l < s.threshold { + m = l + } + for i := 0; i < m; i++ { + if (s.chars[b[i]] & (1 << i)) == 0 { + return false + } + } + h := bytesHash(b) % uint64(len(s.slots)) + slot := s.slots[h] + if slot == nil || len(slot) == 0 { + return false + } + for _, element := range slot { + if bytes.Equal(element, b) { + return true + } + } + return false +}