aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-10-20 04:48:20 +0700
committerShulhan <ms@kilabit.info>2022-10-20 22:09:31 +0700
commit26c0d32b05eefb9b7040be69717d58e62bbdac2f (patch)
tree86d02f3bd444b5148733cee1dca2756bff1c1f3a
parentdc6f1e2a3e720b706dbf945ae6b608199f630fb4 (diff)
downloadasciidoctor-go-26c0d32b05eefb9b7040be69717d58e62bbdac2f.tar.xz
all: implement macro "footnote:"
Macro footnote grammar, ---- "footnote:" [ REF_ID ] "[" STRING "]" ---- In asciidoctor, footnote can be placed anywhere, even after WORD without space in between. The REF_ID, define the unique ID for footnote and can be used to reference the previous footnote. The first footnote with REF_ID, should have the STRING defined. The next footnote with the same REF_ID, should not have the STRING defined; if its defined, the STRING is ignored.
-rw-r--r--README1
-rw-r--r--_doc/SPECS.adoc19
-rw-r--r--document.go28
-rw-r--r--element.go3
-rw-r--r--html_backend.go48
-rw-r--r--inline_parser.go61
-rw-r--r--inline_parser_test.go36
-rw-r--r--macro.go160
-rw-r--r--parser.go11
-rw-r--r--testdata/inline_parser/macro_footnote_externalized_test.txt56
-rw-r--r--testdata/inline_parser/macro_footnote_test.txt55
-rw-r--r--testdata/test.got.html4
12 files changed, 430 insertions, 52 deletions
diff --git a/README b/README
index 52f975d..0d16fd0 100644
--- a/README
+++ b/README
@@ -90,6 +90,7 @@ The numbered one is based on the old documentation.
** 27.2. Defining an Anchor
** 27.3. Internal Cross References
** 27.5. Customizing the Cross Reference Text
+* https://docs.asciidoctor.org/asciidoc/latest/macros/footnote/[Footnotes]
* 28. Include Directive
** 28.1. Anatomy
** 28.2. Processing
diff --git a/_doc/SPECS.adoc b/_doc/SPECS.adoc
index c3f974c..4eae7a5 100644
--- a/_doc/SPECS.adoc
+++ b/_doc/SPECS.adoc
@@ -559,8 +559,25 @@ CELL_ALIGN_VER = "." ("<" / "^" / ">")
CELL_STYLE = "a" / "d" / "e" / "h" / "l" / "m" / "s" / "v"
----
+== Footnote
-== Inconsistencies and bugs on asciidoctor
+Syntax,
+
+----
+"footnote:" [ REF_ID ] "[" STRING "]"
+----
+
+In asciidoctor, footnote can be placed anywhere, even after WORD without space
+in between.
+
+The REF_ID, define the unique ID for footnote and can be used to reference the
+previous footnote.
+The first footnote with REF_ID, should have the STRING defined.
+The next footnote with the same REF_ID, should not have the STRING defined;
+if its defined, the STRING is ignored.
+
+
+== Inconsistencies and bugs in asciidoctor
Listing style "[listing]" followed by "...." is become listing block.
Example,
diff --git a/document.go b/document.go
index 8b9d42e..2b34300 100644
--- a/document.go
+++ b/document.go
@@ -38,6 +38,9 @@ type Document struct {
// its ID.
titleID map[string]string
+ // List of footnote ID and its text.
+ footnotes []*macro
+
Revision Revision
LastUpdated string
@@ -273,6 +276,8 @@ func (doc *Document) toHTMLBody(buf *bytes.Buffer, withHeaderFooter bool) {
htmlWriteBody(doc, buf)
+ htmlWriteFootnoteDefs(doc, buf)
+
if withHeaderFooter {
_, ok = doc.Attributes[metaNameNoFooter]
if !ok {
@@ -313,6 +318,29 @@ func (doc *Document) registerAnchor(id, label string) string {
return id
}
+// registerFootnote add footnote with id and text, where id is optional.
+// If the id already exist it will return true.
+func (doc *Document) registerFootnote(id string, rawContent []byte) (mcr *macro, exist bool) {
+ if len(id) != 0 {
+ // Find existing footnote with the same ID.
+ for _, mcr = range doc.footnotes {
+ if mcr.key == id {
+ return mcr, true
+ }
+ }
+ }
+
+ mcr = &macro{
+ key: id,
+ rawContent: rawContent,
+ }
+ doc.footnotes = append(doc.footnotes, mcr)
+
+ mcr.level = len(doc.footnotes)
+
+ return mcr, false
+}
+
// tocHTML write table of contents with HTML template into out.
func (doc *Document) tocHTML(out io.Writer) {
var (
diff --git a/element.go b/element.go
index a558055..1abba9e 100644
--- a/element.go
+++ b/element.go
@@ -831,6 +831,9 @@ func (el *element) toHTML(doc *Document, w io.Writer) {
}
fmt.Fprintf(w, `<a href="#%s">%s</a>`, href, label)
+ case elKindFootnote:
+ htmlWriteFootnote(el, w)
+
case elKindMacroTOC:
if doc.tocIsEnabled && doc.tocPosition == metaValueMacro {
doc.tocHTML(w)
diff --git a/html_backend.go b/html_backend.go
index e631448..4c2e467 100644
--- a/html_backend.go
+++ b/html_backend.go
@@ -393,6 +393,54 @@ func htmlWriteFooter(doc *Document, out io.Writer) {
fmt.Fprint(out, "\n</div>\n</div>")
}
+// htmlWriteFootnote generate HTML content for footnote.
+// Each unique footnote will have its id, so it can be referenced at footer.
+func htmlWriteFootnote(el *element, out io.Writer) {
+ if len(el.ID) != 0 {
+ // The first footnote with explicit ID.
+ fmt.Fprintf(out, `<sup class="footnote" id="_footnote_%s">[<a id="_footnoteref_%d" class="footnote" href="#_footnotedef_%d" title="View footnote.">%d</a>]</sup>`,
+ el.ID, el.level, el.level, el.level)
+
+ } else if len(el.key) != 0 {
+ // The first footnote without ID.
+ fmt.Fprintf(out, `<sup class="footnote">[<a id="_footnoteref_%d" class="footnote" href="#_footnotedef_%d" title="View footnote.">%d</a>]</sup>`,
+ el.level, el.level, el.level)
+ } else {
+ // The next footnote with same ID.
+ fmt.Fprintf(out, `<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_%d" title="View footnote.">%d</a>]</sup>`,
+ el.level, el.level)
+ }
+}
+
+func htmlWriteFootnoteDefs(doc *Document, out io.Writer) {
+ if len(doc.footnotes) == 0 {
+ return
+ }
+
+ fmt.Fprint(out, "\n")
+ fmt.Fprint(out, `<div id="footnotes">`)
+ fmt.Fprint(out, "\n")
+ fmt.Fprint(out, `<hr>`)
+ fmt.Fprint(out, "\n")
+
+ var (
+ mcr *macro
+ )
+ for _, mcr = range doc.footnotes {
+ fmt.Fprintf(out, `<div class="footnote" id="_footnotedef_%d">`, mcr.level)
+ fmt.Fprint(out, "\n")
+ fmt.Fprintf(out, `<a href="#_footnoteref_%d">%d</a>. `, mcr.level, mcr.level)
+ if mcr.content != nil {
+ mcr.content.toHTML(doc, out)
+ }
+ fmt.Fprint(out, "\n")
+ fmt.Fprint(out, `</div>`)
+ fmt.Fprint(out, "\n")
+ }
+ fmt.Fprint(out, `</div>`)
+ fmt.Fprint(out, "\n")
+}
+
func htmlWriteHeader(doc *Document, out io.Writer) {
fmt.Fprint(out, "\n<div id=\"header\">")
diff --git a/inline_parser.go b/inline_parser.go
index 881374e..eea1e39 100644
--- a/inline_parser.go
+++ b/inline_parser.go
@@ -397,20 +397,6 @@ func (pi *inlineParser) escape() {
pi.prev = pi.c
}
-func (pi *inlineParser) getBackMacroName() (macroName string, lastc byte) {
- var (
- raw []byte = pi.current.raw
- start int = len(raw) - 1
- )
- for start >= 0 {
- if !ascii.IsAlpha(raw[start]) {
- return string(raw[start+1:]), raw[start]
- }
- start--
- }
- return string(raw), 0
-}
-
func (pi *inlineParser) parseCrossRef() bool {
var (
raw []byte = pi.content[pi.x+2:]
@@ -729,51 +715,48 @@ func (pi *inlineParser) parseInlineImage() *element {
func (pi *inlineParser) parseMacro() bool {
var (
- el *element
- name string
- lastc byte
+ el *element
+ name string
+ n int
)
- name, lastc = pi.getBackMacroName()
- if lastc == '\\' || len(name) == 0 {
+ name = pi.parseMacroName(pi.current.raw)
+ if len(name) == 0 {
return false
}
switch name {
- case ``:
- return false
- case macroFTP, macroHTTPS, macroHTTP, macroIRC, macroLink, macroMailto:
- el = pi.parseURL(name)
+ case macroFootnote:
+ el, n = pi.parseMacroFootnote(pi.content[pi.x+1:])
if el == nil {
return false
}
- pi.current.raw = pi.current.raw[:len(pi.current.raw)-len(name)]
+ pi.x += n
+ pi.prev = 0
- pi.current.addChild(el)
- el = &element{
- kind: elKindText,
+ case macroFTP, macroHTTPS, macroHTTP, macroIRC, macroLink, macroMailto:
+ el = pi.parseURL(name)
+ if el == nil {
+ return false
}
- pi.current.addChild(el)
- pi.current = el
- return true
+
case macroImage:
el = pi.parseInlineImage()
if el == nil {
return false
}
+ }
- pi.current.raw = pi.current.raw[:len(pi.current.raw)-len(name)]
+ pi.current.raw = pi.current.raw[:len(pi.current.raw)-len(name)]
- pi.current.addChild(el)
- el = &element{
- kind: elKindText,
- }
- pi.current.addChild(el)
- pi.current = el
- return true
+ pi.current.addChild(el)
+ el = &element{
+ kind: elKindText,
}
- return false
+ pi.current.addChild(el)
+ pi.current = el
+ return true
}
func (pi *inlineParser) parsePassthrough() bool {
diff --git a/inline_parser_test.go b/inline_parser_test.go
index 62c4f74..0fd62bf 100644
--- a/inline_parser_test.go
+++ b/inline_parser_test.go
@@ -96,3 +96,39 @@ func TestInlineParser_parseInlineID_isForToC(t *testing.T) {
test.Assert(t, c.content, c.exp, got)
}
}
+
+func TestInlineParser_macro_footnote(t *testing.T) {
+ var (
+ testFiles = []string{
+ `testdata/inline_parser/macro_footnote_test.txt`,
+ `testdata/inline_parser/macro_footnote_externalized_test.txt`,
+ }
+
+ testFile string
+ got bytes.Buffer
+ tdata *test.Data
+ doc *Document
+ exp []byte
+ err error
+ )
+
+ for _, testFile = range testFiles {
+ tdata, err = test.LoadData(testFile)
+ if err != nil {
+ t.Fatalf(`%s: %s`, testFile, err)
+ }
+
+ doc = Parse(tdata.Input[`input.adoc`])
+
+ err = doc.ToHTMLEmbedded(&got)
+ if err != nil {
+ t.Fatalf(`%s: %s`, testFile, err)
+ }
+
+ exp = tdata.Output[`output.html`]
+
+ test.Assert(t, testFile, string(exp), got.String())
+
+ got.Reset()
+ }
+}
diff --git a/macro.go b/macro.go
new file mode 100644
index 0000000..4485660
--- /dev/null
+++ b/macro.go
@@ -0,0 +1,160 @@
+// SPDX-FileCopyrightText: 2022 M. Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package asciidoctor
+
+import (
+ "strconv"
+
+ "github.com/shuLhan/share/lib/ascii"
+)
+
+const (
+ macroFTP = `ftp`
+ macroFootnote = `footnote`
+ macroHTTP = `http`
+ macroHTTPS = `https`
+ macroIRC = `irc`
+ macroImage = `image`
+ macroLink = `link`
+ macroMailto = `mailto`
+)
+
+var (
+ _macroKind = map[string]int{
+ macroFTP: elKindURL,
+ macroFootnote: elKindFootnote,
+ macroHTTP: elKindURL,
+ macroHTTPS: elKindURL,
+ macroIRC: elKindURL,
+ macroImage: elKindInlineImage,
+ macroLink: elKindURL,
+ macroMailto: elKindURL,
+ }
+)
+
+type macro struct {
+ content *element
+
+ // key represent the URL in elKindURL or elKindImage;
+ // or ID in elKindFootnote.
+ // For ID, it could be empty.
+ key string
+
+ // val represent the text for URL or image and footnote.
+ rawContent []byte
+
+ // level represent footnoted index number.
+ level int
+}
+
+// parseMacroName parse inline macro.
+//
+// The parser read the content textBefore in backward order until it found one
+// of macro name.
+// Macro can be escaped using the backslash, for example "\link:", and it will
+// be ignored.
+//
+// If macro name and value valid it will return the element for that macro.
+func (pi *inlineParser) parseMacroName(textBefore []byte) (macroName string) {
+ var (
+ x int = len(textBefore) - 1
+
+ ok bool
+ )
+
+ for x >= 0 {
+ if !ascii.IsAlpha(textBefore[x]) {
+ return ``
+ }
+
+ macroName = string(textBefore[x:])
+ _, ok = _macroKind[macroName]
+ if ok {
+ break
+ }
+
+ x--
+ }
+ if !ok {
+ return ``
+ }
+
+ x--
+ if x >= 0 {
+ if textBefore[x] == '\\' {
+ // Macro is escaped.
+ return ``
+ }
+ }
+
+ return macroName
+}
+
+// parseMacroFootnote parse the footnote macro,
+//
+// "footnote:" [ REF_ID ] "[" STRING "]"
+//
+// The text content does not have "footnote:".
+//
+// The REF_ID, reference the previous footnote defined with the same.
+// The first footnote with REF_ID, should have the STRING defined.
+// The next footnote with the same REF_ID, should not have the STRING
+// defined; if its already defined, the STRING is ignored.
+//
+// It will return an element if footnote is valid.
+func (pi *inlineParser) parseMacroFootnote(text []byte) (el *element, n int) {
+ var (
+ mcr *macro
+ id string
+ key string
+ vbytes []byte
+ x int
+ exist bool
+ )
+
+ vbytes, x = indexByteUnescape(text, '[')
+ if x > 0 {
+ if !isValidID(vbytes) {
+ return nil, 0
+ }
+ id = string(vbytes)
+ }
+
+ n = x + 1
+ text = text[n:]
+
+ vbytes, x = indexByteUnescape(text, ']')
+ if x < 0 {
+ return nil, 0
+ }
+
+ n += x + 2
+
+ mcr, exist = pi.doc.registerFootnote(id, vbytes)
+ if exist {
+ id = ``
+ vbytes = nil
+ } else {
+ // Footnote without explicit ID will be set the key with its
+ // level.
+ key = strconv.FormatInt(int64(mcr.level), 10)
+ }
+
+ el = &element{
+ key: key,
+ raw: vbytes,
+ kind: elKindFootnote,
+ level: mcr.level,
+
+ elementAttribute: elementAttribute{
+ ID: id,
+ },
+ }
+
+ if vbytes != nil {
+ mcr.content = parseInlineMarkup(pi.doc, vbytes)
+ }
+
+ return el, n
+}
diff --git a/parser.go b/parser.go
index a210069..033604c 100644
--- a/parser.go
+++ b/parser.go
@@ -11,16 +11,6 @@ import (
)
const (
- macroFTP = `ftp`
- macroHTTP = `http`
- macroHTTPS = `https`
- macroIRC = `irc`
- macroImage = `image`
- macroLink = `link`
- macroMailto = `mailto`
-)
-
-const (
elKindUnknown int = iota
elKindDocHeader // Wrapper.
elKindPreamble // Wrapper.
@@ -46,6 +36,7 @@ const (
elKindBlockSidebar // "****"
elKindBlockVideo // "video::"
elKindCrossReference // "<<" REF ("," LABEL) ">>"
+ elKindFootnote // footnote:id[]
elKindInlineID // "[[" REF_ID "]]" TEXT
elKindInlineIDShort // "[#" REF_ID "]#" TEXT "#"
elKindInlineImage // Inline macro for "image:"
diff --git a/testdata/inline_parser/macro_footnote_externalized_test.txt b/testdata/inline_parser/macro_footnote_externalized_test.txt
new file mode 100644
index 0000000..bd8736c
--- /dev/null
+++ b/testdata/inline_parser/macro_footnote_externalized_test.txt
@@ -0,0 +1,56 @@
+>>> input.adoc
+:fn-hail-and-rainbow: footnote:[The _double_ hail-and-rainbow level makes my toes tingle.]
+:fn-disclaimer: footnote:disclaimer[Opinions are my own.]
+
+The hail-and-rainbow protocol can be initiated at five levels:
+
+. double{fn-hail-and-rainbow}
+. tertiary
+. supernumerary
+. supermassive
+. apocalyptic
+
+A bold statement!{fn-disclaimer}
+
+Another outrageous statement.{fn-disclaimer}
+
+<<< output.html
+
+<div class="paragraph">
+<p>The hail-and-rainbow protocol can be initiated at five levels:</p>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>double<sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>
+</p>
+</li>
+<li>
+<p>tertiary</p>
+</li>
+<li>
+<p>supernumerary</p>
+</li>
+<li>
+<p>supermassive</p>
+</li>
+<li>
+<p>apocalyptic</p>
+</li>
+</ol>
+</div>
+<div class="paragraph">
+<p>A bold statement!<sup class="footnote" id="_footnote_disclaimer">[<a id="_footnoteref_2" class="footnote" href="#_footnotedef_2" title="View footnote.">2</a>]</sup></p>
+</div>
+<div class="paragraph">
+<p>Another outrageous statement.<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_2" title="View footnote.">2</a>]</sup></p>
+</div>
+<div id="footnotes">
+<hr>
+<div class="footnote" id="_footnotedef_1">
+<a href="#_footnoteref_1">1</a>. The <em>double</em> hail-and-rainbow level makes my toes tingle.
+</div>
+<div class="footnote" id="_footnotedef_2">
+<a href="#_footnoteref_2">2</a>. Opinions are my own.
+</div>
+</div>
diff --git a/testdata/inline_parser/macro_footnote_test.txt b/testdata/inline_parser/macro_footnote_test.txt
new file mode 100644
index 0000000..f89c0c1
--- /dev/null
+++ b/testdata/inline_parser/macro_footnote_test.txt
@@ -0,0 +1,55 @@
+
+>>> input.adoc
+
+The hail-and-rainbow protocol can be initiated at five levels:
+
+. doublefootnote:[The _double_ hail-and-rainbow level makes my toes tingle.]
+. tertiary
+. supernumerary
+. supermassive
+. apocalyptic
+
+A bold statement!footnote:disclaimer[Opinions are my own.]
+
+Another outrageous statement.footnote:disclaimer[]
+
+<<< output.html
+
+<div class="paragraph">
+<p>The hail-and-rainbow protocol can be initiated at five levels:</p>
+</div>
+<div class="olist arabic">
+<ol class="arabic">
+<li>
+<p>double<sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>
+</p>
+</li>
+<li>
+<p>tertiary</p>
+</li>
+<li>
+<p>supernumerary</p>
+</li>
+<li>
+<p>supermassive</p>
+</li>
+<li>
+<p>apocalyptic</p>
+</li>
+</ol>
+</div>
+<div class="paragraph">
+<p>A bold statement!<sup class="footnote" id="_footnote_disclaimer">[<a id="_footnoteref_2" class="footnote" href="#_footnotedef_2" title="View footnote.">2</a>]</sup></p>
+</div>
+<div class="paragraph">
+<p>Another outrageous statement.<sup class="footnoteref">[<a class="footnote" href="#_footnotedef_2" title="View footnote.">2</a>]</sup></p>
+</div>
+<div id="footnotes">
+<hr>
+<div class="footnote" id="_footnotedef_1">
+<a href="#_footnoteref_1">1</a>. The <em>double</em> hail-and-rainbow level makes my toes tingle.
+</div>
+<div class="footnote" id="_footnotedef_2">
+<a href="#_footnoteref_2">2</a>. Opinions are my own.
+</div>
+</div>
diff --git a/testdata/test.got.html b/testdata/test.got.html
index 33b917d..d21e7a4 100644
--- a/testdata/test.got.html
+++ b/testdata/test.got.html
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="Author A, Author mid dle B">
<meta name="description" content="meta description">
-<meta name="generator" content="asciidoctor-go 0.3.1">
+<meta name="generator" content="asciidoctor-go 0.3.2">
<meta name="keywords" content="key, words">
<title>Example Document title</title>
<style>
@@ -3000,7 +3000,7 @@ this sidebar.</p>
<div id="footer">
<div id="footer-text">
1.1.1<br>
-Last updated 2022-09-04 23:41:43 +0700
+Last updated 2022-10-18 00:21:58 +0700
</div>
</div>
</body>