aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <m.shulhan@gmail.com>2020-12-02 01:56:45 +0700
committerShulhan <m.shulhan@gmail.com>2020-12-03 03:50:04 +0700
commite80d5e10ee50abb56d5af3f1adefcc5262f56cd9 (patch)
tree7726e347133fbcb15b97d48e013102a7831cba2b
parentbe31f67526193a1a575ebe03be4612ac42e981b4 (diff)
downloadasciidoctor-go-e80d5e10ee50abb56d5af3f1adefcc5262f56cd9.tar.xz
all: unified the document node attributes
-rw-r--r--adoc_node.go171
-rw-r--r--adoc_table.go10
-rw-r--r--document_parser.go137
-rw-r--r--element_attribute.go206
-rw-r--r--element_attribute_test.go75
-rw-r--r--html_backend.go75
-rw-r--r--parser.go120
-rw-r--r--parser_inline.go79
-rw-r--r--parser_test.go51
9 files changed, 496 insertions, 428 deletions
diff --git a/adoc_node.go b/adoc_node.go
index f8e5268..821fae6 100644
--- a/adoc_node.go
+++ b/adoc_node.go
@@ -18,17 +18,14 @@ import (
// adocNode is the building block of asciidoc document.
//
type adocNode struct {
- ID string
- Attrs map[string]string
- Opts map[string]string
+ elementAttribute
+
Text string // The content of node without inline formatting.
kind int
- level int // The number of dot for ordered list, or star '*' for unordered list.
+ level int // The number of dot for ordered list, or '*' for unordered list.
raw []byte // unparsed content of node.
rawLabel bytes.Buffer
rawTitle string
- style int64
- classes attributeClass
// The key and value for attribute (lineKindAttribute).
key string
@@ -43,21 +40,13 @@ type adocNode struct {
// It will be set only if attribute "sectnums" is on.
sectnums *sectionCounters
- table *adocTable
-
+ table *adocTable
parent *adocNode
child *adocNode
next *adocNode
prev *adocNode
}
-func (node *adocNode) Classes() string {
- if len(node.classes) == 0 {
- return ""
- }
- return node.classes.String()
-}
-
func (node *adocNode) Content() string {
node.raw = bytes.TrimRight(node.raw, "\n")
return string(node.raw)
@@ -96,16 +85,23 @@ func (node *adocNode) getListOrderedType() string {
//
func (node *adocNode) GetVideoSource() string {
var (
- u = new(url.URL)
- q []string
- fragment string
+ u = new(url.URL)
+ q []string
+ fragment string
+ isYoutube bool
+ isVimeo bool
)
+ if node.rawStyle == attrNameYoutube {
+ isYoutube = true
+ }
+ if node.rawStyle == attrNameVimeo {
+ isVimeo = true
+ }
+
src := node.Attrs[attrNameSrc]
- opts := strings.Split(node.Attrs[attrNameOptions], ",")
- _, ok := node.Attrs[attrNameYoutube]
- if ok {
+ if isYoutube {
u.Scheme = "https"
u.Host = "www.youtube.com"
u.Path = "/embed/" + src
@@ -120,8 +116,7 @@ func (node *adocNode) GetVideoSource() string {
if ok {
q = append(q, attrNameEnd+"="+v)
}
- for _, opt := range opts {
- opt = strings.TrimSpace(opt)
+ for _, opt := range node.options {
switch opt {
case optNameAutoplay, optNameLoop:
q = append(q, opt+"=1")
@@ -144,13 +139,12 @@ func (node *adocNode) GetVideoSource() string {
q = append(q, attrNameYoutubeLang+"="+v)
}
- } else if _, ok = node.Attrs[attrNameVimeo]; ok {
+ } else if isVimeo {
u.Scheme = "https"
u.Host = "player.vimeo.com"
u.Path = "/video/" + src
- for _, opt := range opts {
- opt = strings.TrimSpace(opt)
+ for _, opt := range node.options {
switch opt {
case optNameAutoplay, optNameLoop:
q = append(q, opt+"=1")
@@ -161,8 +155,7 @@ func (node *adocNode) GetVideoSource() string {
fragment = "at=" + v
}
} else {
- for _, opt := range opts {
- opt = strings.TrimSpace(opt)
+ for _, opt := range node.options {
switch opt {
case optNameAutoplay, optNameLoop:
node.Attrs[optNameNocontrols] = ""
@@ -268,33 +261,6 @@ func (node *adocNode) addChild(child *adocNode) {
}
}
-func (node *adocNode) addOptions(opts []string) {
- if len(opts) == 0 {
- return
- }
- if node.Opts == nil {
- node.Opts = make(map[string]string)
- }
- for _, opt := range opts {
- kv := strings.Split(opt, "=")
- if len(kv) == 0 {
- continue
- }
- key := strings.TrimSpace(kv[0])
- if len(kv) == 1 {
- node.Opts[key] = ""
- } else {
- val := strings.TrimSpace(kv[1])
-
- if key == attrNameOptions {
- node.Opts[val] = ""
- } else {
- node.Opts[key] = val
- }
- }
- }
-}
-
// backTrimSpace remove trailing white spaces on raw field.
func (node *adocNode) backTrimSpace() {
x := len(node.raw) - 1
@@ -345,41 +311,14 @@ func (node *adocNode) parseBlockAudio(doc *Document, line string) bool {
}
src := strings.TrimRight(line[:attrBegin], " \t")
- _, _, attrs := parseAttributeElement(line[attrBegin : attrEnd+1])
- node.Attrs = make(map[string]string, len(attrs)+1)
+ if node.Attrs == nil {
+ node.Attrs = make(map[string]string)
+ }
+ node.parseElementAttribute(line[attrBegin : attrEnd+1])
src = string(applySubstitutions(doc, []byte(src)))
node.Attrs[attrNameSrc] = src
- var key, val string
- for _, attr := range attrs {
- kv := strings.Split(attr, "=")
-
- key = strings.ToLower(kv[0])
- if len(kv) >= 2 {
- val = kv[1]
- } else {
- val = "1"
- }
-
- if key == attrNameOptions {
- node.Attrs[key] = val
- opts := strings.Split(val, ",")
- node.Opts = make(map[string]string, len(opts))
- node.Opts[optNameControls] = "1"
-
- for _, opt := range opts {
- switch opt {
- case optNameNocontrols:
- node.Opts[optNameControls] = "0"
- case optNameControls:
- node.Opts[optNameControls] = "1"
- default:
- node.Opts[opt] = "1"
- }
- }
- }
- }
return true
}
@@ -445,7 +384,7 @@ func (node *adocNode) parseBlockImage(doc *Document, line string) bool {
if val == "center" {
val = "text-center"
}
- node.classes.add(val)
+ node.addRole(val)
default:
node.Attrs[key] = val
}
@@ -483,7 +422,7 @@ func (node *adocNode) parseInlineMarkup(doc *Document, kind int) {
func (node *adocNode) parseLineAdmonition(line string) {
sep := strings.IndexByte(line, ':')
class := strings.ToLower(line[:sep])
- node.classes.add(class)
+ node.addRole(class)
node.rawLabel.WriteString(strings.Title(class))
line = strings.TrimSpace(line[sep+1:])
node.WriteString(line)
@@ -630,7 +569,7 @@ func (node *adocNode) parseStyleClass(line string) {
for _, class := range parts {
class = strings.TrimSpace(class)
if len(class) > 0 {
- node.classes.add(class)
+ node.addRole(class)
}
}
}
@@ -645,39 +584,16 @@ func (node *adocNode) parseBlockVideo(doc *Document, line string) bool {
return false
}
- videoSrc := strings.TrimRight(line[:attrBegin], " \t")
- key, _, attrs := parseAttributeElement(line[attrBegin : attrEnd+1])
-
if node.Attrs == nil {
- node.Attrs = make(map[string]string, len(attrs)+1)
+ node.Attrs = make(map[string]string)
}
+
+ videoSrc := strings.TrimRight(line[:attrBegin], " \t")
videoSrc = string(applySubstitutions(doc, []byte(videoSrc)))
node.Attrs[attrNameSrc] = videoSrc
- start := 0
- if key == attrNameYoutube || key == attrNameVimeo {
- node.Attrs[key] = ""
- start = 1
- }
-
- var val string
- for _, attr := range attrs[start:] {
- kv := strings.Split(attr, "=")
-
- key = strings.ToLower(kv[0])
- if len(kv) >= 2 {
- val = kv[1]
- } else {
- val = ""
- }
+ node.parseElementAttribute(line[attrBegin : attrEnd+1])
- switch key {
- case attrNameWidth, attrNameHeight,
- attrNameOptions, attrNamePoster, attrNameStart,
- attrNameEnd, attrNameTheme, attrNameLang:
- node.Attrs[key] = val
- }
- }
return true
}
@@ -762,7 +678,15 @@ func (node *adocNode) postParseParagraphAsQuote(lines [][]byte) bool {
node.kind = nodeKindBlockExcerpts
opts := strings.SplitN(string(lastLine[3:]), `,`, 2)
- node.setQuoteOpts(opts)
+ if node.Attrs == nil {
+ node.Attrs = make(map[string]string)
+ }
+ if len(opts) >= 1 {
+ node.Attrs[attrNameAttribution] = strings.TrimSpace(opts[0])
+ }
+ if len(opts) >= 2 {
+ node.Attrs[attrNameCitation] = strings.TrimSpace(opts[1])
+ }
return true
}
@@ -772,7 +696,7 @@ func (node *adocNode) postParseParagraphAsQuote(lines [][]byte) bool {
// multiple rows, based on empty line between row.
//
func (node *adocNode) postConsumeTable() (table *adocTable) {
- node.table = newTable(node.Attrs, node.Opts, node.raw)
+ node.table = newTable(node.Attrs, node.options, node.raw)
return node.table
}
@@ -802,18 +726,9 @@ func (node *adocNode) removeLastIfEmpty() {
c.parent = nil
}
-func (node *adocNode) setQuoteOpts(opts []string) {
- if len(opts) >= 1 {
- node.key = strings.TrimSpace(opts[0])
- }
- if len(opts) >= 2 {
- node.value = strings.TrimSpace(opts[1])
- }
-}
-
func (node *adocNode) setStyleAdmonition(admName string) {
admName = strings.ToLower(admName)
- node.classes.add(admName)
+ node.addRole(admName)
node.rawLabel.WriteString(strings.Title(admName))
}
diff --git a/adoc_table.go b/adoc_table.go
index 68a4c83..d44a8d6 100644
--- a/adoc_table.go
+++ b/adoc_table.go
@@ -9,6 +9,7 @@ import (
"strings"
"github.com/shuLhan/share/lib/math/big"
+ libstrings "github.com/shuLhan/share/lib/strings"
)
type adocTable struct {
@@ -19,7 +20,7 @@ type adocTable struct {
hasFooter bool
}
-func newTable(attrs, opts map[string]string, content []byte) (table *adocTable) {
+func newTable(attrs map[string]string, opts []string, content []byte) (table *adocTable) {
var (
row *tableRow
)
@@ -44,8 +45,7 @@ func newTable(attrs, opts map[string]string, content []byte) (table *adocTable)
row = pt.row(table.ncols)
}
if pt.nrow == 1 && !row.cells[0].endWithLF() {
- _, ok := opts[attrValueNoHeader]
- if !ok {
+ if !libstrings.IsContain(opts, attrValueNoHeader) {
table.hasHeader = true
}
}
@@ -91,11 +91,11 @@ func (table *adocTable) initializeFormats() {
}
}
-func (table *adocTable) parseOptions(opts map[string]string) {
+func (table *adocTable) parseOptions(opts []string) {
if opts == nil {
return
}
- for key := range opts {
+ for _, key := range opts {
switch key {
case attrValueHeader:
table.hasHeader = true
diff --git a/document_parser.go b/document_parser.go
index 310b018..9fc0d48 100644
--- a/document_parser.go
+++ b/document_parser.go
@@ -42,8 +42,10 @@ func Parse(content []byte) (doc *Document) {
}
preamble := &adocNode{
- kind: nodeKindPreamble,
- Attrs: make(map[string]string),
+ elementAttribute: elementAttribute{
+ Attrs: make(map[string]string),
+ },
+ kind: nodeKindPreamble,
}
doc.content.addChild(preamble)
@@ -222,39 +224,12 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
continue
case lineKindAttributeElement:
- key, val, opts := parseAttributeElement(line)
-
- styleKind := parseStyle(key)
- if styleKind > 0 {
- node.style |= styleKind
- if isStyleAdmonition(styleKind) {
- node.setStyleAdmonition(key)
- } else if isStyleQuote(styleKind) {
- node.setQuoteOpts(opts[1:])
- } else if isStyleVerse(styleKind) {
- node.setQuoteOpts(opts[1:])
- }
- line = ""
- continue
- }
- if key == attrNameRefText {
- if node.Attrs == nil {
- node.Attrs = make(map[string]string)
+ node.parseElementAttribute(line)
+ if node.style > 0 {
+ if isStyleAdmonition(node.style) {
+ node.setStyleAdmonition(node.rawStyle)
}
- node.Attrs[key] = val
- line = ""
- continue
- }
- if key == attrNameCols {
- if node.Attrs == nil {
- node.Attrs = make(map[string]string)
- }
- node.Attrs[key] = val
- node.addOptions(opts)
- line = ""
- continue
}
- node.addOptions(opts)
line = ""
continue
@@ -340,7 +315,7 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
spaces+line, term)
} else {
node.kind = docp.kind
- node.classes.add(classNameLiteralBlock)
+ node.addRole(classNameLiteralBlock)
node.WriteString(line)
node.WriteByte('\n')
line = docp.consumeLinesUntil(
@@ -361,7 +336,7 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
case nodeKindBlockLiteral:
node.kind = docp.kind
- node.classes.add(classNameLiteralBlock)
+ node.addRole(classNameLiteralBlock)
line = docp.consumeLinesUntil(node, docp.kind, nil)
node.raw = applySubstitutions(docp.doc, node.raw)
parent.addChild(node)
@@ -370,7 +345,7 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
case nodeKindBlockLiteralNamed:
node.kind = docp.kind
- node.classes.add(classNameLiteralBlock)
+ node.addRole(classNameLiteralBlock)
line = docp.consumeLinesUntil(node, lineKindEmpty, nil)
node.raw = applySubstitutions(docp.doc, node.raw)
parent.addChild(node)
@@ -379,7 +354,7 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
case nodeKindBlockListing:
node.kind = docp.kind
- node.classes.add(classNameListingBlock)
+ node.addRole(classNameListingBlock)
line = docp.consumeLinesUntil(node, docp.kind, nil)
node.raw = applySubstitutions(docp.doc, node.raw)
parent.addChild(node)
@@ -388,7 +363,7 @@ func (docp *documentParser) parseBlock(parent *adocNode, term int) {
case nodeKindBlockListingNamed:
node.kind = docp.kind
- node.classes.add(classNameListingBlock)
+ node.addRole(classNameListingBlock)
line = docp.consumeLinesUntil(
node,
lineKindEmpty,
@@ -606,8 +581,10 @@ func (docp *documentParser) parseListBlock() (node *adocNode, line string) {
if docp.kind == lineKindAdmonition {
node = &adocNode{
- kind: nodeKindParagraph,
- style: styleAdmonition,
+ elementAttribute: elementAttribute{
+ style: styleAdmonition,
+ },
+ kind: nodeKindParagraph,
}
node.parseLineAdmonition(line)
line = docp.consumeLinesUntil(
@@ -638,8 +615,10 @@ func (docp *documentParser) parseListBlock() (node *adocNode, line string) {
}
if docp.kind == nodeKindLiteralParagraph {
node = &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
node.WriteString(strings.TrimLeft(line, " \t"))
node.WriteByte('\n')
@@ -674,8 +653,10 @@ func (docp *documentParser) parseListBlock() (node *adocNode, line string) {
}
if docp.kind == nodeKindBlockListing {
node = &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameListingBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameListingBlock},
+ },
+ kind: docp.kind,
}
docp.consumeLinesUntil(node, docp.kind, nil)
node.raw = applySubstitutions(docp.doc, node.raw)
@@ -699,13 +680,17 @@ func (docp *documentParser) parseListDescription(parent, node *adocNode, line st
got string,
) {
list := &adocNode{
+ elementAttribute: elementAttribute{
+ style: node.style,
+ },
kind: nodeKindListDescription,
rawTitle: node.rawTitle,
- style: node.style,
}
listItem := &adocNode{
- kind: nodeKindListDescriptionItem,
- style: list.style,
+ elementAttribute: elementAttribute{
+ style: list.style,
+ },
+ kind: nodeKindListDescriptionItem,
}
listItem.parseListDescriptionItem(line)
list.level = listItem.level
@@ -754,8 +739,10 @@ func (docp *documentParser) parseListDescription(parent, node *adocNode, line st
}
if docp.kind == nodeKindListDescriptionItem {
node := &adocNode{
- kind: nodeKindListDescriptionItem,
- style: list.style,
+ elementAttribute: elementAttribute{
+ style: list.style,
+ },
+ kind: nodeKindListDescriptionItem,
}
node.parseListDescriptionItem(line)
if listItem.level == node.level {
@@ -782,8 +769,10 @@ func (docp *documentParser) parseListDescription(parent, node *adocNode, line st
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameListingBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameListingBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
@@ -800,8 +789,10 @@ func (docp *documentParser) parseListDescription(parent, node *adocNode, line st
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
@@ -968,8 +959,10 @@ func (docp *documentParser) parseListOrdered(parent *adocNode, title, line strin
if docp.kind == nodeKindLiteralParagraph {
if docp.prevKind == lineKindEmpty {
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
node.WriteString(strings.TrimLeft(line, " \t"))
node.WriteByte('\n')
@@ -991,8 +984,10 @@ func (docp *documentParser) parseListOrdered(parent *adocNode, title, line strin
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameListingBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameListingBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
@@ -1009,8 +1004,10 @@ func (docp *documentParser) parseListOrdered(parent *adocNode, title, line strin
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
@@ -1055,8 +1052,10 @@ func (docp *documentParser) parseListUnordered(parent, node *adocNode, line stri
got string,
) {
list := &adocNode{
+ elementAttribute: elementAttribute{
+ roles: []string{classNameUlist},
+ },
kind: nodeKindListUnordered,
- classes: attributeClass{classNameUlist},
rawTitle: node.rawTitle,
}
listItem := &adocNode{
@@ -1176,8 +1175,10 @@ func (docp *documentParser) parseListUnordered(parent, node *adocNode, line stri
if docp.kind == nodeKindLiteralParagraph {
if docp.prevKind == lineKindEmpty {
node = &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
node.WriteString(strings.TrimLeft(line, " \t"))
node.WriteByte('\n')
@@ -1199,8 +1200,10 @@ func (docp *documentParser) parseListUnordered(parent, node *adocNode, line stri
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameListingBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameListingBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
@@ -1217,8 +1220,10 @@ func (docp *documentParser) parseListUnordered(parent, node *adocNode, line stri
break
}
node := &adocNode{
- kind: docp.kind,
- classes: attributeClass{classNameLiteralBlock},
+ elementAttribute: elementAttribute{
+ roles: []string{classNameLiteralBlock},
+ },
+ kind: docp.kind,
}
line = docp.consumeLinesUntil(node,
lineKindEmpty,
diff --git a/element_attribute.go b/element_attribute.go
new file mode 100644
index 0000000..31525b6
--- /dev/null
+++ b/element_attribute.go
@@ -0,0 +1,206 @@
+// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package asciidoctor
+
+import (
+ "bytes"
+ "strings"
+
+ libstrings "github.com/shuLhan/share/lib/strings"
+)
+
+type elementAttribute struct {
+ ID string
+ Attrs map[string]string
+ style int64
+ rawStyle string
+ roles []string
+ options []string
+ pos int
+}
+
+func (ea *elementAttribute) addRole(role string) {
+ ea.roles = libstrings.AppendUniq(ea.roles, role)
+}
+
+func (ea *elementAttribute) htmlClasses() string {
+ if len(ea.roles) == 0 {
+ return ""
+ }
+ return strings.Join(ea.roles, " ")
+}
+
+//
+// parseElementAttribute parse list of attributes in between "[" "]".
+//
+// BLOCK_ATTRS = BLOCK_ATTR *("," BLOCK_ATTR)
+//
+// BLOCK_ATTR = ATTR_NAME ( "=" (DQUOTE) ATTR_VALUE (DQUOTE) )
+//
+// ATTR_NAME = WORD
+//
+// ATTR_VALUE = STRING
+//
+// The attribute may not have a value.
+//
+// If the attribute value contains space or comma, it must be wrapped with
+// double quote.
+// The double quote on value will be removed when stored on options.
+//
+func (ea *elementAttribute) parseElementAttribute(raw string) {
+ raw = strings.TrimSpace(raw)
+ if len(raw) == 0 {
+ return
+ }
+ if raw[0] != '[' {
+ return
+ }
+ if raw[len(raw)-1] != ']' {
+ return
+ }
+ raw = raw[1 : len(raw)-1]
+ if len(raw) == 0 {
+ return
+ }
+ var (
+ buf []byte
+ prevc byte
+ c = raw[0]
+ x int
+ )
+ if c == '#' || c == '.' || c == '%' {
+ prevc = c
+ x = 1
+ }
+ for ; x < len(raw); x++ {
+ c = raw[x]
+ switch c {
+ case '"':
+ isEscaped := false
+ x++
+ for ; x < len(raw); x++ {
+ if raw[x] == '\\' {
+ if isEscaped {
+ buf = append(buf, '\\')
+ isEscaped = false
+ } else {
+ isEscaped = true
+ }
+ continue
+ }
+ if raw[x] == '"' {
+ break
+ }
+ buf = append(buf, raw[x])
+ }
+ case ',':
+ str := string(bytes.TrimSpace(buf))
+ ea.setByPreviousChar(prevc, str)
+ ea.pos++
+ buf = buf[:0]
+ prevc = c
+ case '#', '%':
+ ea.setByPreviousChar(prevc, string(bytes.TrimSpace(buf)))
+ buf = buf[:0]
+ prevc = c
+ case '.':
+ if ea.style == styleQuote || ea.style == styleVerse {
+ // Make the '.' as part of attribution.
+ if prevc == ',' {
+ buf = append(buf, c)
+ continue
+ }
+ } else if ea.style == styleLink && ea.pos == 0 {
+ buf = append(buf, c)
+ continue
+ }
+ ea.setByPreviousChar(prevc, string(bytes.TrimSpace(buf)))
+ buf = buf[:0]
+ prevc = c
+ default:
+ buf = append(buf, c)
+ }
+ }
+ if len(buf) > 0 {
+ ea.setByPreviousChar(prevc, string(bytes.TrimSpace(buf)))
+ }
+}
+
+func (ea *elementAttribute) parseNamedValue(prevc byte, str string) {
+ kv := strings.Split(str, "=")
+ key := kv[0]
+ val := strings.TrimSpace(kv[1])
+ if val[0] == '"' {
+ val = val[1:]
+ }
+ if val[len(val)-1] == '"' {
+ val = val[:len(val)-1]
+ }
+
+ rawvals := strings.Split(val, ",")
+ vals := make([]string, 0, len(rawvals))
+ for _, v := range rawvals {
+ v = strings.TrimSpace(v)
+ if len(v) == 0 {
+ continue
+ }
+ vals = append(vals, v)
+ }
+
+ switch key {
+ case attrNameOptions, attrNameOpts:
+ ea.options = append(ea.options, vals...)
+ case attrNameRole:
+ ea.roles = append(ea.roles, vals...)
+ default:
+ if ea.Attrs == nil {
+ ea.Attrs = make(map[string]string)
+ }
+ ea.Attrs[key] = val
+ }
+}
+
+func (ea *elementAttribute) setByPreviousChar(prevc byte, str string) {
+ switch prevc {
+ case 0:
+ if strings.IndexByte(str, '=') > 0 {
+ ea.parseNamedValue(prevc, str)
+ } else {
+ ea.rawStyle = str
+ ea.style = parseStyle(str)
+ }
+ case '#':
+ ea.ID = str
+ case '.':
+ ea.addRole(str)
+ case '%':
+ ea.options = append(ea.options, str)
+ case ',':
+ if strings.IndexByte(str, '=') > 0 {
+ ea.parseNamedValue(prevc, str)
+ } else {
+ if ea.Attrs == nil {
+ ea.Attrs = make(map[string]string)
+ }
+
+ switch ea.pos {
+ case 1:
+ switch ea.style {
+ case styleQuote, styleVerse:
+ ea.Attrs[attrNameAttribution] = str
+ default:
+ ea.Attrs[str] = ""
+ }
+ case 2:
+ switch ea.style {
+ case styleQuote, styleVerse:
+ ea.Attrs[attrNameCitation] = str
+ default:
+ ea.Attrs[str] = ""
+ }
+ }
+ }
+ }
+}
diff --git a/element_attribute_test.go b/element_attribute_test.go
new file mode 100644
index 0000000..b112161
--- /dev/null
+++ b/element_attribute_test.go
@@ -0,0 +1,75 @@
+// Copyright 2020, Shulhan <ms@kilabit.info>. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package asciidoctor
+
+import (
+ "testing"
+
+ "github.com/shuLhan/share/lib/test"
+)
+
+func Test_parseElementAttribute(t *testing.T) {
+ cases := []struct {
+ raw string
+ exp elementAttribute
+ }{{
+ raw: "",
+ }, {
+ raw: "[]",
+ }, {
+ raw: "[STYLE]",
+ exp: elementAttribute{
+ rawStyle: "STYLE",
+ },
+ }, {
+ raw: "[style#id]",
+ exp: elementAttribute{
+ ID: "id",
+ rawStyle: "style",
+ },
+ }, {
+ raw: `[#id.role1.role2,options="opt1,opt2"]`,
+ exp: elementAttribute{
+ ID: "id",
+ roles: []string{"role1", "role2"},
+ options: []string{"opt1", "opt2"},
+ pos: 1,
+ },
+ }, {
+ raw: `[cols="3*,^"]`,
+ exp: elementAttribute{
+ Attrs: map[string]string{
+ attrNameCols: "3*,^",
+ },
+ },
+ }, {
+ raw: `[quote, attribution]`,
+ exp: elementAttribute{
+ Attrs: map[string]string{
+ attrNameAttribution: "attribution",
+ },
+ rawStyle: "quote",
+ style: styleQuote,
+ pos: 1,
+ },
+ }, {
+ raw: `[quote, attribution, citation]`,
+ exp: elementAttribute{
+ Attrs: map[string]string{
+ attrNameAttribution: "attribution",
+ attrNameCitation: "citation",
+ },
+ rawStyle: "quote",
+ style: styleQuote,
+ pos: 2,
+ },
+ }}
+
+ for _, c := range cases {
+ got := elementAttribute{}
+ got.parseElementAttribute(c.raw)
+ test.Assert(t, c.raw, c.exp, got, false)
+ }
+}
diff --git a/html_backend.go b/html_backend.go
index 1766223..d326c33 100644
--- a/html_backend.go
+++ b/html_backend.go
@@ -9,6 +9,8 @@ import (
"fmt"
"io"
"strings"
+
+ libstrings "github.com/shuLhan/share/lib/strings"
)
const (
@@ -53,7 +55,7 @@ func htmlWriteBlockBegin(node *adocNode, out io.Writer, addClass string) {
fmt.Fprintf(out, ` id="%s"`, node.ID)
}
- classes := node.Classes()
+ classes := node.htmlClasses()
c := strings.TrimSpace(addClass + " " + classes)
if len(c) > 0 {
fmt.Fprintf(out, ` class="%s">`, c)
@@ -80,7 +82,7 @@ func htmlWriteBlockAdmonition(node *adocNode, out io.Writer) {
iconsFont := node.Attrs[attrNameIcons]
if iconsFont == attrValueFont {
fmt.Fprintf(out, _htmlAdmonitionIconsFont,
- strings.ToLower(node.Classes()), node.rawLabel.String())
+ strings.ToLower(node.htmlClasses()), node.rawLabel.String())
} else {
fmt.Fprintf(out, "\n<div class=%q>%s</div>", attrValueTitle,
node.rawLabel.String())
@@ -95,22 +97,25 @@ func htmlWriteBlockAdmonition(node *adocNode, out io.Writer) {
}
func htmlWriteBlockAudio(node *adocNode, out io.Writer) {
+ var (
+ optAutoplay string
+ optControls = " controls"
+ optLoop string
+ )
+
htmlWriteBlockBegin(node, out, "audioblock")
fmt.Fprintf(out, "\n<div class=%q>", attrValueContent)
src := node.Attrs[attrNameSrc]
- optAutoplay, ok := node.Opts[optNameAutoplay]
- if ok {
+ if libstrings.IsContain(node.options, optNameAutoplay) {
optAutoplay = " autoplay"
}
- optControls, ok := node.Opts[optNameControls]
- if ok {
- optControls = " controls"
+ if libstrings.IsContain(node.options, optNameNocontrols) {
+ optControls = ""
}
- optLoop, ok := node.Opts[optNameLoop]
- if ok {
+ if libstrings.IsContain(node.options, optNameLoop) {
optLoop = " loop"
}
@@ -171,12 +176,12 @@ func htmlWriteBlockQuote(node *adocNode, out io.Writer) {
func htmlWriteBlockQuoteEnd(node *adocNode, out io.Writer) {
fmt.Fprint(out, "\n</blockquote>")
- if len(node.key) > 0 {
+ if v, ok := node.Attrs[attrNameAttribution]; ok {
fmt.Fprintf(out, "\n<div class=%q>\n&#8212; %s",
- attrValueAttribution, node.key)
+ attrNameAttribution, v)
}
- if len(node.value) > 0 {
- fmt.Fprintf(out, "<br>\n<cite>%s</cite>", node.value)
+ if v, ok := node.Attrs[attrNameCitation]; ok {
+ fmt.Fprintf(out, "<br>\n<cite>%s</cite>", v)
}
fmt.Fprint(out, "\n</div>\n</div>")
}
@@ -197,17 +202,22 @@ func htmlWriteBlockVerse(node *adocNode, out io.Writer) {
func htmlWriteBlockVerseEnd(node *adocNode, out io.Writer) {
fmt.Fprint(out, "</pre>")
- if len(node.key) > 0 {
+ if v, ok := node.Attrs[attrNameAttribution]; ok {
fmt.Fprintf(out, "\n<div class=%q>\n&#8212; %s",
- attrValueAttribution, node.key)
+ attrNameAttribution, v)
}
- if len(node.value) > 0 {
- fmt.Fprintf(out, "<br>\n<cite>%s</cite>", node.value)
+ if v, ok := node.Attrs[attrNameCitation]; ok {
+ fmt.Fprintf(out, "<br>\n<cite>%s</cite>", v)
}
fmt.Fprint(out, "\n</div>\n</div>")
}
func htmlWriteBlockVideo(node *adocNode, out io.Writer) {
+ var (
+ isYoutube bool
+ isVimeo bool
+ )
+
src := node.GetVideoSource()
width, withWidth := node.Attrs[attrNameWidth]
if withWidth {
@@ -217,8 +227,13 @@ func htmlWriteBlockVideo(node *adocNode, out io.Writer) {
if withHeight {
height = fmt.Sprintf(` height="%s"`, height)
}
- _, isYoutube := node.Attrs[attrNameYoutube]
- _, isVimeo := node.Attrs[attrNameVimeo]
+
+ if node.rawStyle == attrNameYoutube {
+ isYoutube = true
+ }
+ if node.rawStyle == attrNameVimeo {
+ isVimeo = true
+ }
htmlWriteBlockBegin(node, out, "videoblock")
@@ -233,20 +248,24 @@ func htmlWriteBlockVideo(node *adocNode, out io.Writer) {
} else if isVimeo {
fmt.Fprintf(out, _htmlBlockVideoVimeo, width, height, src)
} else {
+ var (
+ optControls = " controls"
+ optAutoplay string
+ optLoop string
+ )
+
optPoster, withPoster := node.Attrs[attrNamePoster]
if withPoster {
optPoster = fmt.Sprintf(` poster="%s"`, optPoster)
}
- optControls, ok := node.Attrs[optNameNocontrols]
- if !ok {
- optControls = " controls"
+
+ if libstrings.IsContain(node.options, optNameNocontrols) {
+ optControls = ""
}
- optAutoplay, ok := node.Attrs[optNameAutoplay]
- if ok {
+ if libstrings.IsContain(node.options, optNameAutoplay) {
optAutoplay = " autoplay"
}
- optLoop, ok := node.Attrs[optNameLoop]
- if ok {
+ if libstrings.IsContain(node.options, optNameLoop) {
optLoop = " loop"
}
@@ -364,7 +383,7 @@ func htmlWriteHeader(doc *Document, out io.Writer) {
}
func htmlWriteInlineImage(node *adocNode, out io.Writer) {
- classes := strings.TrimSpace("image " + node.Classes())
+ classes := strings.TrimSpace("image " + node.htmlClasses())
fmt.Fprintf(out, "<span class=%q>", classes)
link, withLink := node.Attrs[attrNameLink]
if withLink {
@@ -692,7 +711,7 @@ func htmlWriteToC(doc *Document, node *adocNode, out io.Writer, level int) {
func htmlWriteURLBegin(node *adocNode, out io.Writer) {
fmt.Fprintf(out, "<a href=\"%s\"", node.Attrs[attrNameHref])
- classes := node.Classes()
+ classes := node.htmlClasses()
if len(classes) > 0 {
fmt.Fprintf(out, ` class="%s"`, classes)
}
diff --git a/parser.go b/parser.go
index c2d5e12..ff9ff84 100644
--- a/parser.go
+++ b/parser.go
@@ -10,7 +10,6 @@ import (
"unicode"
"github.com/shuLhan/share/lib/ascii"
- "github.com/shuLhan/share/lib/parser"
)
const (
@@ -97,6 +96,8 @@ const (
const (
attrNameAlign = "align"
attrNameAlt = "alt"
+ attrNameAttribution = "attribution"
+ attrNameCitation = "citation"
attrNameCols = "cols"
attrNameDiscrete = "discrete"
attrNameEnd = "end"
@@ -107,6 +108,7 @@ const (
attrNameLang = "lang"
attrNameLink = "link"
attrNameOptions = "options"
+ attrNameOpts = "opts"
attrNamePoster = "poster"
attrNameRefText = "reftext"
attrNameRel = "rel"
@@ -124,21 +126,20 @@ const (
)
const (
- attrValueAttribution = "attribution"
- attrValueAuthor = "author"
- attrValueBare = "bare"
- attrValueBlank = "_blank"
- attrValueContent = "content"
- attrValueEmail = "email"
- attrValueFont = "font"
- attrValueFooter = "footer"
- attrValueHeader = "header"
- attrValueImage = "image"
- attrValueNoopener = "noopener"
- attrValueNoHeader = "noheader"
- attrValueRevDate = "revdate"
- attrValueRevNumber = "revnumber"
- attrValueTitle = attrNameTitle
+ attrValueAuthor = "author"
+ attrValueBare = "bare"
+ attrValueBlank = "_blank"
+ attrValueContent = "content"
+ attrValueEmail = "email"
+ attrValueFont = "font"
+ attrValueFooter = "footer"
+ attrValueHeader = "header"
+ attrValueImage = "image"
+ attrValueNoopener = "noopener"
+ attrValueNoHeader = "noheader"
+ attrValueRevDate = "revdate"
+ attrValueRevNumber = "revnumber"
+ attrValueTitle = attrNameTitle
)
// List of document metadata.
@@ -218,6 +219,7 @@ const (
styleSectionIndex
styleParagraphLead
styleParagraphNormal
+ styleLink
styleNumberingArabic
styleNumberingDecimal
styleNumberingLoweralpha
@@ -473,88 +475,6 @@ func parseAttribute(line string, strict bool) (key, value string, ok bool) {
}
//
-// parseAttributeElement parse list of attributes in between "[" "]".
-//
-// BLOCK_ATTRS = BLOCK_ATTR *("," BLOCK_ATTR)
-//
-// BLOCK_ATTR = ATTR_NAME ( "=" (DQUOTE) ATTR_VALUE (DQUOTE) )
-//
-// ATTR_NAME = WORD
-//
-// ATTR_VALUE = STRING
-//
-// The attribute may not have a value.
-//
-// If the attribute value contains space or comma, it must be wrapped with
-// double quote.
-// The double quote on value will be removed when stored on opts.
-//
-// It will return nil if input is not a valid block attribute.
-//
-func parseAttributeElement(in string) (attrName, attrValue string, opts []string) {
- p := parser.New(in, `[,="]`)
- tok, c := p.Token()
- if c != '[' {
- return "", "", nil
- }
- if len(tok) > 0 {
- return "", "", nil
- }
-
- for c != 0 {
- tok, c = p.Token()
- tok = strings.TrimSpace(tok)
- if c == '"' && len(tok) == 0 {
- tok, c = p.ReadEnclosed('"', '"')
- tok = strings.TrimSpace(tok)
- opts = append(opts, tok)
- continue
- }
- if c == ',' || c == ']' {
- if len(tok) > 0 {
- opts = append(opts, tok)
- }
- if c == ']' {
- break
- }
- continue
- }
- if c != '=' {
- // Ignore invalid attribute.
- for c != ',' && c != 0 {
- _, c = p.Token()
- }
- continue
- }
- key := tok
- tok, c = p.Token()
- tok = strings.TrimSpace(tok)
- if c == '"' {
- tok, c = p.ReadEnclosed('"', '"')
- tok = strings.TrimSpace(tok)
- opts = append(opts, key+"="+tok)
- } else {
- opts = append(opts, key+"="+tok)
- }
-
- for c != ',' && c != 0 {
- _, c = p.Token()
- }
- }
- if len(opts) == 0 {
- return "", "", nil
- }
-
- nameValue := strings.Split(opts[0], "=")
- attrName = nameValue[0]
- if len(nameValue) >= 2 {
- attrValue = strings.Join(nameValue[1:], "=")
- }
-
- return attrName, attrValue, opts
-}
-
-//
// parseAttrRef parse the attribute reference, an attribute key wrapped by
// "{" "}". If the attribute reference exist, replace the content with the
// attribute value and reset the parser state to zero.
@@ -583,7 +503,7 @@ func parseAttrRef(doc *Document, content []byte, x int) (
}
//
-// parseIDLabel parse the s "ID (,LABEL)" into ID and label.
+// parseIDLabel parse the string "ID (,LABEL)" into ID and label.
// It will return empty id and label if ID is not valid.
//
func parseIDLabel(s string) (id, label string) {
@@ -605,7 +525,7 @@ func parseInlineMarkup(doc *Document, content []byte) (container *adocNode) {
}
//
-// parseStyle parse line that start with "[" and end with "]".
+// parseStyle get the style based on string value.
//
func parseStyle(styleName string) (styleKind int64) {
// Check for admonition label first...
diff --git a/parser_inline.go b/parser_inline.go
index b37d84f..e0bfb78 100644
--- a/parser_inline.go
+++ b/parser_inline.go
@@ -439,12 +439,14 @@ func (pi *parserInline) parseCrossRef() bool {
// The ID field will we non-empty if href is empty, it will be
// revalidated later when rendered.
nodeCrossRef := &adocNode{
+ elementAttribute: elementAttribute{
+ Attrs: map[string]string{
+ attrNameHref: href,
+ attrNameTitle: title,
+ },
+ },
kind: nodeKindCrossReference,
raw: []byte(label),
- Attrs: map[string]string{
- attrNameHref: href,
- attrNameTitle: title,
- },
}
pi.current.addChild(nodeCrossRef)
node := &adocNode{
@@ -475,7 +477,9 @@ func (pi *parserInline) parseInlineID() bool {
id = pi.doc.registerAnchor(id, label)
node := &adocNode{
- ID: id,
+ elementAttribute: elementAttribute{
+ ID: id,
+ },
kind: nodeKindInlineID,
}
pi.current.backTrimSpace()
@@ -516,7 +520,9 @@ func (pi *parserInline) parseInlineIDShort() bool {
stringID = pi.doc.registerAnchor(stringID, "")
node := &adocNode{
- ID: stringID,
+ elementAttribute: elementAttribute{
+ ID: stringID,
+ },
kind: nodeKindInlineIDShort,
}
pi.state.push(nodeKindInlineIDShort)
@@ -672,8 +678,10 @@ func (pi *parserInline) parseInlineImage() *adocNode {
lineImage := content[:idx+1]
nodeImage := &adocNode{
- kind: nodeKindInlineImage,
- Attrs: make(map[string]string),
+ elementAttribute: elementAttribute{
+ Attrs: make(map[string]string),
+ },
+ kind: nodeKindInlineImage,
}
if nodeImage.parseBlockImage(pi.doc, string(lineImage)) {
pi.x += idx + 2
@@ -906,8 +914,10 @@ func (pi *parserInline) parseURL(scheme string) (node *adocNode) {
}
node = &adocNode{
- kind: nodeKindURL,
- Attrs: make(map[string]string),
+ elementAttribute: elementAttribute{
+ Attrs: make(map[string]string),
+ },
+ kind: nodeKindURL,
}
content := pi.content[pi.x+1:]
@@ -920,7 +930,7 @@ func (pi *parserInline) parseURL(scheme string) (node *adocNode) {
}
if c != '[' {
if scheme == macroHTTP || scheme == macroHTTPS {
- node.classes.add(attrValueBare)
+ node.addRole(attrValueBare)
}
if c == '.' || c == ',' || c == ';' {
uri = uri[:len(uri)-1]
@@ -949,54 +959,23 @@ func (pi *parserInline) parseURL(scheme string) (node *adocNode) {
pi.prev = 0
attr := string(content[x : x+idx+1])
- key, _, attrs := parseAttributeElement(attr)
- if len(attrs) == 0 {
+ node.style = styleLink
+ node.parseElementAttribute(attr)
+ if len(node.Attrs) == 0 {
// empty "[]"
node.raw = uri
return node
}
- if len(attrs) >= 1 {
- if key[len(key)-1] == '^' {
+ if len(node.rawStyle) >= 1 {
+ l := len(node.rawStyle)
+ if node.rawStyle[l-1] == '^' {
node.Attrs[attrNameTarget] = attrValueBlank
- key = strings.TrimRight(key, "^")
+ node.rawStyle = node.rawStyle[:l-1]
node.Attrs[attrNameRel] = attrValueNoopener
}
- child := parseInlineMarkup(pi.doc, []byte(key))
+ child := parseInlineMarkup(pi.doc, []byte(node.rawStyle))
node.addChild(child)
}
- if len(attrs) >= 2 {
- attrTarget := strings.Split(attrs[1], "=")
- if len(attrTarget) == 2 {
- switch attrTarget[0] {
- case attrNameWindow:
- node.Attrs[attrNameTarget] = attrTarget[1]
- if attrTarget[1] == attrValueBlank {
- node.Attrs[attrNameRel] = attrValueNoopener
- }
- case attrNameRole:
- classes := strings.Split(attrTarget[1], ",")
- for _, c := range classes {
- if len(c) > 0 {
- node.classes.add(c)
- }
- }
- }
- }
- }
- if len(attrs) >= 3 {
- attrRole := strings.Split(attrs[2], "=")
- if len(attrRole) == 2 {
- switch attrRole[0] {
- case attrNameRole:
- classes := strings.Split(attrRole[1], ",")
- for _, c := range classes {
- if len(c) > 0 {
- node.classes.add(c)
- }
- }
- }
- }
- }
return node
}
diff --git a/parser_test.go b/parser_test.go
index c0835c7..64d1a4d 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -26,54 +26,3 @@ func TestIsValidID(t *testing.T) {
test.Assert(t, c.id, c.exp, got, true)
}
}
-
-func TestParseAttributeElement(t *testing.T) {
- cases := []struct {
- in string
- expKey string
- expValue string
- expOpts []string
- }{{
- in: "",
- }, {
- in: "[]",
- }, {
- in: `[a]`,
- expKey: "a",
- expOpts: []string{
- "a",
- },
- }, {
- in: `[a=2]`,
- expKey: "a",
- expValue: "2",
- expOpts: []string{
- "a=2",
- },
- }, {
- in: `[a=2,b="c, d",e,f=3]`,
- expKey: "a",
- expValue: "2",
- expOpts: []string{
- "a=2",
- `b=c, d`,
- "e",
- "f=3",
- },
- }, {
- in: `["A,B",w=_blank,role="a,b"]`,
- expKey: "A,B",
- expOpts: []string{
- "A,B",
- "w=_blank",
- "role=a,b",
- },
- }}
-
- for _, c := range cases {
- key, val, opts := parseAttributeElement(c.in)
- test.Assert(t, "attribute name", c.expKey, key, true)
- test.Assert(t, "attribute value", c.expValue, val, true)
- test.Assert(t, "opts", c.expOpts, opts, true)
- }
-}