Skip to content

Commit

Permalink
feature(types/parser/renderer): Support for counters (#715)
Browse files Browse the repository at this point in the history
This adds support for {counter:name} and {counter2:name}
as attribute substitutions that reference the named counters.

The existing table, image, etc. counters are not converted to use
these yet, but it's likely that doing so will help once attribute
substitution is supported.

Note that when that work is done, care must be taken to ensure that
the counter is incremented when expanded to it's literal value, not
just expanded from the built-in value.

This includes support for setting the start including alpha counters
(English letters only, same as asciidoctor).

Fixes #714
  • Loading branch information
gdamore authored Jul 12, 2020
1 parent a6273dd commit b9e82cd
Show file tree
Hide file tree
Showing 9 changed files with 4,033 additions and 3,424 deletions.
1 change: 1 addition & 0 deletions pkg/parser/document_processing.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func ParseRawDocument(r io.Reader, config configuration.Configuration, options .
attrs := types.AttributesWithOverrides{
Content: map[string]interface{}{},
Overrides: map[string]string{},
Counters: map[string]interface{}{},
}
if doc.Blocks, err = processFileInclusions(doc.Blocks, attrs, []levelOffset{}, config, options...); err != nil {
return types.RawDocument{}, err
Expand Down
56 changes: 54 additions & 2 deletions pkg/parser/document_processing_apply_substitutions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package parser
import (
"fmt"
"io"
"strconv"
"strings"

"github.com/bytesparadise/libasciidoc/pkg/configuration"
Expand All @@ -22,6 +23,7 @@ func ApplySubstitutions(rawDoc types.RawDocument, config configuration.Configura
attrs := types.AttributesWithOverrides{
Content: types.Attributes{},
Overrides: config.AttributeOverrides,
Counters: map[string]interface{}{},
}
// also, add all front-matter key/values
attrs.Add(rawDoc.FrontMatter.Content)
Expand Down Expand Up @@ -74,6 +76,54 @@ func applyAttributeSubstitutions(elements []interface{}, attrs types.AttributesW

}

// applyCounterSubstitutions is called by applyAttributeSubstitutionsOnElement. Unless there is an error with
// the element (the counter is the wrong type, which should never occur), it will return a StringElement, true
// (because we always either find the element, or allocate one), and nil. On an error it will return nil, false,
// and the error. The extra boolean here is to fit the calling expectations of our caller. This function was
// factored out of a case from applyAttributeSubstitutionsOnElement in order to reduce the complexity of that
// function, but otherwise it should have no callers.
func applyCounterSubstitution(c types.CounterSubstitution, attrs types.AttributesWithOverrides) (interface{}, bool, error) {
counter := attrs.Counters[c.Name]
if counter == nil {
counter = 0
}
increment := true
if c.Value != nil {
attrs.Counters[c.Name] = c.Value
counter = c.Value
increment = false
}
switch counter := counter.(type) {
case int:
if increment {
counter++
}
attrs.Counters[c.Name] = counter
if c.Hidden {
// return empty string facilitates merging
return types.StringElement{Content: ""}, true, nil
}
return types.StringElement{
Content: strconv.Itoa(counter),
}, true, nil
case rune:
if increment {
counter++
}
attrs.Counters[c.Name] = counter
if c.Hidden {
// return empty string facilitates merging
return types.StringElement{Content: ""}, true, nil
}
return types.StringElement{
Content: string(counter),
}, true, nil

default:
return nil, false, fmt.Errorf("invalid counter type %T", counter)
}

}
func applyAttributeSubstitutionsOnElement(element interface{}, attrs types.AttributesWithOverrides) (interface{}, bool, error) {
switch e := element.(type) {
case types.AttributeDeclaration:
Expand All @@ -92,6 +142,8 @@ func applyAttributeSubstitutionsOnElement(element interface{}, attrs types.Attri
return types.StringElement{
Content: "{" + e.Name + "}",
}, false, nil
case types.CounterSubstitution:
return applyCounterSubstitution(e, attrs)
case types.ImageBlock:
return e.ResolveLocation(attrs), false, nil
case types.InlineImage:
Expand Down Expand Up @@ -193,7 +245,7 @@ func parseInlineLinks(elements []interface{}) ([]interface{}, error) {
// Block substitutions
// ----------------------------------------------------------------------------

// applyBlockSubstitutions applies the subtitutions on paragraphs and delimited blocks (including when in continued list elements)
// applyBlockSubstitutions applies the substitutions on paragraphs and delimited blocks (including when in continued list elements)
func applyBlockSubstitutions(elements []interface{}, config configuration.Configuration, options ...Option) ([]interface{}, error) {
log.Debug("apply block substitutions")
if len(elements) == 0 {
Expand Down Expand Up @@ -436,7 +488,7 @@ func applyParagraphSubstitutions(lines []interface{}, sub paragraphSubstitution)

type paragraphSubstitution func(lines []interface{}, options ...Option) ([]interface{}, error)

func normalParagraph(options ...Option) paragraphSubstitution {
func normalParagraph(_ ...Option) paragraphSubstitution {
return func(lines []interface{}, options ...Option) ([]interface{}, error) {
log.Debugf("applying the 'normal' substitution on a paragraph")
raw, err := serializeParagraph(lines)
Expand Down
68 changes: 68 additions & 0 deletions pkg/parser/document_processing_apply_substitutions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,72 @@ var _ = Describe("document attribute subsititutions", func() {
})
})

Context("counters", func() {

It("should start at one", func() {
// given
elements := []interface{}{
types.CounterSubstitution{
Name: "foo",
},
}
result, err := applyAttributeSubstitutions(elements, types.AttributesWithOverrides{
Content: map[string]interface{}{},
Overrides: map[string]string{},
Counters: map[string]interface{}{},
})
Expect(err).To(Not(HaveOccurred()))
Expect(result).To(Equal([]interface{}{ // at this stage, AttributeDeclaration and AttributeReset are still present
types.StringElement{
Content: "1",
},
}))
})

It("should increment correctly", func() {
// given
elements := []interface{}{
types.CounterSubstitution{
Name: "foo",
},
types.CounterSubstitution{
Name: "bar",
},
types.CounterSubstitution{
Name: "foo",
},
types.CounterSubstitution{
Name: "alpha",
Value: 'a',
Hidden: true,
},
types.CounterSubstitution{
Name: "alpha",
},
types.CounterSubstitution{
Name: "set",
Value: 33,
Hidden: true,
},
types.CounterSubstitution{
Name: "set",
Hidden: true,
},
types.CounterSubstitution{
Name: "set",
},
}
result, err := applyAttributeSubstitutions(elements, types.AttributesWithOverrides{
Content: map[string]interface{}{},
Overrides: map[string]string{},
Counters: map[string]interface{}{},
})
Expect(err).To(Not(HaveOccurred()))
Expect(result).To(Equal([]interface{}{ // at this stage, AttributeDeclaration and AttributeReset are still present
types.StringElement{
Content: "112b35", // elements get concatenated
},
}))
})
})
})
2 changes: 1 addition & 1 deletion pkg/parser/document_processing_filter_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ var emptyPreambleMatcher filterMatcher = func(element interface{}) bool {
// a AttributeSubstitution, a AttributeReset or a standalone Attribute
var attributeMatcher filterMatcher = func(element interface{}) bool {
switch element.(type) {
case types.AttributeDeclaration, types.AttributeSubstitution, types.AttributeReset, types.Attributes:
case types.AttributeDeclaration, types.AttributeSubstitution, types.AttributeReset, types.Attributes, types.CounterSubstitution:
return true
default:
return false
Expand Down
16 changes: 16 additions & 0 deletions pkg/parser/paragraph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,22 @@ foo`
}
Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected))
})

It("with counters", func() {
source := `foo{counter:foo} bar{counter2:foo} baz{counter:foo} bob{counter:bob}`
expected := types.DraftDocument{
Blocks: []interface{}{
types.Paragraph{
Lines: []interface{}{
[]interface{}{
types.StringElement{Content: "foo1 bar baz3 bob1"},
},
},
},
},
}
Expect(ParseDraftDocument(source)).To(MatchDraftDocument(expected))
})
})

Context("admonition paragraphs", func() {
Expand Down
Loading

0 comments on commit b9e82cd

Please sign in to comment.