diff --git a/README.adoc b/README.adoc
index c453e797..4788b9a4 100644
--- a/README.adoc
+++ b/README.adoc
@@ -58,21 +58,21 @@ Libasciidoc provides 2 functions to convert an Asciidoc content into HTML:
1. Converting an `io.Reader` into an HTML document:
- func ConvertToHTML(ctx context.Context, source io.Reader, output io.Writer, options renderer.Option...) (map[string]interface{}, error)
+ ConvertToHTML(r io.Reader, output io.Writer, config configuration.Configuration) (types.Metadata, error)
2. Converting a file (given its name) into an HTML document:
- func ConvertFileToHTML(ctx context.Context, filename string, output io.Writer, options renderer.Option...) (map[string]interface{}, error)
+ ConvertFileToHTML(output io.Writer, config configuration.Configuration) (types.Metadata, error)
-where the returned `map[string]interface{}` object contains the document's title which is not part of the generated HTML `
` part, as well as the other attributes of the source document.
+where the returned `types.Metadata` object contains the document's title which is not part of the generated HTML `` part, as well as the other attributes of the source document.
-For now, the sole option to pass as a last argument is `renderer.IncludeHeaderFooter` to include the `` and `` elements in the generated HTML document or not. Default is `false`, which means that only the `` part of the HTML document is generated.
+All options/settings are passed via the `config` parameter.
=== Macro definition
-The user can define a macro by calling `renderer.DefineMacro()` and passing return value to conversion functions.
+The user can define a macro by calling `renderer.WithMacroTemplate()` and passing return value to conversion functions.
-`renderer.DefineMacro()` defines a macro by the given name and associates the given template. The template is an implementation of `renderer.MacroTemplate` interface (ex. `text.Template`)
+`renderer.WithMacroTemplate()` defines a macro by the given name and associates the given template. The template is an implementation of `renderer.MacroTemplate` interface (ex. `text.Template`)
Libasciidoc calls `Execute()` method and passes `types.UserMacro` object to template when rendering.
@@ -85,7 +85,7 @@ var tmpl = template.Must(t.Parse(tmplStr))
output := &strings.Builder{}
content := strings.NewReader(`example::hello world[suffix=!!!!!]`)
-libasciidoc.ConvertToHTML(context.Background(), content, output, renderer.DefineMacro(tmpl.Name(), tmpl))
+libasciidoc.ConvertToHTML(context.Background(), content, output, renderer.WithMacroTemplate(tmpl.Name(), tmpl))
```
== How to contribute
diff --git a/cmd/libasciidoc/root_cmd.go b/cmd/libasciidoc/root_cmd.go
index a34ddf85..bcee4e2f 100644
--- a/cmd/libasciidoc/root_cmd.go
+++ b/cmd/libasciidoc/root_cmd.go
@@ -9,8 +9,8 @@ import (
"path/filepath"
"github.com/bytesparadise/libasciidoc"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
logsupport "github.com/bytesparadise/libasciidoc/pkg/log"
- "github.com/bytesparadise/libasciidoc/pkg/renderer"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -23,6 +23,7 @@ func NewRootCmd() *cobra.Command {
var outputName string
var logLevel string
var css string
+ var attributes []string
rootCmd := &cobra.Command{
Use: "libasciidoc [flags] FILE",
@@ -43,13 +44,19 @@ func NewRootCmd() *cobra.Command {
if len(args) == 0 {
return helpCommand.RunE(cmd, args)
}
- for _, source := range args {
- out, close := getOut(cmd, source, outputName)
+ attrs := parseAttributes(attributes)
+ for _, sourcePath := range args {
+ out, close := getOut(cmd, sourcePath, outputName)
if out != nil {
defer close()
- path, _ := filepath.Abs(source)
+ path, _ := filepath.Abs(sourcePath)
log.Debugf("Starting to process file %v", path)
- _, err := libasciidoc.ConvertFileToHTML(source, out, renderer.IncludeHeaderFooter(!noHeaderFooter), renderer.IncludeCSS(css))
+ config := configuration.NewConfiguration(
+ configuration.WithFilename(sourcePath),
+ configuration.WithAttributes(attrs),
+ configuration.WithCSS(css),
+ configuration.WithHeaderFooter(!noHeaderFooter))
+ _, err := libasciidoc.ConvertFileToHTML(out, config)
if err != nil {
return err
}
@@ -63,6 +70,7 @@ func NewRootCmd() *cobra.Command {
flags.StringVarP(&outputName, "out-file", "o", "", "output file (default: based on path of input file); use - to output to STDOUT")
flags.StringVar(&logLevel, "log", "warning", "log level to set [debug|info|warning|error|fatal|panic]")
flags.StringVar(&css, "css", "", "the path to the CSS file to link to the document")
+ flags.StringArrayVarP(&attributes, "attribute", "a", []string{}, "a document attribute to set in the form of name, name!, or name=value pair")
return rootCmd
}
@@ -78,7 +86,7 @@ func newCloseFileFunc(c io.Closer) closeFunc {
}
}
-func getOut(cmd *cobra.Command, source, outputName string) (io.Writer, closeFunc) {
+func getOut(cmd *cobra.Command, sourcePath, outputName string) (io.Writer, closeFunc) {
if outputName == "-" {
// outfile is STDOUT
return cmd.OutOrStdout(), defaultCloseFunc()
@@ -89,9 +97,9 @@ func getOut(cmd *cobra.Command, source, outputName string) (io.Writer, closeFunc
log.Warnf("Cannot create output file - %v, skipping", outputName)
}
return outfile, newCloseFileFunc(outfile)
- } else if source != "" {
- // outfile is based on source
- path, _ := filepath.Abs(source)
+ } else if sourcePath != "" {
+ // outfile is based on sourcePath
+ path, _ := filepath.Abs(sourcePath)
outname := strings.TrimSuffix(path, filepath.Ext(path)) + ".html"
outfile, err := os.Create(outname)
if err != nil {
@@ -102,3 +110,17 @@ func getOut(cmd *cobra.Command, source, outputName string) (io.Writer, closeFunc
}
return cmd.OutOrStdout(), defaultCloseFunc()
}
+
+// converts the `name`, `!name` and `name=value` into a map
+func parseAttributes(attributes []string) map[string]string {
+ result := make(map[string]string, len(attributes))
+ for _, attr := range attributes {
+ data := strings.Split(attr, "=")
+ if len(data) > 1 {
+ result[data[0]] = data[1]
+ } else {
+ result[data[0]] = ""
+ }
+ }
+ return result
+}
diff --git a/cmd/libasciidoc/root_cmd_test.go b/cmd/libasciidoc/root_cmd_test.go
index ef915ce3..1ddaf23e 100644
--- a/cmd/libasciidoc/root_cmd_test.go
+++ b/cmd/libasciidoc/root_cmd_test.go
@@ -11,7 +11,6 @@ import (
)
var _ = Describe("root cmd", func() {
- RegisterFailHandler(Fail)
It("render with STDOUT output", func() {
// given
@@ -68,6 +67,38 @@ var _ = Describe("root cmd", func() {
Expect(buf.String()).ToNot(ContainSubstring(`
`
- Expect(RenderHTML5Body(source, WithFilename("test.adoc"), renderer.LastUpdated(lastUpdated))).To(Equal(expected))
+ Expect(RenderHTML5Body(source, configuration.WithFilename("test.adoc"), configuration.WithLastUpdated(lastUpdated))).To(Equal(expected))
Expect(DocumentMetadata(source, lastUpdated)).To(Equal(types.Metadata{
Title: "",
- LastUpdated: lastUpdated.Format(renderer.LastUpdatedFormat),
+ LastUpdated: lastUpdated.Format(configuration.LastUpdatedFormat),
TableOfContents: types.TableOfContents{
Sections: []types.ToCSection{
{
@@ -245,7 +245,7 @@ a paragraph with _italic content_`
`
- Expect(RenderHTML5Body(source, WithFilename("tmp/foo.adoc"))).To(Equal(expectedContent))
+ Expect(RenderHTML5Body(source, configuration.WithFilename("tmp/foo.adoc"))).To(Equal(expectedContent))
})
})
@@ -281,7 +281,7 @@ Last updated {{.LastUpdated}}
filename := "test/includes/chapter-a.adoc"
stat, err := os.Stat(filename)
Expect(err).NotTo(HaveOccurred())
- Expect(RenderHTML5Document(filename, renderer.IncludeCSS("path/to/style.css"))).To(MatchHTML5Template(expectedContent, stat.ModTime()))
+ Expect(RenderHTML5Document(filename, configuration.WithCSS("path/to/style.css"), configuration.WithHeaderFooter(true))).To(MatchHTML5Template(expectedContent, stat.ModTime()))
})
})
diff --git a/pkg/configuration/configuration.go b/pkg/configuration/configuration.go
new file mode 100644
index 00000000..ff7bc10d
--- /dev/null
+++ b/pkg/configuration/configuration.go
@@ -0,0 +1,98 @@
+package configuration
+
+import (
+ "errors"
+ "time"
+)
+
+// NewConfiguration returns a new configuration
+func NewConfiguration(settings ...Setting) Configuration {
+ config := Configuration{
+ AttributeOverrides: make(map[string]string),
+ macros: make(map[string]MacroTemplate),
+ }
+ for _, set := range settings {
+ set(&config)
+ }
+ return config
+}
+
+// Configuration the configuration used when rendering a document
+type Configuration struct {
+ Filename string
+ AttributeOverrides map[string]string
+ LastUpdated time.Time
+ IncludeHeaderFooter bool
+ CSS string
+ macros map[string]MacroTemplate
+}
+
+// Clone return a clone of the current configuration
+func (c Configuration) Clone() Configuration {
+ return Configuration{
+ CSS: c.CSS,
+ AttributeOverrides: c.AttributeOverrides,
+ Filename: c.Filename,
+ IncludeHeaderFooter: c.IncludeHeaderFooter,
+ LastUpdated: c.LastUpdated,
+ }
+}
+
+// MacroTemplate finds and returns a user macro function by specified name.
+func (c Configuration) MacroTemplate(name string) (MacroTemplate, error) {
+ macro, ok := c.macros[name]
+ if ok {
+ return macro, nil
+ }
+ return nil, errors.New("unknown user macro: " + name)
+}
+
+const (
+ // LastUpdatedFormat key to the time format for the `last updated` document attribute
+ LastUpdatedFormat string = "2006-01-02 15:04:05 -0700"
+)
+
+// Setting a setting to customize the configuration used during parsing and rendering of a document
+type Setting func(config *Configuration)
+
+// WithLastUpdated function to set the `last updated` option in the renderer context (default is `time.Now()`)
+func WithLastUpdated(value time.Time) Setting {
+ return func(config *Configuration) {
+ config.LastUpdated = value
+ }
+}
+
+// WithAttributes function to set the `attribute overrides`
+func WithAttributes(attrs map[string]string) Setting {
+ return func(config *Configuration) {
+ config.AttributeOverrides = attrs
+ }
+}
+
+// WithHeaderFooter function to set the `include header/footer` setting in the config
+func WithHeaderFooter(value bool) Setting {
+ return func(config *Configuration) {
+ config.IncludeHeaderFooter = value
+ }
+}
+
+// WithCSS function to set the `css` setting in the config
+func WithCSS(href string) Setting {
+ return func(config *Configuration) {
+ config.CSS = href
+ }
+}
+
+// WithFilename function to set the `filename` setting in the config
+func WithFilename(filename string) Setting {
+ return func(config *Configuration) {
+ config.Filename = filename
+ }
+}
+
+// WithMacro defines the given template to a user macro with the given name
+func WithMacroTemplate(name string, t MacroTemplate) Setting {
+ return func(config *Configuration) {
+ config.macros[name] = t
+ }
+}
diff --git a/pkg/configuration/macro_template.go b/pkg/configuration/macro_template.go
new file mode 100644
index 00000000..9ca4bf4f
--- /dev/null
+++ b/pkg/configuration/macro_template.go
@@ -0,0 +1,8 @@
+package configuration
+
+import "io"
+
+// MacroTemplate an interface of template for user macro.
+type MacroTemplate interface {
+ Execute(wr io.Writer, data interface{}) error
+}
diff --git a/pkg/parser/document_preprocessing.go b/pkg/parser/document_preprocessing.go
index 37f67a8d..7e446244 100644
--- a/pkg/parser/document_preprocessing.go
+++ b/pkg/parser/document_preprocessing.go
@@ -3,6 +3,7 @@ package parser
import (
"io"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/types"
"github.com/davecgh/go-spew/spew"
@@ -16,19 +17,22 @@ type ContextKey string
const LevelOffset ContextKey = "leveloffset"
// ParseDraftDocument parses a document's content and applies the preprocessing directives (file inclusions)
-func ParseDraftDocument(filename string, r io.Reader, opts ...Option) (types.DraftDocument, error) {
- opts = append(opts, Entrypoint("AsciidocDocument"))
- return parseDraftDocument(filename, r, []levelOffset{}, opts...)
+func ParseDraftDocument(r io.Reader, config configuration.Configuration, options ...Option) (types.DraftDocument, error) {
+ options = append(options, Entrypoint("AsciidocDocument"))
+ return parseDraftDocument(r, []levelOffset{}, config, options...)
}
-func parseDraftDocument(filename string, r io.Reader, levelOffsets []levelOffset, opts ...Option) (types.DraftDocument, error) {
- d, err := ParseReader(filename, r, opts...)
+func parseDraftDocument(r io.Reader, levelOffsets []levelOffset, config configuration.Configuration, options ...Option) (types.DraftDocument, error) {
+ d, err := ParseReader(config.Filename, r, options...)
if err != nil {
return types.DraftDocument{}, err
}
doc := d.(types.DraftDocument)
- attrs := types.DocumentAttributes{}
- blocks, err := parseElements(filename, doc.Blocks, attrs, levelOffsets, opts...)
+ attrs := types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{},
+ Overrides: map[string]string{},
+ }
+ blocks, err := parseElements(doc.Blocks, attrs, levelOffsets, config, options...)
if err != nil {
return types.DraftDocument{}, err
}
@@ -41,25 +45,25 @@ func parseDraftDocument(filename string, r io.Reader, levelOffsets []levelOffset
}
// parseElements resolves the file inclusions if any is found in the given elements
-func parseElements(filename string, elements []interface{}, attrs types.DocumentAttributes, levelOffsets []levelOffset, opts ...Option) ([]interface{}, error) {
+func parseElements(elements []interface{}, attrs types.DocumentAttributesWithOverrides, levelOffsets []levelOffset, config configuration.Configuration, options ...Option) ([]interface{}, error) {
result := []interface{}{}
for _, e := range elements {
switch e := e.(type) {
case types.DocumentAttributeDeclaration:
- attrs[e.Name] = e.Value
+ attrs.Add(e.Name, e.Value)
result = append(result, e)
case types.FileInclusion:
// read the file and include its content
- embedded, err := parseFileToInclude(filename, e, attrs, levelOffsets, opts...)
+ embedded, err := parseFileToInclude(e, attrs, levelOffsets, config, options...)
if err != nil {
// do not fail, but instead report the error in the console
log.Errorf("failed to include file '%s': %v", e.Location, err)
}
result = append(result, embedded.Blocks...)
case types.DelimitedBlock:
- elmts, err := parseElements(filename, e.Elements, attrs, levelOffsets,
+ elmts, err := parseElements(e.Elements, attrs, levelOffsets, config,
// use a new var to avoid overridding the current one which needs to stay as-is for the rest of the doc parsing
- append(opts, Entrypoint("AsciidocDocumentWithinDelimitedBlock"))...)
+ append(options, Entrypoint("AsciidocDocumentWithinDelimitedBlock"))...)
if err != nil {
return nil, err
}
diff --git a/pkg/parser/document_processing.go b/pkg/parser/document_processing.go
index 3ac6ada0..4668e13a 100644
--- a/pkg/parser/document_processing.go
+++ b/pkg/parser/document_processing.go
@@ -3,35 +3,26 @@ package parser
import (
"io"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/types"
)
// ParseDocument parses the content of the reader identitied by the filename
-func ParseDocument(filename string, r io.Reader, opts ...Option) (types.Document, error) {
- draftDoc, err := ParseDraftDocument(filename, r, opts...)
+func ParseDocument(r io.Reader, config configuration.Configuration) (types.Document, error) {
+ draftDoc, err := ParseDraftDocument(r, config)
if err != nil {
return types.Document{}, err
}
- attrs := types.DocumentAttributes{}
- // add all predefined attributes
- for k, v := range Predefined {
- if v, ok := v.(string); ok {
- attrs[k] = v
- }
+ attrs := types.DocumentAttributesWithOverrides{
+ Content: types.DocumentAttributes{},
+ Overrides: config.AttributeOverrides,
}
-
+ // add all predefined attributes
+ attrs.AddAll(Predefined)
// also, add all front-matter key/values
- for k, v := range draftDoc.FrontMatter.Content {
- if v, ok := v.(string); ok {
- attrs[k] = v
- }
- }
-
+ attrs.AddAll(draftDoc.FrontMatter.Content)
// also, add all DocumentAttributeDeclaration at the top of the document
- documentAttributes := draftDoc.DocumentAttributes()
- for k, v := range documentAttributes {
- attrs[k] = v
- }
+ attrs.AddAll(draftDoc.DocumentAttributes())
// apply document attribute substitutions and re-parse paragraphs that were affected
blocks, _, err := applyDocumentAttributeSubstitutions(draftDoc.Blocks, attrs)
@@ -44,7 +35,7 @@ func ParseDocument(filename string, r io.Reader, opts ...Option) (types.Document
if err != nil {
return types.Document{}, err
}
- // apply document attribute substitutions and re-parse paragraphs that were affected
+ // filter out blocks not needed in the final doc
blocks = filter(blocks.([]interface{}), allMatchers...)
// now, rearrange elements in a hierarchical manner
@@ -57,8 +48,6 @@ func ParseDocument(filename string, r io.Reader, opts ...Option) (types.Document
doc.Attributes[k] = v
}
// and add all remaining attributes, too
- for k, v := range documentAttributes {
- doc.Attributes[k] = v
- }
+ doc.Attributes.AddAll(draftDoc.DocumentAttributes())
return doc, nil
}
diff --git a/pkg/parser/document_processing_apply_substitutions.go b/pkg/parser/document_processing_apply_substitutions.go
index 13e7b33c..d3c267ac 100644
--- a/pkg/parser/document_processing_apply_substitutions.go
+++ b/pkg/parser/document_processing_apply_substitutions.go
@@ -11,7 +11,7 @@ import (
// applyDocumentAttributeSubstitutions(elements applies the document attribute substitutions
// and re-parse the paragraphs that were affected
// nolint: gocyclo
-func applyDocumentAttributeSubstitutions(element interface{}, attrs types.DocumentAttributes) (interface{}, bool, error) {
+func applyDocumentAttributeSubstitutions(element interface{}, attrs types.DocumentAttributesWithOverrides) (interface{}, bool, error) {
// the document attributes, as they are resolved while processing the blocks
log.Debugf("applying document substitutions on block of type %T", element)
switch e := element.(type) {
@@ -33,13 +33,13 @@ func applyDocumentAttributeSubstitutions(element interface{}, attrs types.Docume
}
return elements, false, nil
case types.DocumentAttributeDeclaration:
- attrs[e.Name] = e.Value
+ attrs.Add(e.Name, e.Value)
return e, false, nil
case types.DocumentAttributeReset:
- delete(attrs, e.Name)
+ attrs.Delete(e.Name)
return e, false, nil
case types.DocumentAttributeSubstitution:
- if value, ok := attrs[e.Name].(string); ok {
+ if value, ok := attrs.GetAsString(e.Name); ok {
return types.StringElement{
Content: value,
}, true, nil
diff --git a/pkg/parser/document_processing_apply_substitutions_test.go b/pkg/parser/document_processing_apply_substitutions_test.go
index 11b24dad..73c9ab60 100644
--- a/pkg/parser/document_processing_apply_substitutions_test.go
+++ b/pkg/parser/document_processing_apply_substitutions_test.go
@@ -27,8 +27,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -65,8 +68,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -106,8 +112,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -147,7 +156,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{})
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{},
+ Overrides: map[string]string{},
+ })
+
// then
Expect(err).To(Not(HaveOccurred()))
Expect(applied).To(BeFalse())
@@ -186,7 +199,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{})
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{},
+ Overrides: map[string]string{},
+ })
+
// then
Expect(err).To(Not(HaveOccurred()))
Expect(applied).To(BeFalse())
@@ -231,10 +248,15 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "scheme": "https",
- "host": "foo.bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ "scheme": "https",
+ "host": "foo.bar",
+ },
+ Overrides: map[string]string{},
})
+
// then
Expect(err).To(Not(HaveOccurred()))
Expect(applied).To(BeTrue())
@@ -289,8 +311,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -335,8 +360,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -381,8 +409,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -430,8 +461,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -486,8 +520,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -535,8 +572,11 @@ var _ = Describe("document attribute subsititutions", func() {
},
}
// when
- result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributes{
- "foo": "bar",
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{},
})
// then
Expect(err).To(Not(HaveOccurred()))
@@ -552,4 +592,65 @@ var _ = Describe("document attribute subsititutions", func() {
}))
})
})
+
+ Context("attribute overrides", func() {
+
+ It("should replace with new StringElement on first position", func() {
+ // given
+ elements := []interface{}{
+ types.DocumentAttributeDeclaration{
+ Name: "foo",
+ Value: "foo",
+ },
+ types.DocumentAttributeReset{
+ Name: "foo",
+ },
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.DocumentAttributeSubstitution{
+ Name: "foo",
+ },
+ types.StringElement{
+ Content: " and more content.",
+ },
+ },
+ },
+ },
+ }
+ // when
+ result, applied, err := applyDocumentAttributeSubstitutions(elements, types.DocumentAttributesWithOverrides{
+ Content: map[string]interface{}{
+ "foo": "bar",
+ },
+ Overrides: map[string]string{
+ "foo": "BAR",
+ },
+ })
+ // then
+ Expect(err).To(Not(HaveOccurred()))
+ Expect(applied).To(BeTrue())
+ Expect(result).To(Equal([]interface{}{ // at this stage, DocumentAttributeDeclaration and DocumentAttributeReset are still present
+ types.DocumentAttributeDeclaration{
+ Name: "foo",
+ Value: "foo",
+ },
+ types.DocumentAttributeReset{
+ Name: "foo",
+ },
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "BAR and more content.",
+ },
+ },
+ },
+ },
+ }))
+ })
+ })
+
})
diff --git a/pkg/parser/file_inclusion.go b/pkg/parser/file_inclusion.go
index 2489f436..2f75ab8c 100644
--- a/pkg/parser/file_inclusion.go
+++ b/pkg/parser/file_inclusion.go
@@ -9,6 +9,7 @@ import (
"strings"
"text/template"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/types"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
@@ -55,33 +56,35 @@ func absoluteOffset(offset int) levelOffset {
}
}
-func parseFileToInclude(filename string, incl types.FileInclusion, attrs types.DocumentAttributes, levelOffsets []levelOffset, opts ...Option) (types.DraftDocument, error) {
+func parseFileToInclude(incl types.FileInclusion, attrs types.DocumentAttributesWithOverrides, levelOffsets []levelOffset, config configuration.Configuration, options ...Option) (types.DraftDocument, error) {
path := incl.Location.Resolve(attrs).String()
- currentDir := filepath.Dir(filename)
- log.Debugf("parsing '%s' from '%s' (%s)", path, currentDir, filename)
- log.Debugf("file inclusion attributes: %s", spew.Sdump(incl.Attributes))
+ currentDir := filepath.Dir(config.Filename)
+ if log.IsLevelEnabled(log.DebugLevel) {
+ log.Debugf("parsing '%s' from '%s' (%s)", path, currentDir, config.Filename)
+ log.Debugf("file inclusion attributes: %s", spew.Sdump(incl.Attributes))
+ }
f, absPath, done, err := open(filepath.Join(currentDir, path))
defer done()
if err != nil {
- return invalidFileErrMsg(filename, path, incl.RawText, err)
+ return invalidFileErrMsg(config.Filename, path, incl.RawText, err)
}
content := bytes.NewBuffer(nil)
scanner := bufio.NewScanner(bufio.NewReader(f))
if lineRanges, ok := incl.LineRanges(); ok {
if err := readWithinLines(scanner, content, lineRanges); err != nil {
- return invalidFileErrMsg(filename, path, incl.RawText, err)
+ return invalidFileErrMsg(config.Filename, path, incl.RawText, err)
}
} else if tagRanges, ok := incl.TagRanges(); ok {
if err := readWithinTags(path, scanner, content, tagRanges); err != nil {
- return invalidFileErrMsg(filename, path, incl.RawText, err)
+ return invalidFileErrMsg(config.Filename, path, incl.RawText, err)
}
} else {
if err := readAll(scanner, content); err != nil {
- return invalidFileErrMsg(filename, path, incl.RawText, err)
+ return invalidFileErrMsg(config.Filename, path, incl.RawText, err)
}
}
if err := scanner.Err(); err != nil {
- msg, err2 := invalidFileErrMsg(filename, path, incl.RawText, err)
+ msg, err2 := invalidFileErrMsg(config.Filename, path, incl.RawText, err)
if err2 != nil {
return types.DraftDocument{}, err2
}
@@ -103,9 +106,11 @@ func parseFileToInclude(filename string, incl types.FileInclusion, attrs types.D
}
// use a simpler/different grammar for non-asciidoc files.
if !IsAsciidoc(absPath) {
- opts = append(opts, Entrypoint("TextDocument"))
+ options = append(options, Entrypoint("TextDocument"))
}
- return parseDraftDocument(absPath, content, levelOffsets, opts...)
+ inclConfig := config.Clone()
+ inclConfig.Filename = absPath
+ return parseDraftDocument(content, levelOffsets, config, options...)
}
func invalidFileErrMsg(filename, path, rawText string, err error) (types.DraftDocument, error) {
diff --git a/pkg/parser/file_inclusion_test.go b/pkg/parser/file_inclusion_test.go
index 70746dfd..b359952b 100644
--- a/pkg/parser/file_inclusion_test.go
+++ b/pkg/parser/file_inclusion_test.go
@@ -702,7 +702,7 @@ include::{includedir}/include.foo[]`
It("should include adoc file within fenced block", func() {
source := "```\n" +
- "include::../../test/includes/chapter-a.adoc[]\n" +
+ "include::../../test/includes/parent-include.adoc[]\n" +
"```"
expected := types.DraftDocument{
Blocks: []interface{}{
@@ -715,7 +715,7 @@ include::{includedir}/include.foo[]`
Lines: [][]interface{}{
{
types.StringElement{
- Content: "= Chapter A",
+ Content: "= parent title",
},
},
},
@@ -726,7 +726,84 @@ include::{includedir}/include.foo[]`
Lines: [][]interface{}{
{
types.StringElement{
- Content: "content",
+ Content: "first line of parent",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "= child title",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "first line of child",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "== grandchild title",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "first line of grandchild",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "last line of grandchild",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "last line of child",
+ },
+ },
+ },
+ },
+ types.BlankLine{},
+ types.Paragraph{
+ Attributes: types.ElementAttributes{},
+ Lines: [][]interface{}{
+ {
+ types.StringElement{
+ Content: "last line of parent",
},
},
},
diff --git a/pkg/renderer/context.go b/pkg/renderer/context.go
index 05df716d..f66c02d4 100644
--- a/pkg/renderer/context.go
+++ b/pkg/renderer/context.go
@@ -1,62 +1,48 @@
package renderer
import (
- "errors"
- "io"
-
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/types"
+
log "github.com/sirupsen/logrus"
)
-// MacroTemplate an interface of template for user macro.
-type MacroTemplate interface {
- Execute(wr io.Writer, data interface{}) error
-}
-
// Context is a custom implementation of the standard golang context.Context interface,
// which carries the types.Document which is being processed
type Context struct {
Document types.Document
+ Config configuration.Configuration
// TableOfContents exists even if the document did not specify the `:toc:` attribute.
// It will take into account the configured `:toclevels:` attribute value.
TableOfContents types.TableOfContents
- options map[string]interface{}
- macros map[string]MacroTemplate
+ // macros map[string]MacroTemplate
+ includeBlankLine bool
+ withinDelimitedBlock bool
+ withinList int
+ counters map[string]int
}
// NewContext returns a new rendering context for the given document.
-func NewContext(document types.Document, options ...Option) Context {
- result := Context{
+func NewContext(document types.Document, config configuration.Configuration) Context {
+ return Context{
Document: document,
- options: make(map[string]interface{}),
- macros: make(map[string]MacroTemplate),
+ Config: config,
+ counters: make(map[string]int),
+ // macros: make(map[string]MacroTemplate),
}
- for _, option := range options {
- option(&result)
- }
- return result
}
-const includeBlankLine string = "includeBlankLine"
-
// SetIncludeBlankLine sets the rendering context to include (or not) the blank lines
func (ctx *Context) SetIncludeBlankLine(b bool) bool {
- var oldvalue bool
- if v, ok := ctx.options[includeBlankLine].(bool); ok {
- oldvalue = v
- }
- ctx.options[includeBlankLine] = b
- log.Debugf("set '%s' context param to '%t' (was '%t' before)", includeBlankLine, b, oldvalue)
+ oldvalue := ctx.includeBlankLine
+ ctx.includeBlankLine = b
+ log.Debugf("set 'includeBlankLine' context param to '%t' (was '%t' before)", b, oldvalue)
return oldvalue
}
// IncludeBlankLine indicates if blank lines should be rendered (default false)
func (ctx *Context) IncludeBlankLine() bool {
- if b, found := ctx.options[includeBlankLine].(bool); found {
- return b
- }
- // by default, ignore blank lines
- return false
+ return ctx.includeBlankLine
}
const withinDelimitedBlock string = "withinDelimitedBlock"
@@ -64,58 +50,31 @@ const withinDelimitedBlock string = "withinDelimitedBlock"
// SetWithinDelimitedBlock sets the rendering context to be within a delimited block
func (ctx *Context) SetWithinDelimitedBlock(b bool) bool {
log.Debugf("set rendering elements within a delimited block to `%t`", b)
- var oldvalue bool
- if v, ok := ctx.options[withinDelimitedBlock].(bool); ok {
- oldvalue = v
- }
+ oldvalue := ctx.withinDelimitedBlock
log.Debugf("set '%s' context param to '%t' (was '%t' before)", withinDelimitedBlock, b, oldvalue)
- ctx.options[withinDelimitedBlock] = b
+ ctx.withinDelimitedBlock = b
return oldvalue
}
// WithinDelimitedBlock indicates if the current element to render is within a delimited block or not
func (ctx *Context) WithinDelimitedBlock() bool {
- if b, found := ctx.options[withinDelimitedBlock].(bool); found {
- log.Debugf("rendering elements within a delimited block? %t", b)
- return b
- }
- // by default, consider not within a block
- return false
+ return ctx.withinDelimitedBlock
}
const withinList string = "withinList"
// SetWithinList sets the rendering context to be within a list or a nest list
func (ctx *Context) SetWithinList(w bool) {
- // log.Debugf("set rendering elements within a list to `%t`", w)
- var counter int
- var ok bool
- if counter, ok = ctx.options[withinList].(int); ok {
- // keep track of the depth of the list
- if w {
- counter++
- } else {
- counter--
- }
+ if w {
+ ctx.withinList++
} else {
- if w {
- counter = 1
- } else {
- counter = 0
- }
+ ctx.withinList--
}
- // update the counter in the context
- ctx.options[withinList] = counter
}
// WithinList indicates if the current element to render is within a list or not
func (ctx *Context) WithinList() bool {
- if counter, found := ctx.options[withinList].(int); found {
- // log.Debugf("rendering elements within a list? %t (%d)", (counter > 0), counter)
- return counter > 0
- }
- // by default, ignore blank lines
- return false
+ return ctx.withinList > 0
}
const tableCounter = "tableCounter"
@@ -140,24 +99,87 @@ func (ctx *Context) GetAndIncrementExampleBlockCounter() int {
}
// getAndIncrementCounter returns the current value for the counter after internally incrementing it.
-func (ctx *Context) getAndIncrementCounter(counter string) int {
- if _, found := ctx.options[counter]; !found {
- ctx.options[counter] = 1
+func (ctx *Context) getAndIncrementCounter(name string) int {
+ if _, found := ctx.counters[name]; !found {
+ ctx.counters[name] = 1
+ return 1
}
- if c, ok := ctx.options[counter].(int); ok {
- ctx.options[counter] = c + 1
- return c
- }
- ctx.options[counter] = 1
- log.Warnf("'%s' counter was set to a non-int value", counter)
- return 1
+ ctx.counters[name]++
+ return ctx.counters[name]
}
-// MacroTemplate finds and returns a user macro function by specified name.
-func (ctx *Context) MacroTemplate(name string) (MacroTemplate, error) {
- macro, ok := ctx.macros[name]
- if ok {
- return macro, nil
- }
- return nil, errors.New("unknown user macro: " + name)
-}
+// //Option the options when rendering a document
+// type Option func(ctx *Context)
+
+// const (
+// // keyLastUpdated the key to specify the last update of the document to render.
+// // Can be a string or a time, which will be formatted using the 2006/01/02 15:04:05 MST` pattern
+// keyLastUpdated string = types.AttrLastUpdated
+// // keyIncludeHeaderFooter key to a bool value to indicate if the header and footer should be rendered
+// keyIncludeHeaderFooter string = "IncludeHeaderFooter"
+// // keyCSS key to the options CSS to add in the document head. Default is empty ("")
+// keyCSS string = "CSS"
+// // keyEntrypoint key to the entrypoint to start with when parsing the document
+// keyEntrypoint string = "Entrypoint"
+// // LastUpdatedFormat key to the time format for the `last updated` document attribute
+// LastUpdatedFormat string = "2006-01-02 15:04:05 -0700"
+// )
+
+// // LastUpdated function to set the `last updated` option in the renderer context (default is `time.Now()`)
+// func WithLastUpdated(value time.Time) Option {
+// return func(ctx *Context) {
+// ctx.options[keyLastUpdated] = value
+// }
+// }
+
+// // IncludeHeaderFooter function to set the `include header/footer` option in the renderer context
+// func WithHeaderFooter(value bool) Option {
+// return func(ctx *Context) {
+// ctx.options[keyIncludeHeaderFooter] = value
+// }
+// }
+
+// // IncludeCSS function to set the `css` option in the renderer context
+// func WithCSS(href string) Option {
+// return func(ctx *Context) {
+// ctx.options[keyCSS] = href
+// }
+// }
+
+// // Entrypoint function to set the `entrypoint` option in the renderer context
+// func Entrypoint(entrypoint string) Option {
+// return func(ctx *Context) {
+// ctx.options[keyEntrypoint] = entrypoint
+// }
+// }
+
+// // DefineMacro defines the given template to a user macro with the given name
+// func WithMacroTemplate(name string, t MacroTemplate) Option {
+// return func(ctx *Context) {
+// ctx.macros[name] = t
+// }
+// }
+
+// // LastUpdated returns the value of the 'LastUpdated' Option if it was present,
+// // otherwise it returns the current time using the `2006/01/02 15:04:05 MST` format
+// func (ctx *Context) WithLastUpdated() string {
+// if lastUpdated, ok := ctx.options[keyLastUpdated].(time.Time); ok {
+// return lastUpdated.Format(LastUpdatedFormat)
+// }
+// return time.Now().Format(LastUpdatedFormat)
+// }
+
+// IncludeHeaderFooter returns the value of the 'IncludeHeaderFooter' Option if it was present,
+// otherwise it returns `false`
+// func (ctx *Context) WithHeaderFooter() bool {
+// return ctx.IncludeHeaderFooter
+// }
+
+// // CSS returns the value of the 'CSS' Option if it was present,
+// // otherwise it returns an empty string
+// func (ctx *Context) CSS() string {
+// if css, ok := ctx.options[keyCSS].(string); ok {
+// return css
+// }
+// return ""
+// }
diff --git a/pkg/renderer/html5/article_adoc_test.go b/pkg/renderer/html5/article_adoc_test.go
index 57a74f44..5a2ecf26 100644
--- a/pkg/renderer/html5/article_adoc_test.go
+++ b/pkg/renderer/html5/article_adoc_test.go
@@ -5,6 +5,7 @@ import (
"bytes"
"os"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/parser"
"github.com/bytesparadise/libasciidoc/pkg/renderer"
"github.com/bytesparadise/libasciidoc/pkg/renderer/html5"
@@ -20,11 +21,12 @@ var _ = Describe("article.adoc", func() {
f, err := os.Open("article.adoc")
Expect(err).ToNot(HaveOccurred())
reader := bufio.NewReader(f)
- doc, err := parser.ParseDocument("", reader)
+ config := configuration.NewConfiguration()
+ doc, err := parser.ParseDocument(reader, config)
Expect(err).ToNot(HaveOccurred())
GinkgoT().Logf("actual document: `%s`", spew.Sdump(doc))
buff := bytes.NewBuffer(nil)
- ctx := renderer.NewContext(doc)
+ ctx := renderer.NewContext(doc, config)
_, err = html5.Render(ctx, buff)
Expect(err).ToNot(HaveOccurred())
})
diff --git a/pkg/renderer/html5/document_details.go b/pkg/renderer/html5/document_details.go
index 8865a621..9cc2bdb7 100644
--- a/pkg/renderer/html5/document_details.go
+++ b/pkg/renderer/html5/document_details.go
@@ -7,6 +7,7 @@ import (
texttemplate "text/template"
"github.com/bytesparadise/libasciidoc/pkg/renderer"
+ "github.com/bytesparadise/libasciidoc/pkg/types"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -27,7 +28,7 @@ func init() {
}
func renderDocumentDetails(ctx renderer.Context) (*htmltemplate.HTML, error) {
- if ctx.Document.Attributes.HasAuthors() {
+ if ctx.Document.Attributes.Has(types.AttrAuthor) {
authors, err := renderDocumentAuthorsDetails(ctx)
if err != nil {
return nil, errors.Wrap(err, "error while rendering the document details")
diff --git a/pkg/renderer/html5/document_details_test.go b/pkg/renderer/html5/document_details_test.go
index 5f82e5eb..2e7cc7a6 100644
--- a/pkg/renderer/html5/document_details_test.go
+++ b/pkg/renderer/html5/document_details_test.go
@@ -3,7 +3,7 @@ package html5_test
import (
"time"
- "github.com/bytesparadise/libasciidoc/pkg/renderer"
+ "github.com/bytesparadise/libasciidoc/pkg/configuration"
. "github.com/bytesparadise/libasciidoc/testsupport"
. "github.com/onsi/ginkgo"
@@ -52,8 +52,7 @@ Last updated {{.LastUpdated}}
`
now := time.Now()
- Expect(RenderHTML5Body(source, renderer.IncludeHeaderFooter(true), renderer.LastUpdated(time.Now()))).
- To(MatchHTML5Template(expected, now))
+ Expect(RenderHTML5Body(source, configuration.WithHeaderFooter(true), configuration.WithLastUpdated(time.Now()))).To(MatchHTML5Template(expected, now))
})
It("header with 2 authors and no revision", func() {
@@ -91,9 +90,8 @@ Last updated {{.LastUpdated}}