diff options
| author | Shulhan <ms@kilabit.info> | 2024-08-15 00:59:58 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2024-08-16 00:18:55 +0700 |
| commit | 836385a4ad7bc9a914c9a8544901518f0a64f2ed (patch) | |
| tree | f6f859636b60933be93de2fac718e2e5b54b0fe8 | |
| parent | 64bd8146914636d395ecd50bf2623b5353976521 (diff) | |
| download | asciidoctor-go-836385a4ad7bc9a914c9a8544901518f0a64f2ed.tar.xz | |
all: support document attribute "leveloffset"
The ":leveloffset:" on document attribute allow increment
or decrement the heading level on included files.
Reference: https://docs.asciidoctor.org/asciidoc/latest/directives/include-with-leveloffset/
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | document_attribute.go | 31 | ||||
| -rw-r--r-- | document_attribute_test.go | 100 | ||||
| -rw-r--r-- | document_parser.go | 265 | ||||
| -rw-r--r-- | element.go | 2 | ||||
| -rw-r--r-- | parser.go | 213 | ||||
| -rw-r--r-- | testdata/_includes/section.adoc | 3 | ||||
| -rw-r--r-- | testdata/leveloffset_test.txt | 17 |
8 files changed, 404 insertions, 230 deletions
@@ -126,6 +126,8 @@ Supported document attribute references, * `idseparator` * `lastname(_x)` * `last-update-label` +* [`leveloffset`](https://docs.asciidoctor.org/asciidoc/latest/directives/include-with-leveloffset/). +Only on document attributes, not on include directive. * `middlename(_x)` * `nofooter` * `noheader` @@ -216,7 +218,6 @@ List of features which may be implemented, * Cross References * Inter-document Cross References * Include Directive - * Partitioning large documents and using leveloffset * AsciiDoc vs non-AsciiDoc files * Normalize Block Indentation * Include a File Multiple Times in the Same Document diff --git a/document_attribute.go b/document_attribute.go index 357a0e0..7d40a50 100644 --- a/document_attribute.go +++ b/document_attribute.go @@ -3,7 +3,11 @@ package asciidoctor -import "strings" +import ( + "fmt" + "strconv" + "strings" +) // List of document attribute. const ( @@ -22,6 +26,7 @@ const ( docAttrLastName = `lastname` docAttrLastUpdateLabel = `last-update-label` docAttrLastUpdateValue = `last-update-value` + docAttrLevelOffset = `leveloffset` docAttrMiddleName = `middlename` docAttrNoFooter = `nofooter` docAttrNoHeader = `noheader` @@ -57,7 +62,8 @@ const ( // DocumentAttribute contains the mapping of global attribute keys in the // headers with its value. type DocumentAttribute struct { - Entry map[string]string + Entry map[string]string + LevelOffset int } func newDocumentAttribute() DocumentAttribute { @@ -74,18 +80,33 @@ func newDocumentAttribute() DocumentAttribute { } } -func (docAttr *DocumentAttribute) apply(key, val string) { +func (docAttr *DocumentAttribute) apply(key, val string) (err error) { if key[0] == '!' { key = strings.TrimSpace(key[1:]) delete(docAttr.Entry, key) - return + return nil } var n = len(key) if key[n-1] == '!' { key = strings.TrimSpace(key[:n-1]) delete(docAttr.Entry, key) - return + return nil + } + + if key == docAttrLevelOffset { + var offset int64 + offset, err = strconv.ParseInt(val, 10, 32) + if err != nil { + return fmt.Errorf(`DocumentAttribute: %s: invalid value %q`, key, val) + } + if val[0] == '+' || val[0] == '-' { + docAttr.LevelOffset += int(offset) + goto valid + } + docAttr.LevelOffset = int(offset) } +valid: docAttr.Entry[key] = val + return nil } diff --git a/document_attribute_test.go b/document_attribute_test.go new file mode 100644 index 0000000..cec898e --- /dev/null +++ b/document_attribute_test.go @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2024 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later + +package asciidoctor + +import ( + "testing" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestDocumentAttributeApply(t *testing.T) { + type testCase struct { + desc string + key string + val string + expError string + exp DocumentAttribute + } + + var docAttr = DocumentAttribute{ + Entry: map[string]string{ + `key1`: ``, + `key2`: ``, + }, + } + + var listCase = []testCase{{ + key: `key3`, + exp: docAttr, + }, { + desc: `prefix negation`, + key: `!key1`, + exp: DocumentAttribute{ + Entry: map[string]string{ + `key2`: ``, + `key3`: ``, + }, + }, + }, { + desc: `suffix negation`, + key: `key2!`, + exp: DocumentAttribute{ + Entry: map[string]string{ + `key3`: ``, + }, + }, + }, { + desc: `leveloffset +`, + key: docAttrLevelOffset, + val: `+2`, + exp: DocumentAttribute{ + Entry: map[string]string{ + `key3`: ``, + `leveloffset`: `+2`, + }, + LevelOffset: 2, + }, + }, { + desc: `leveloffset -`, + key: docAttrLevelOffset, + val: `-2`, + exp: DocumentAttribute{ + Entry: map[string]string{ + `key3`: ``, + `leveloffset`: `-2`, + }, + LevelOffset: 0, + }, + }, { + desc: `leveloffset`, + key: docAttrLevelOffset, + val: `1`, + exp: DocumentAttribute{ + Entry: map[string]string{ + `key3`: ``, + `leveloffset`: `1`, + }, + LevelOffset: 1, + }, + }, { + desc: `leveloffset: invalid`, + key: docAttrLevelOffset, + val: `*1`, + expError: `DocumentAttribute: leveloffset: invalid value "*1"`, + }} + + var ( + tc testCase + err error + ) + for _, tc = range listCase { + err = docAttr.apply(tc.key, tc.val) + if err != nil { + test.Assert(t, `apply: `+tc.desc, tc.expError, err.Error()) + continue + } + test.Assert(t, `apply: `+tc.desc, tc.exp, docAttr) + } +} diff --git a/document_parser.go b/document_parser.go index e1acb4b..96c37db 100644 --- a/document_parser.go +++ b/document_parser.go @@ -141,19 +141,20 @@ func (docp *documentParser) hasPreamble() bool { notEmtpy int line []byte - kind int ) for ; start < len(docp.lines); start++ { line = docp.lines[start] if len(line) == 0 { continue } - kind, _, _ = whatKindOfLine(line) - if kind == elKindSectionL1 || kind == elKindSectionL2 || - kind == elKindSectionL3 || kind == elKindSectionL4 || - kind == elKindSectionL5 || - kind == lineKindID || - kind == lineKindIDShort { + _, _ = docp.whatKindOfLine(line) + if docp.kind == elKindSectionL1 || + docp.kind == elKindSectionL2 || + docp.kind == elKindSectionL3 || + docp.kind == elKindSectionL4 || + docp.kind == elKindSectionL5 || + docp.kind == lineKindID || + docp.kind == lineKindIDShort { return notEmtpy > 0 } notEmtpy++ @@ -202,7 +203,7 @@ func (docp *documentParser) line(logp string) (spaces, line []byte, ok bool) { } docp.lineNum++ - docp.kind, spaces, line = whatKindOfLine(line) + spaces, line = docp.whatKindOfLine(line) return spaces, line, true } @@ -397,7 +398,7 @@ func (docp *documentParser) parseBlock(parent *element, term int) { } el.Attrs[key] = value } else { - docp.doc.Attributes.apply(key, value) + _ = docp.doc.Attributes.apply(key, value) parent.addChild(&element{ kind: docp.kind, key: key, @@ -749,7 +750,7 @@ func (docp *documentParser) parseHeader() { var key, value string key, value, ok = docp.parseAttribute(line, false) if ok { - docp.doc.Attributes.apply(key, value) + _ = docp.doc.Attributes.apply(key, value) } line = nil continue @@ -1599,3 +1600,247 @@ func (docp *documentParser) skipCommentAndEmptyLine() (line []byte, ok bool) { } return line, true } + +// whatKindOfLine return the kind of line. +// It will return lineKindText if the line does not match with known syntax. +func (docp *documentParser) whatKindOfLine(line []byte) (spaces, got []byte) { + docp.kind = lineKindText + + line = bytes.TrimRight(line, " \f\n\r\t\v") + + // All of the comparison MUST be in order. + + if len(line) == 0 { + docp.kind = lineKindEmpty + return nil, line + } + if bytes.HasPrefix(line, []byte(`////`)) { + // Check for comment block first, since we use HasPrefix to + // check for single line comment. + docp.kind = lineKindBlockComment + return spaces, line + } + if bytes.HasPrefix(line, []byte(`//`)) { + // Use HasPrefix to allow single line comment without space, + // for example "//comment". + docp.kind = lineKindComment + return spaces, line + } + + var strline = string(line) + + switch strline { + case `'''`, `---`, `- - -`, `***`, `* * *`: + docp.kind = lineKindHorizontalRule + return spaces, line + case `<<<`: + docp.kind = lineKindPageBreak + return spaces, line + case `--`: + docp.kind = elKindBlockOpen + return spaces, line + case `____`: + docp.kind = elKindBlockExcerpts + return spaces, line + case `....`: + docp.kind = elKindBlockLiteral + return nil, line + case `++++`: + docp.kind = elKindBlockPassthrough + return spaces, line + case `****`: + docp.kind = elKindBlockSidebar + return nil, line + case `====`: + docp.kind = elKindBlockExample + return spaces, line + case `[listing]`: + docp.kind = elKindBlockListingNamed + return nil, line + case `[literal]`: + docp.kind = elKindBlockLiteralNamed + return nil, line + case `toc::[]`: + docp.kind = elKindMacroTOC + return spaces, line + } + + if bytes.HasPrefix(line, []byte(`|===`)) { + docp.kind = elKindTable + return nil, line + } + if bytes.HasPrefix(line, []byte(`image::`)) { + docp.kind = elKindBlockImage + return spaces, line + } + if bytes.HasPrefix(line, []byte(`include::`)) { + docp.kind = lineKindInclude + return nil, line + } + if bytes.HasPrefix(line, []byte(`video::`)) { + docp.kind = elKindBlockVideo + return nil, line + } + if bytes.HasPrefix(line, []byte(`audio::`)) { + docp.kind = elKindBlockAudio + return nil, line + } + if isAdmonition(line) { + docp.kind = lineKindAdmonition + return nil, line + } + + var ( + x int + r byte + hasSpace bool + ) + for x, r = range line { + if r == ' ' || r == '\t' { + hasSpace = true + continue + } + break + } + if hasSpace { + spaces = line[:x] + line = line[x:] + + // A line indented with space only allowed on list item, + // otherwise it would be set as literal paragraph. + + if isLineDescriptionItem(line) { + docp.kind = elKindListDescriptionItem + return spaces, line + } + + if line[0] != '*' && line[0] != '-' && line[0] != '.' { + docp.kind = elKindLiteralParagraph + return spaces, line + } + } + + switch line[0] { + case ':': + docp.kind = lineKindAttribute + case '[': + var ( + newline = bytes.TrimRight(line, " \t") + l = len(newline) + ) + + if newline[l-1] != ']' { + return nil, line + } + if l >= 5 { + // [[x]] + if newline[1] == '[' && newline[l-2] == ']' { + docp.kind = lineKindID + return nil, line + } + } + if l >= 4 { + // [#x] + if line[1] == '#' { + docp.kind = lineKindIDShort + return nil, line + } + // [.x] + if line[1] == '.' { + docp.kind = lineKindStyleClass + return nil, line + } + } + docp.kind = lineKindAttributeElement + return spaces, line + case '=', '#': + var subs = bytes.Fields(line) + + switch string(subs[0]) { + case `=`, `#`: + docp.kind = elKindSectionL0 + case `==`, `##`: + docp.kind = elKindSectionL1 + case `===`, `###`: + docp.kind = elKindSectionL2 + case `====`, `####`: + docp.kind = elKindSectionL3 + case `=====`, `#####`: + docp.kind = elKindSectionL4 + case `======`, `######`: + docp.kind = elKindSectionL5 + default: + return spaces, line + } + docp.kind += docp.doc.Attributes.LevelOffset + if docp.kind < elKindSectionL0 || docp.kind > elKindSectionL5 { + docp.kind = elKindText + } + return spaces, line + + case '.': + switch { + case len(line) <= 1: + docp.kind = lineKindText + case ascii.IsAlnum(line[1]): + docp.kind = lineKindBlockTitle + default: + x = 0 + for ; x < len(line); x++ { + if line[x] == '.' { + continue + } + if line[x] == ' ' || line[x] == '\t' { + docp.kind = elKindListOrderedItem + return spaces, line + } + } + } + case '*', '-': + if len(line) <= 1 { + return spaces, line + } + + var ( + listItemChar = line[0] + count = 0 + ) + x = 0 + for ; x < len(line); x++ { + if line[x] == listItemChar { + count++ + continue + } + if line[x] == ' ' || line[x] == '\t' { + docp.kind = elKindListUnorderedItem + return spaces, line + } + // Break on the first non-space, so from above + // condition we have, + // - item + // -- item + // --- item + // ---- // block listing + // --unknown // break here + break + } + if listItemChar == '-' && count == 4 && x == len(line) { + docp.kind = elKindBlockListing + } else { + docp.kind = lineKindText + } + return spaces, line + default: + switch string(line) { + case `+`: + docp.kind = lineKindListContinue + case `----`: + docp.kind = elKindBlockListing + default: + if isLineDescriptionItem(line) { + docp.kind = elKindListDescriptionItem + } + } + } + return spaces, line +} @@ -804,7 +804,7 @@ func (el *element) setStyleAdmonition(admName string) { func (el *element) toHTML(doc *Document, w io.Writer) { switch el.kind { case lineKindAttribute: - doc.Attributes.apply(el.key, el.value) + _ = doc.Attributes.apply(el.key, el.value) case elKindCrossReference: var ( @@ -654,216 +654,3 @@ func parseStyle(styleName string) (styleKind int64) { return 0 } - -// whatKindOfLine return the kind of line. -// It will return lineKindText if the line does not match with known syntax. -func whatKindOfLine(line []byte) (kind int, spaces, got []byte) { - kind = lineKindText - - line = bytes.TrimRight(line, " \f\n\r\t\v") - - // All of the comparison MUST be in order. - - if len(line) == 0 { - return lineKindEmpty, nil, line - } - if bytes.HasPrefix(line, []byte(`////`)) { - // Check for comment block first, since we use HasPrefix to - // check for single line comment. - return lineKindBlockComment, spaces, line - } - if bytes.HasPrefix(line, []byte(`//`)) { - // Use HasPrefix to allow single line comment without space, - // for example "//comment". - return lineKindComment, spaces, line - } - - var strline = string(line) - - switch strline { - case `'''`, `---`, `- - -`, `***`, `* * *`: - return lineKindHorizontalRule, spaces, line - case `<<<`: - return lineKindPageBreak, spaces, line - case `--`: - return elKindBlockOpen, spaces, line - case `____`: - return elKindBlockExcerpts, spaces, line - case `....`: - return elKindBlockLiteral, nil, line - case `++++`: - return elKindBlockPassthrough, spaces, line - case `****`: - return elKindBlockSidebar, nil, line - case `====`: - return elKindBlockExample, spaces, line - case `[listing]`: - return elKindBlockListingNamed, nil, line - case `[literal]`: - return elKindBlockLiteralNamed, nil, line - case `toc::[]`: - return elKindMacroTOC, spaces, line - } - - if bytes.HasPrefix(line, []byte(`|===`)) { - return elKindTable, nil, line - } - if bytes.HasPrefix(line, []byte(`image::`)) { - return elKindBlockImage, spaces, line - } - if bytes.HasPrefix(line, []byte(`include::`)) { - return lineKindInclude, nil, line - } - if bytes.HasPrefix(line, []byte(`video::`)) { - return elKindBlockVideo, nil, line - } - if bytes.HasPrefix(line, []byte(`audio::`)) { - return elKindBlockAudio, nil, line - } - if isAdmonition(line) { - return lineKindAdmonition, nil, line - } - - var ( - x int - r byte - hasSpace bool - ) - for x, r = range line { - if r == ' ' || r == '\t' { - hasSpace = true - continue - } - break - } - if hasSpace { - spaces = line[:x] - line = line[x:] - - // A line indented with space only allowed on list item, - // otherwise it would be set as literal paragraph. - - if isLineDescriptionItem(line) { - return elKindListDescriptionItem, spaces, line - } - - if line[0] != '*' && line[0] != '-' && line[0] != '.' { - return elKindLiteralParagraph, spaces, line - } - } - - switch line[0] { - case ':': - kind = lineKindAttribute - case '[': - var ( - newline = bytes.TrimRight(line, " \t") - l = len(newline) - ) - - if newline[l-1] != ']' { - return lineKindText, nil, line - } - if l >= 5 { - // [[x]] - if newline[1] == '[' && newline[l-2] == ']' { - return lineKindID, nil, line - } - } - if l >= 4 { - // [#x] - if line[1] == '#' { - return lineKindIDShort, nil, line - } - // [.x] - if line[1] == '.' { - return lineKindStyleClass, nil, line - } - } - return lineKindAttributeElement, spaces, line - case '=': - var subs = bytes.Fields(line) - - switch string(subs[0]) { - case `=`: - kind = elKindSectionL0 - case `==`: - kind = elKindSectionL1 - case `===`: - kind = elKindSectionL2 - case `====`: - kind = elKindSectionL3 - case `=====`: - kind = elKindSectionL4 - case `======`: - kind = elKindSectionL5 - } - return kind, spaces, line - - case '.': - switch { - case len(line) <= 1: - kind = lineKindText - case ascii.IsAlnum(line[1]): - kind = lineKindBlockTitle - default: - x = 0 - for ; x < len(line); x++ { - if line[x] == '.' { - continue - } - if line[x] == ' ' || line[x] == '\t' { - kind = elKindListOrderedItem - return kind, spaces, line - } - } - } - case '*', '-': - if len(line) <= 1 { - kind = lineKindText - return kind, spaces, line - } - - var ( - listItemChar = line[0] - count = 0 - ) - x = 0 - for ; x < len(line); x++ { - if line[x] == listItemChar { - count++ - continue - } - if line[x] == ' ' || line[x] == '\t' { - kind = elKindListUnorderedItem - return kind, spaces, line - } - // Break on the first non-space, so from above - // condition we have, - // - item - // -- item - // --- item - // ---- // block listing - // --unknown // break here - break - } - if listItemChar == '-' && count == 4 && x == len(line) { - kind = elKindBlockListing - } else { - kind = lineKindText - } - return kind, spaces, line - default: - switch string(line) { - case `+`: - kind = lineKindListContinue - case `----`: - kind = elKindBlockListing - default: - if isLineDescriptionItem(line) { - kind = elKindListDescriptionItem - } - } - } - return kind, spaces, line -} diff --git a/testdata/_includes/section.adoc b/testdata/_includes/section.adoc new file mode 100644 index 0000000..aa66f8c --- /dev/null +++ b/testdata/_includes/section.adoc @@ -0,0 +1,3 @@ += Section 1 + +This is included with leveloffset +1. diff --git a/testdata/leveloffset_test.txt b/testdata/leveloffset_test.txt new file mode 100644 index 0000000..61f5b95 --- /dev/null +++ b/testdata/leveloffset_test.txt @@ -0,0 +1,17 @@ +Test the ":leveloffset:" document attribute. + +>>> leveloffset +:leveloffset: +1 + +include::testdata/_includes/section.adoc[] + +<<< leveloffset + +<div class="sect1"> +<h2 id="section_1">Section 1</h2> +<div class="sectionbody"> +<div class="paragraph"> +<p>This is included with leveloffset +1.</p> +</div> +</div> +</div> |
