M README.md => README.md +2 -1
@@ 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
M document_attribute.go => document_attribute.go +26 -5
@@ 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
}
A document_attribute_test.go => document_attribute_test.go +100 -0
@@ 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)
+ }
+}
M document_parser.go => document_parser.go +255 -10
@@ 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
+}
M element.go => element.go +1 -1
@@ 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 (
M parser.go => parser.go +0 -213
@@ 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
-}
A testdata/_includes/section.adoc => testdata/_includes/section.adoc +3 -0
@@ 0,0 1,3 @@
+= Section 1
+
+This is included with leveloffset +1.
A testdata/leveloffset_test.txt => testdata/leveloffset_test.txt +17 -0
@@ 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>