diff options
| author | Shulhan <m.shulhan@gmail.com> | 2020-12-02 01:56:45 +0700 |
|---|---|---|
| committer | Shulhan <m.shulhan@gmail.com> | 2020-12-03 03:50:04 +0700 |
| commit | e80d5e10ee50abb56d5af3f1adefcc5262f56cd9 (patch) | |
| tree | 7726e347133fbcb15b97d48e013102a7831cba2b | |
| parent | be31f67526193a1a575ebe03be4612ac42e981b4 (diff) | |
| download | asciidoctor-go-e80d5e10ee50abb56d5af3f1adefcc5262f56cd9.tar.xz | |
all: unified the document node attributes
| -rw-r--r-- | adoc_node.go | 171 | ||||
| -rw-r--r-- | adoc_table.go | 10 | ||||
| -rw-r--r-- | document_parser.go | 137 | ||||
| -rw-r--r-- | element_attribute.go | 206 | ||||
| -rw-r--r-- | element_attribute_test.go | 75 | ||||
| -rw-r--r-- | html_backend.go | 75 | ||||
| -rw-r--r-- | parser.go | 120 | ||||
| -rw-r--r-- | parser_inline.go | 79 | ||||
| -rw-r--r-- | parser_test.go | 51 |
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— %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— %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) } @@ -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) - } -} |
