diff options
| author | Shulhan <ms@kilabit.info> | 2022-10-20 04:48:20 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2022-10-20 22:09:31 +0700 |
| commit | 26c0d32b05eefb9b7040be69717d58e62bbdac2f (patch) | |
| tree | 86d02f3bd444b5148733cee1dca2756bff1c1f3a | |
| parent | dc6f1e2a3e720b706dbf945ae6b608199f630fb4 (diff) | |
| download | asciidoctor-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-- | README | 1 | ||||
| -rw-r--r-- | _doc/SPECS.adoc | 19 | ||||
| -rw-r--r-- | document.go | 28 | ||||
| -rw-r--r-- | element.go | 3 | ||||
| -rw-r--r-- | html_backend.go | 48 | ||||
| -rw-r--r-- | inline_parser.go | 61 | ||||
| -rw-r--r-- | inline_parser_test.go | 36 | ||||
| -rw-r--r-- | macro.go | 160 | ||||
| -rw-r--r-- | parser.go | 11 | ||||
| -rw-r--r-- | testdata/inline_parser/macro_footnote_externalized_test.txt | 56 | ||||
| -rw-r--r-- | testdata/inline_parser/macro_footnote_test.txt | 55 | ||||
| -rw-r--r-- | testdata/test.got.html | 4 |
12 files changed, 430 insertions, 52 deletions
@@ -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 = ¯o{ + 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 ( @@ -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 +} @@ -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> |
