~shulhan/asciidoctor-go

5c7bfc04dc3d2cd60e84c80229804fdcd615709e — Shulhan 4 months ago 3d1caba
all: implement inline macro for passthrough ("pass:")

The inline passthrough "pass:" can be used to control the substitutions
applied to a run of text.

Ref: https://docs.asciidoctor.org/asciidoc/latest/pass/pass-macro/
M _doc/SPECS.adoc => _doc/SPECS.adoc +34 -0
@@ 476,6 476,8 @@ exist, otherwise it would be considered as normal text.

==  Passthrough

{url_ref}/pass/[Reference^]

----
PASSTHROUGH_SINGLE = FORMAT_BEGIN "+" TEXT "+" FORMAT_END



@@ 484,8 486,40 @@ PASSTHROUGH_DOUBLE = "++" TEXT "++"
PASSTHROUGH_TRIPLE = "+++" TEXT "+++"

PASSTHROUGH_BLOCK  = "++++" LF 1*LINE "++++" LF

PASSTHROUGH_MACRO  = "pass:" *(PASSMACRO_SUB) "[" TEXT "]"

PASSMACRO_SUB      = PASSMACRO_CHAR *("," PASSMACRO_CHAR)

PASSMACRO_CHAR     = "c" / "q" / "a" / "r" / "m" / "p"
                   / PASSMACRO_GROUP_NORMAL
                   / PASSMACRO_GROUP_VERBATIM

PASSMACRO_GROUP_NORMAL   = "n" ; equal to "c,q,r,m,p"

PASSMACRO_GROUP_VERBATIM = "v" ; equal to "c"
----

The "c" allow
{url_ref}/subs/special-characters/[special character substitutions].

The "q" allow
{url_ref}/subs/quotes/[quotes substitutions].

The "a" allow
{url_ref}/subs/attributes/[attributes references substitutions].

The "r" allow
{url_ref}/subs/replacements/[character replacement substitutions].

The "m" allow
{url_ref}/subs/macros/[macro substitutions].

The "p" allow
{url_ref}/subs/post-replacements/[post-replacement substitutions].

The substitutions are applied in above order.


==  URLs


A const.go => const.go +14 -0
@@ 0,0 1,14 @@
package asciidoctor

// List of passthrough substitutions.
const (
	passSubNone     int = 0
	passSubChar         = 1  // 'c'
	passSubQuote        = 2  // 'q'
	passSubAttr         = 4  // 'a'
	passSubRepl         = 8  // 'r'
	passSubMacro        = 16 // 'm'
	passSubPostRepl     = 32 // 'p'
	passSubNormal       = passSubChar | passSubQuote | passSubAttr | passSubRepl | passSubMacro | passSubPostRepl
	passSubVerbatim     = passSubChar
)

M element.go => element.go +6 -0
@@ 45,6 45,9 @@ type element struct {
	rawLabel bytes.Buffer
	level    int // The number of dot for ordered list, or '*' for unordered list.
	kind     int

	// List of substitutions to be applied on raw.
	applySubs int
}

func (el *element) getListOrderedClass() string {


@@ 870,6 873,9 @@ func (el *element) toHTML(doc *Document, w io.Writer) {
	case elKindInlineImage:
		htmlWriteInlineImage(el, w)

	case elKindInlinePass:
		htmlWriteInlinePass(doc, el, w)

	case elKindListDescription:
		htmlWriteListDescription(el, w)
	case elKindListOrdered:

M html_backend.go => html_backend.go +521 -0
@@ 9,6 9,7 @@ import (
	"io"
	"strings"

	libascii "github.com/shuLhan/share/lib/ascii"
	libstrings "github.com/shuLhan/share/lib/strings"
)



@@ 60,6 61,519 @@ const (
	htmlSymbolZeroWidthSpace    = `​`
)

// htmlSubs apply the text substitutions to element.raw based on applySubs in
// the following order: c, q, a, r, m, p.
// If applySubs is 0, it will return element.raw as is.
func htmlSubs(doc *Document, el *element) []byte {
	var (
		input = el.raw
	)
	if el.applySubs == 0 {
		return input
	}
	if el.applySubs&passSubChar != 0 {
		input = htmlSubsChar(input)
	}
	if el.applySubs&passSubQuote != 0 {
		input = htmlSubsQuote(input)
	}
	if el.applySubs&passSubAttr != 0 {
		input = htmlSubsAttr(doc, input)
	}
	if el.applySubs&passSubRepl != 0 {
		input = htmlSubsRepl(input)
	}
	if el.applySubs&passSubMacro != 0 {
		input = htmlSubsMacro(doc, input, el.kind == elKindInlinePass)
	}
	return input
}

// htmlSubsChar replace character '<', '>', and '&' with "&lt;", "&gt;", and
// "&amp;".
//
// Ref: https://docs.asciidoctor.org/asciidoc/latest/subs/special-characters/
func htmlSubsChar(input []byte) []byte {
	var (
		bb bytes.Buffer
		c  byte
	)
	for _, c = range input {
		if c == '<' {
			bb.WriteString(`&lt;`)
			continue
		}
		if c == '>' {
			bb.WriteString(`&gt;`)
			continue
		}
		if c == '&' {
			bb.WriteString(`&amp;`)
			continue
		}
		bb.WriteByte(c)
	}
	return bb.Bytes()
}

// htmlSubsQuote replace inline markup with its HTML markup.
// The following inline markup ara parsed and substitutes,
//
//   - emphasis: _word_ with "<em>word</em>".
//   - strong: *word* with "<strong>word</strong>".
//   - monospace: `word` with "<code>word</code>".
//   - superscript: ^word^ with "<sup>word</sup>".
//   - subscript: ~word~ with "<sub>word</sub>".
//   - double curved quotes: "`word`" with "&#8220;word&#8221;"
//   - single curved quotes: '`word`' with "&#8216;word&#8217;"
//
// Ref: https://docs.asciidoctor.org/asciidoc/latest/subs/quotes/
func htmlSubsQuote(input []byte) []byte {
	var (
		bb    bytes.Buffer
		x     int
		idx   int
		text  []byte
		c1    byte
		nextc byte
	)
	for x < len(input) {
		c1 = input[x]

		x++
		if x == len(input) {
			// Nothing left to parsed.
			bb.WriteByte(c1)
			break
		}
		nextc = input[x]

		if c1 == '_' {
			text, idx = indexByteUnescape(input[x:], c1)
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(`<em>`)
			bb.Write(text)
			bb.WriteString(`</em>`)
			x = x + idx + 1
			continue
		}
		if c1 == '*' {
			text, idx = indexByteUnescape(input[x:], c1)
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(`<strong>`)
			bb.Write(text)
			bb.WriteString(`</strong>`)
			x = x + idx + 1
			continue
		}
		if c1 == '`' {
			text, idx = indexByteUnescape(input[x:], c1)
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(`<code>`)
			bb.Write(text)
			bb.WriteString(`</code>`)
			x = x + idx + 1
			continue
		}
		if c1 == '^' {
			text, idx = indexByteUnescape(input[x:], c1)
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(`<sup>`)
			bb.Write(text)
			bb.WriteString(`</sup>`)
			x = x + idx + 1
			continue
		}
		if c1 == '~' {
			text, idx = indexByteUnescape(input[x:], c1)
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(`<sub>`)
			bb.Write(text)
			bb.WriteString(`</sub>`)
			x = x + idx + 1
			continue
		}
		if c1 == '"' {
			if nextc != '`' {
				bb.WriteByte(c1)
				continue
			}
			if x+1 == len(input) {
				bb.WriteByte(c1)
				continue
			}

			text, idx = indexUnescape(input[x+1:], []byte("`\""))
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(htmlSymbolLeftDoubleQuote)
			bb.Write(text)
			bb.WriteString(htmlSymbolRightDoubleQuote)
			x = x + idx + 3
			continue
		}
		if c1 == '\'' {
			if nextc != '`' {
				bb.WriteByte(c1)
				continue
			}
			if x+1 == len(input) {
				bb.WriteByte(c1)
				continue
			}

			text, idx = indexUnescape(input[x+1:], []byte("`'"))
			if text == nil {
				bb.WriteByte(c1)
				continue
			}
			bb.WriteString(htmlSymbolLeftSingleQuote)
			bb.Write(text)
			bb.WriteString(htmlSymbolRightSingleQuote)
			x = x + idx + 3
			continue
		}
		bb.WriteByte(c1)
	}
	return bb.Bytes()
}

// htmlSubsAttr replace attribute (the `{...}`) with its values.
//
// Ref: https://docs.asciidoctor.org/asciidoc/latest/subs/attributes/
func htmlSubsAttr(doc *Document, input []byte) []byte {
	var (
		bb     bytes.Buffer
		key    string
		val    string
		vbytes []byte
		idx    int
		x      int
		c      byte
		ok     bool
	)

	for x < len(input) {
		c = input[x]
		x++
		if c != '{' {
			bb.WriteByte(c)
			continue
		}

		vbytes, idx = indexByteUnescape(input[x:], '}')
		if vbytes == nil {
			bb.WriteByte(c)
			continue
		}
		vbytes = bytes.TrimSpace(vbytes)
		vbytes = bytes.ToLower(vbytes)

		key = string(vbytes)
		val, ok = _attrRef[key]
		if ok {
			bb.WriteString(val)
			x = x + idx + 1
			continue
		}

		val, ok = doc.Attributes[key]
		if !ok {
			bb.WriteByte(c)
			continue
		}

		// Add prefix "mailto:" if the ref name start with email, so
		// it can be parsed by caller as macro link.
		if key == `email` || strings.HasPrefix(key, `email_`) {
			val = `mailto:` + val + `[` + val + `]`
		}

		bb.WriteString(val)
		x = x + idx + 1
	}

	return bb.Bytes()
}

// htmlSubsRepl substitutes special characters with HTML unicode.
//
// The special characters are,
//
//   - (C) replaced with &#169;
//   - (R)  : &#174;
//   - (TM) : &#8482;
//   - --   : &#8212; Only replaced if between two word characters, between a
//     word character and a line boundary, or flanked by spaces.
//     When flanked by space characters (e.g., a -- b), the normal spaces are
//     replaced by thin spaces (&#8201;).
//   - ...  : &#8230;
//   - ->   : &#8594;
//   - =>   : &#8658;
//   - <-   : &#8592;
//   - <=   : &#8656;
//   - '    : &#8217;
//
// According to [the documentation], this substitution step also recognizes
// HTML and XML character references as well as decimal and hexadecimal
// Unicode code points, but we only cover the above right now.
//
// [the documentation]: https://docs.asciidoctor.org/asciidoc/latest/subs/replacements/
func htmlSubsRepl(input []byte) (out []byte) {
	var (
		text  []byte
		x     int
		idx   int
		c1    byte
		nextc byte
		prevc byte
	)

	out = make([]byte, 0, len(input))

	for x < len(input) {
		prevc = c1
		c1 = input[x]

		x++
		if x == len(input) {
			out = append(out, c1)
			break
		}
		nextc = input[x]

		if c1 == '(' {
			text, idx = indexByteUnescape(input[x:], ')')
			if len(text) == 1 {
				if text[0] == 'C' {
					out = append(out, []byte(htmlSymbolCopyright)...)
					x = x + idx + 1
					c1 = ')'
					continue
				}
				if text[0] == 'R' {
					out = append(out, []byte(htmlSymbolRegistered)...)
					x = x + idx + 1
					c1 = ')'
					continue
				}
			} else if len(text) == 2 {
				if text[0] == 'T' && text[1] == 'M' {
					out = append(out, []byte(htmlSymbolTrademark)...)
					x = x + idx + 1
					c1 = ')'
					continue
				}
			}

			out = append(out, c1)
			continue
		}
		if c1 == '-' {
			if nextc == '>' {
				out = append(out, []byte(htmlSymbolSingleRightArrow)...)
				x++
				c1 = nextc
				continue
			}
			if nextc == '-' {
				if x+1 >= len(input) {
					out = append(out, c1)
					continue
				}
				// set c1 to the third character after '--'.
				c1 = input[x+1]
				if libascii.IsSpace(prevc) && libascii.IsSpace(c1) {
					out = out[:len(out)-1]
					out = append(out, []byte(htmlSymbolThinSpace)...)
					out = append(out, []byte(htmlSymbolEmdash)...)
					out = append(out, []byte(htmlSymbolThinSpace)...)
					x += 2
					continue
				}
				if libascii.IsAlpha(prevc) && libascii.IsAlpha(c1) {
					out = append(out, []byte(htmlSymbolEmdash)...)
					x++
					continue
				}
			}
			out = append(out, c1)
			continue
		}
		if c1 == '=' {
			if nextc == '>' {
				out = append(out, []byte(htmlSymbolDoubleRightArrow)...)
				x++
				c1 = nextc
				continue
			}
			out = append(out, c1)
			continue
		}
		if c1 == '<' {
			if nextc == '-' {
				out = append(out, []byte(htmlSymbolSingleLeftArrow)...)
				x++
				continue
			}
			if nextc == '=' {
				out = append(out, []byte(htmlSymbolDoubleLeftArrow)...)
				x++
				continue
			}
			out = append(out, c1)
			continue
		}
		if c1 == '.' {
			if nextc != '.' {
				out = append(out, c1)
				continue
			}
			if x+1 >= len(input) {
				out = append(out, c1)
				continue
			}
			// Set c1 to the third character.
			c1 = input[x+1]
			if c1 == '.' {
				out = append(out, []byte(htmlSymbolEllipsis)...)
				x += 2
				continue
			}
			out = append(out, c1)
			continue
		}
		if c1 == '\'' {
			if libascii.IsAlpha(prevc) {
				out = append(out, []byte(htmlSymbolApostrophe)...)
				continue
			}
			out = append(out, c1)
			continue
		}
		out = append(out, c1)
	}
	return out
}

// htmlSubsMacro substitutes macro with its HTML markup.
func htmlSubsMacro(doc *Document, input []byte, isInlinePass bool) (out []byte) {
	var (
		el        *element
		bb        bytes.Buffer
		macroName string
		x         int
		n         int
		c         byte
	)

	for x < len(input) {
		c = input[x]
		if c != ':' {
			out = append(out, c)
			x++
			continue
		}

		macroName = parseMacroName(input[:x])
		if len(macroName) == 0 {
			out = append(out, c)
			x++
			continue
		}

		switch macroName {
		case macroFootnote:
			el, n = parseMacroFootnote(doc, input[x+1:])
			if el == nil {
				out = append(out, c)
				x++
				continue
			}
			x += n
			n = len(out)
			out = out[:n-len(macroName)] // Undo the macro name
			bb.Reset()
			htmlWriteFootnote(el, &bb)
			out = append(out, bb.Bytes()...)

		case macroFTP, macroHTTPS, macroHTTP, macroIRC, macroLink, macroMailto:
			el, n = parseURL(doc, macroName, input[x+1:])
			if el == nil {
				out = append(out, c)
				x++
				continue
			}
			x += n
			n = len(out)
			out = out[:n-len(macroName)]
			bb.Reset()
			htmlWriteURLBegin(el, &bb)
			if el.child != nil {
				el.child.toHTML(doc, &bb)
			}
			htmlWriteURLEnd(&bb)
			out = append(out, bb.Bytes()...)

		case macroImage:
			el, n = parseInlineImage(doc, input[x+1:])
			if el == nil {
				out = append(out, c)
				x++
				continue
			}
			x += n
			n = len(out)
			out = out[:n-len(macroName)]
			bb.Reset()
			htmlWriteInlineImage(el, &bb)
			out = append(out, bb.Bytes()...)

		case macroPass:
			if isInlinePass {
				// Prevent recursive substitutions.
				out = append(out, c)
				x++
				continue
			}
			el, n = parseMacroPass(input[x+1:])
			if el == nil {
				out = append(out, c)
				x++
				continue
			}
			x += n
			n = len(out)
			out = out[:n-len(macroName)]
			bb.Reset()
			htmlWriteInlinePass(doc, el, &bb)
			out = append(out, bb.Bytes()...)

		default:
			out = append(out, c)
			x++
		}
	}
	return out
}

func htmlWriteBlockBegin(el *element, out io.Writer, addClass string) {
	fmt.Fprint(out, "\n<div")



@@ 560,6 1074,13 @@ func htmlWriteInlineImage(el *element, out io.Writer) {
	fmt.Fprint(out, `</span>`)
}

func htmlWriteInlinePass(doc *Document, el *element, out io.Writer) {
	var (
		text []byte = htmlSubs(doc, el)
	)
	fmt.Fprint(out, string(text))
}

func htmlWriteListDescription(el *element, out io.Writer) {
	var openTag string
	if el.isStyleQandA() {

M inline_parser.go => inline_parser.go +9 -3
@@ 743,6 743,14 @@ func (pi *inlineParser) parseMacro() bool {
		}
		pi.x += n
		pi.prev = 0

	case macroPass:
		el, n = parseMacroPass(pi.content[pi.x+1:])
		if el == nil {
			return false
		}
		pi.x += n
		pi.prev = 0
	}

	pi.current.raw = pi.current.raw[:len(pi.current.raw)-len(name)]


@@ 934,8 942,6 @@ func (pi *inlineParser) parseSuperscript() bool {

// parseURL parser the URL, an optional text, optional attribute for target,
// and optional role.
//
// The current state of p.x is equal to ":".
func parseURL(doc *Document, scheme string, content []byte) (el *element, n int) {
	var (
		x   int


@@ 1066,7 1072,7 @@ func (pi *inlineParser) terminate(kind int, style int64) {

// indexByteUnescape find the index of the first unescaped byte `c` on
// slice of byte `in`.
// It will return nil and -1 if no unescape byte `c` found.
// It will return nil and -1 if no unescaped byte `c` found.
func indexByteUnescape(in []byte, c byte) (out []byte, idx int) {
	var (
		x     int

M inline_parser_test.go => inline_parser_test.go +48 -0
@@ 6,6 6,7 @@ package asciidoctor
import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"github.com/shuLhan/share/lib/test"


@@ 132,3 133,50 @@ func TestInlineParser_macro_footnote(t *testing.T) {
		got.Reset()
	}
}

func TestInlineParser_macro_pass(t *testing.T) {
	var (
		testFiles = []string{
			`testdata/inline_parser/macro_pass_none_test.txt`,
			`testdata/inline_parser/macro_pass_c_test.txt`,
			`testdata/inline_parser/macro_pass_q_test.txt`,
			`testdata/inline_parser/macro_pass_a_test.txt`,
			`testdata/inline_parser/macro_pass_r_test.txt`,
			`testdata/inline_parser/macro_pass_m_test.txt`,
		}

		testFile   string
		inputName  string
		outputName 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)
		}

		for inputName = range tdata.Input {
			t.Logf(`%s: %s`, testFile, inputName)

			outputName = strings.Replace(inputName, `.adoc`, `.html`, 1)

			doc = Parse(tdata.Input[inputName])

			got.Reset()
			err = doc.ToHTMLEmbedded(&got)
			if err != nil {
				t.Fatalf(`%s: %s`, inputName, err)
			}

			exp = tdata.Output[outputName]

			test.Assert(t, inputName, string(exp), got.String())
		}
	}
}

M macro.go => macro.go +70 -1
@@ 9,6 9,7 @@ import (
	"github.com/shuLhan/share/lib/ascii"
)

// List of macro names.
const (
	macroFTP      = `ftp`
	macroFootnote = `footnote`


@@ 18,6 19,7 @@ const (
	macroImage    = `image`
	macroLink     = `link`
	macroMailto   = `mailto`
	macroPass     = `pass`
)

var (


@@ 30,6 32,7 @@ var (
		macroImage:    elKindInlineImage,
		macroLink:     elKindURL,
		macroMailto:   elKindURL,
		macroPass:     elKindText,
	}
)



@@ 44,7 47,7 @@ type macro struct {
	// val represent the text for URL or image and footnote.
	rawContent []byte

	// level represent footnoted index number.
	// level represent footnote index number.
	level int
}



@@ 158,3 161,69 @@ func parseMacroFootnote(doc *Document, text []byte) (el *element, n int) {

	return el, n
}

// parseMacroPass parse the macro for passthrough.
//
//	"pass:" *(SUB) "[" TEXT "]"
//
//	SUB      = SUB_KIND *("," SUB_KIND)
//
//	SUB_KIND = "c" / "q" / "a" / "r" / "m" / "p" / "n" / "v"
func parseMacroPass(text []byte) (el *element, n int) {
	var (
		x int
		c byte
	)

	el = &element{
		kind: elKindInlinePass,
	}

	// Consume the substitutions until "[" or spaces.
	// Spaces automatically stop the process.
	// Other characters except the sub kinds are ignored.
	for ; x < len(text); x++ {
		c = text[x]
		if c == '[' {
			break
		}
		if c == ',' {
			continue
		}
		if ascii.IsSpace(c) {
			return nil, 0
		}
		switch c {
		case 'c':
			el.applySubs |= passSubChar
		case 'q':
			el.applySubs |= passSubQuote
		case 'a':
			el.applySubs |= passSubAttr
		case 'r':
			el.applySubs |= passSubRepl
		case 'm':
			el.applySubs |= passSubMacro
		case 'p':
			el.applySubs |= passSubPostRepl
		case 'n':
			el.applySubs |= passSubNormal
		case 'v':
			el.applySubs |= passSubChar
		}
	}
	if c != '[' {
		return nil, 0
	}
	x++
	n = x

	el.raw, x = parseClosedBracket(text[x:], '[', ']')
	if x < 0 {
		return nil, 0
	}

	n += x + 2

	return el, n
}

M parser.go => parser.go +62 -0
@@ 40,6 40,7 @@ const (
	elKindInlineID                   // "[[" REF_ID "]]" TEXT
	elKindInlineIDShort              // "[#" REF_ID "]#" TEXT "#"
	elKindInlineImage                // Inline macro for "image:"
	elKindInlinePass                 // Inline macro for passthrough "pass:"
	elKindInlineParagraph            //
	elKindListOrdered                // Wrapper.
	elKindListOrderedItem            // 30: Line start with ". "


@@ 637,6 638,67 @@ func parseAttrRef(doc *Document, content []byte, x int) (newContent []byte, ok b
	return newContent, true
}

// parseClosedBracket parse the text in input until we found the last close
// bracket.
// It will skip any open-close brackets inside input.
// For example, parsing ("test:[]]", '[', ']') will return ("test:[]", 7).
//
// If no closed bracket found it will return (nil, -1).
func parseClosedBracket(input []byte, openb, closedb byte) (out []byte, idx int) {
	var (
		openCount int
		c         byte
		isEsc     bool
	)

	out = make([]byte, 0, len(input))

	for idx, c = range input {
		if c == '\\' {
			if isEsc {
				out = append(out, '\\')
				isEsc = false
			} else {
				isEsc = true
			}
			continue
		}

		if c == closedb {
			if isEsc {
				out = append(out, c)
				isEsc = false
				continue
			}
			if openCount == 0 {
				return out, idx
			}
			openCount--
			out = append(out, c)
			continue
		}

		if c == openb {
			out = append(out, c)
			if isEsc {
				isEsc = false
			} else {
				openCount++
			}
			continue
		}

		if isEsc {
			out = append(out, '\\')
			isEsc = false
		}
		out = append(out, c)
	}

	// No closed bracket found.
	return nil, -1
}

// 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 []byte) (id, label []byte) {

M parser_test.go => parser_test.go +45 -0
@@ 87,6 87,51 @@ func TestGenerateID(t *testing.T) {
	}
}

func TestParseClosedBracket(t *testing.T) {
	type testCase struct {
		input  string
		expOut string
		expIdx int
	}

	var cases = []testCase{{
		input:  `test:[]] input`,
		expOut: `test:[]`,
		expIdx: 7,
	}, {
		input:  `[test:[]]] input`,
		expOut: `[test:[]]`,
		expIdx: 9,
	}, {
		input:  `[test:[]] input`,
		expOut: ``,
		expIdx: -1,
	}, {
		input:  `test:\[\]] input`,
		expOut: `test:[]`,
		expIdx: 9,
	}, {
		input:  `test:\x\]] input`,
		expOut: `test:\x]`,
		expIdx: 9,
	}}

	var (
		c      testCase
		got    []byte
		gotIdx int
	)

	for _, c = range cases {
		t.Logf(`input: %s`, c.input)

		got, gotIdx = parseClosedBracket([]byte(c.input), '[', ']')

		test.Assert(t, `got`, c.expOut, string(got))
		test.Assert(t, `got index`, c.expIdx, gotIdx)
	}
}

func TestIsValidID(t *testing.T) {
	type testCase struct {
		id  string

A testdata/inline_parser/macro_pass_a_test.txt => testdata/inline_parser/macro_pass_a_test.txt +13 -0
@@ 0,0 1,13 @@
Test macro pass with attribute substitutions only.

>>> pass_a.adoc
:meta-a: meta A
:meta-b: meta B

pass:a[attributes: {meta-A}, {meta-b}, and {meta-not_exist}].

<<< pass_a.html

<div class="paragraph">
<p>attributes: meta A, meta B, and {meta-not_exist}.</p>
</div>

A testdata/inline_parser/macro_pass_c_test.txt => testdata/inline_parser/macro_pass_c_test.txt +16 -0
@@ 0,0 1,16 @@
Test macro pass with special character substitutions only.

>>> pass_c.adoc

pass:c[char: < > &].

pass:c[replacement: (C) (R) (TM) -- ... -> => <- <= user's input].

<<< pass_c.html

<div class="paragraph">
<p>char: &lt; &gt; &amp;.</p>
</div>
<div class="paragraph">
<p>replacement: (C) (R) (TM) -- ... -&gt; =&gt; &lt;- &lt;= user's input.</p>
</div>

A testdata/inline_parser/macro_pass_m_test.txt => testdata/inline_parser/macro_pass_m_test.txt +32 -0
@@ 0,0 1,32 @@
Test macro pass with macro only.

>>> pass_m.adoc

pass:m[Text with footnote:id[footnote]].

pass:m[Text with http://127.0.0.1[HTTP URL]].

pass:m[Text with image:test.jpg[image]].

pass:m[Text with pass:[_none_] and pass:c[<_char_>]].

<<< pass_m.html

<div class="paragraph">
<p>Text with <sup class="footnote" id="_footnote_id">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>.</p>
</div>
<div class="paragraph">
<p>Text with <a href="http://127.0.0.1">HTTP URL</a>.</p>
</div>
<div class="paragraph">
<p>Text with <span class="image"><img src="test.jpg" alt="image"></span>.</p>
</div>
<div class="paragraph">
<p>Text with pass:[_none_] and pass:c[<_char_>].</p>
</div>
<div id="footnotes">
<hr>
<div class="footnote" id="_footnotedef_1">
<a href="#_footnoteref_1">1</a>. footnote
</div>
</div>

A testdata/inline_parser/macro_pass_none_test.txt => testdata/inline_parser/macro_pass_none_test.txt +29 -0
@@ 0,0 1,29 @@

>>> pass_none.adoc

pass:[char: < > &].

pass:[quote: _emphasis_, *strong*],
pass:[`monospace`, ^superscript^, ~subscript~],
pass:["`double curved quotes`", and '`single curved quotes`'].

pass:[attributes: {meta-A}, {meta-b}, and {meta-not_exist}].

pass:[replacement: (C) (R) (TM) -- ... -> => <- <= user's input].

<<< pass_none.html

<div class="paragraph">
<p>char: < > &.</p>
</div>
<div class="paragraph">
<p>quote: _emphasis_, *strong*,
`monospace`, ^superscript^, ~subscript~,
"`double curved quotes`", and '`single curved quotes`'.</p>
</div>
<div class="paragraph">
<p>attributes: {meta-A}, {meta-b}, and {meta-not_exist}.</p>
</div>
<div class="paragraph">
<p>replacement: (C) (R) (TM) -- ... -> => <- <= user's input.</p>
</div>

A testdata/inline_parser/macro_pass_q_test.txt => testdata/inline_parser/macro_pass_q_test.txt +15 -0
@@ 0,0 1,15 @@
Test macro pass with inline markup substitutions only.

>>> pass_q.adoc

pass:q[quote: _emphasis_, *strong*],
pass:q[`monospace`, ^superscript^, ~subscript~],
pass:q["`double curved quotes`", and '`single curved quotes`'].

<<< pass_q.html

<div class="paragraph">
<p>quote: <em>emphasis</em>, <strong>strong</strong>,
<code>monospace</code>, <sup>superscript</sup>, <sub>subscript</sub>,
&#8220;double curved quotes&#8221;, and &#8216;single curved quotes&#8217;.</p>
</div>

A testdata/inline_parser/macro_pass_r_test.txt => testdata/inline_parser/macro_pass_r_test.txt +18 -0
@@ 0,0 1,18 @@
Test macro pass with special replacements only.

>>> pass_r.adoc

pass:r[char: < > &].

pass:r[replacement: (C) (R) (TM) -- ...]
pass:r[-> => <- <= user's input].

<<< pass_r.html

<div class="paragraph">
<p>char: < > &.</p>
</div>
<div class="paragraph">
<p>replacement: &#169; &#174; &#8482;&#8201;&#8212;&#8201;&#8230;
&#8594; &#8658; &#8592; &#8656; user&#8217;s input.</p>
</div>