From 7d8bee11ca7d409078c07282281315a05ef6540d Mon Sep 17 00:00:00 2001 From: yuin Date: Sun, 8 Dec 2019 18:53:01 +0900 Subject: [PATCH] Closes #33 : Now NodeRenderers render attributes --- _test/options.txt | 20 ++--- extension/definition_list.go | 33 ++++++- extension/footnote.go | 13 ++- extension/strikethrough.go | 13 ++- renderer/html/html.go | 168 ++++++++++++++++++++++++++++++++--- util/util.go | 94 ++++++++++++++++++++ 6 files changed, 309 insertions(+), 32 deletions(-) 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"} //- - - - - - - - -//

Title 0

Title1

Title2

Title3

-

Title4

-

Title5

-

Title6

-

Title7

-

Title8

+

Title4

+

Title5

+

Title6

+

Title7

+

Title8

//= = = = = = = = = = = = = = = = = = = = = = = =// 2 diff --git a/extension/definition_list.go b/extension/definition_list.go index 3682b58..eb16dd0 100644 --- a/extension/definition_list.go +++ b/extension/definition_list.go @@ -196,31 +196,56 @@ func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFunc reg.Register(ast.KindDefinitionDescription, r.renderDefinitionDescription) } +// DefinitionListAttributeFilter defines attribute names which dl elements can have. +var DefinitionListAttributeFilter = html.GlobalAttributeFilter + func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { if entering { - _, _ = w.WriteString("
\n") + if n.Attributes() != nil { + _, _ = w.WriteString("\n") + } else { + _, _ = w.WriteString("
\n") + } } else { _, _ = w.WriteString("
\n") } return gast.WalkContinue, nil } +// DefinitionTermAttributeFilter defines attribute names which dd elements can have. +var DefinitionTermAttributeFilter = html.GlobalAttributeFilter + func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { if entering { - _, _ = w.WriteString("
") + if n.Attributes() != nil { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("
") + } } else { _, _ = w.WriteString("
\n") } return gast.WalkContinue, nil } +// DefinitionDescriptionAttributeFilter defines attribute names which dd elements can have. +var DefinitionDescriptionAttributeFilter = html.GlobalAttributeFilter + func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { if entering { n := node.(*ast.DefinitionDescription) + _, _ = w.WriteString("") + _, _ = w.WriteString(">") } else { - _, _ = w.WriteString("
\n") + _, _ = w.WriteString(">\n") } } else { _, _ = w.WriteString("
\n") diff --git a/extension/footnote.go b/extension/footnote.go index 31efddc..592d7f4 100644 --- a/extension/footnote.go +++ b/extension/footnote.go @@ -268,8 +268,11 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n if entering { _, _ = w.WriteString(`
  • `) - _, _ = w.WriteString("\n") + _, _ = w.WriteString(`" role="doc-endnote"`) + if node.Attributes() != nil { + html.RenderAttributes(w, node, html.ListItemAttributeFilter) + } + _, _ = w.WriteString(">\n") } else { _, _ = w.WriteString("
  • \n") } @@ -284,7 +287,11 @@ func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byt if entering { _, _ = w.WriteString("<") _, _ = w.WriteString(tag) - _, _ = w.WriteString(` class="footnotes" role="doc-endnotes">`) + _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) + if node.Attributes() != nil { + html.RenderAttributes(w, node, html.GlobalAttributeFilter) + } + _ = w.WriteByte('>') if r.Config.XHTML { _, _ = w.WriteString("\n
    \n") } else { diff --git a/extension/strikethrough.go b/extension/strikethrough.go index cd7584b..1b629ad 100644 --- a/extension/strikethrough.go +++ b/extension/strikethrough.go @@ -82,11 +82,20 @@ func (r *StrikethroughHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncR reg.Register(ast.KindStrikethrough, r.renderStrikethrough) } +// StrikethroughAttributeFilter defines attribute names which dd elements can have. +var StrikethroughAttributeFilter = html.GlobalAttributeFilter + func (r *StrikethroughHTMLRenderer) renderStrikethrough(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) { if entering { - w.WriteString("") + if n.Attributes() != nil { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("") + } } else { - w.WriteString("") + _, _ = w.WriteString("") } return gast.WalkContinue, nil } diff --git a/renderer/html/html.go b/renderer/html/html.go index 3019511..b2ca75b 100644 --- a/renderer/html/html.go +++ b/renderer/html/html.go @@ -194,18 +194,43 @@ func (r *Renderer) writeLines(w util.BufWriter, source []byte, n ast.Node) { } } +// GlobalAttributeFilter defines attribute names which any elements can have. +var GlobalAttributeFilter = util.NewBytesFilter( + []byte("accesskey"), + []byte("autocapitalize"), + []byte("class"), + []byte("contenteditable"), + []byte("contextmenu"), + []byte("dir"), + []byte("draggable"), + []byte("dropzone"), + []byte("hidden"), + []byte("id"), + []byte("itemprop"), + []byte("lang"), + []byte("slot"), + []byte("spellcheck"), + []byte("style"), + []byte("tabindex"), + []byte("title"), + []byte("translate"), +) + func (r *Renderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { // nothing to do return ast.WalkContinue, nil } +// HeadingAttributeFilter defines attribute names which heading elements can have +var HeadingAttributeFilter = GlobalAttributeFilter + func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*ast.Heading) if entering { _, _ = w.WriteString("') } else { @@ -216,9 +241,20 @@ func (r *Renderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, return ast.WalkContinue, nil } +// BlockquoteAttributeFilter defines attribute names which blockquote elements can have +var BlockquoteAttributeFilter = GlobalAttributeFilter.Extend( + []byte("cite"), +) + func (r *Renderer) renderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _, _ = w.WriteString("
    \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("") + 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("') } 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 +}