Skip to content

Commit

Permalink
feat(parser/renderer): add user macro feature (bytesparadise#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
odknt authored and xcoulon committed May 15, 2019
1 parent 18b54a2 commit 96b01cf
Show file tree
Hide file tree
Showing 10 changed files with 24,995 additions and 22,651 deletions.
20 changes: 20 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ where the returned `map[string]interface{}` object contains the document's title

For now, the sole option to pass as a last argument is `renderer.IncludeHeaderFooter` to include the `<header>` and `<footer>` elements in the generated HTML document or not. Default is `false`, which means that only the `<body>` part of the HTML document is generated.

=== Macro definition

The user can define a macro by calling `renderer.DefineMacro()` 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`)

Libasciidoc calls `Execute()` method and passes `types.UserMacro` object to template when rendering.

An example the following:

```
var tmplStr = `<span>Example: {{.Value}}{{.Attributes.GetAsString "suffix"}}</span>`
var t = template.New("example")
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))
```

== How to contribute

Please refer to the link:CONTRIBUTE.adoc[Contribute] page.
25 changes: 25 additions & 0 deletions pkg/parser/asciidoc-grammar.peg
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ DocumentElement <- !EOF // when reaching EOF, do not try to parse a new document
/ DocumentAttributeDeclaration
/ DocumentAttributeReset
/ TableOfContentsMacro
/ UserMacroBlock
/ Paragraph) {
return element, nil
}
Expand Down Expand Up @@ -548,6 +549,29 @@ TitleElement <- element:(Spaces / Dot / CrossReference / Passthrough / InlineIma
// ------------------------------------------
TableOfContentsMacro <- "toc::[]" NEWLINE

// ------------------------------------------
// User Macro
// ------------------------------------------
UserMacroBlock <- name:(UserMacroName) "::" value:(UserMacroValue) attrs:(UserMacroAttributes) {
return types.NewUserMacroBlock(name.(string), value.(string), attrs.(types.ElementAttributes), string(c.text))
}

InlineUserMacro <- name:(UserMacroName) ":" value:(UserMacroValue) attrs:(UserMacroAttributes) {
return types.NewInlineUserMacro(name.(string), value.(string), attrs.(types.ElementAttributes), string(c.text))
}

UserMacroName <- (!URL_SCHEME !"." !":" !"[" !"]" !WS !EOL .)+ {
return string(c.text), nil
}

UserMacroValue <- (!":" !"[" !"]" !EOL .)* {
return string(c.text), nil
}

UserMacroAttributes <- "[" attrs:(GenericAttribute)* "]" {
return types.NewInlineAttributes(attrs.([]interface{}))
}

// ------------------------------------------
// File inclusions
// ------------------------------------------
Expand Down Expand Up @@ -834,6 +858,7 @@ InlineElement <- !EOL !LineBreak
/ Link
/ Passthrough
/ InlineFootnote
/ InlineUserMacro
/ Alphanums
/ QuotedText
/ CrossReference
Expand Down
47,310 changes: 24,660 additions & 22,650 deletions pkg/parser/asciidoc_parser.go

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions pkg/parser/user_macro_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package parser_test

import (
"github.com/bytesparadise/libasciidoc/pkg/parser"
"github.com/bytesparadise/libasciidoc/pkg/types"
. "github.com/onsi/ginkgo"
)

var _ = Describe("user macros", func() {

Context("user macros", func() {

It("user block macro", func() {
actualContent := "git::some/url.git[key1=value1,key2=value2]"
expectedResult := types.UserMacro{
Kind: types.BlockMacro,
Name: "git",
Value: "some/url.git",
Attributes: types.ElementAttributes{
"key1": "value1",
"key2": "value2",
},
RawText: "git::some/url.git[key1=value1,key2=value2]",
}
verifyWithPreprocessing(GinkgoT(), expectedResult, actualContent, parser.Entrypoint("DocumentBlock"))
})

It("inline user macro", func() {
actualContent := "repository: git:some/url.git[key1=value1,key2=value2]"
expectedResult := types.Paragraph{
Attributes: types.ElementAttributes{},
Lines: []types.InlineElements{
{
types.StringElement{
Content: "repository: ",
},
types.UserMacro{
Kind: types.InlineMacro,
Name: "git",
Value: "some/url.git",
Attributes: types.ElementAttributes{
"key1": "value1",
"key2": "value2",
},
RawText: "git:some/url.git[key1=value1,key2=value2]",
},
},
},
}
verifyWithPreprocessing(GinkgoT(), expectedResult, actualContent, parser.Entrypoint("DocumentBlock"))
})
})
})
18 changes: 18 additions & 0 deletions pkg/renderer/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ package renderer

import (
"context"
"errors"
"io"
"time"

"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 {
context context.Context
Document types.Document
options map[string]interface{}
macros map[string]MacroTemplate
}

// Wrap wraps the given `ctx` context into a new context which will contain the given `document` document.
Expand All @@ -22,6 +30,7 @@ func Wrap(ctx context.Context, document types.Document, options ...Option) *Cont
context: ctx,
Document: document,
options: make(map[string]interface{}),
macros: make(map[string]MacroTemplate),
}
for _, option := range options {
option(result)
Expand Down Expand Up @@ -155,6 +164,15 @@ func (ctx *Context) GetImagesDir() string {
return ""
}

// 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)
}

// -----------------------
// context.Context methods
// -----------------------
Expand Down
2 changes: 2 additions & 0 deletions pkg/renderer/html5/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ func renderElement(ctx *renderer.Context, element interface{}) ([]byte, error) {
return renderAttributeSubstitution(ctx, e), nil
case types.LineBreak:
return renderLineBreak()
case types.UserMacro:
return renderUserMacro(ctx, e)
case types.SingleLineComment:
return nil, nil // nothing to do
default:
Expand Down
33 changes: 33 additions & 0 deletions pkg/renderer/html5/user_macro.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package html5

import (
"bytes"

"github.com/bytesparadise/libasciidoc/pkg/renderer"
"github.com/bytesparadise/libasciidoc/pkg/types"
)

func renderUserMacro(ctx *renderer.Context, um types.UserMacro) ([]byte, error) {
buf := bytes.NewBuffer([]byte{})
macro, err := ctx.MacroTemplate(um.Name)
if err != nil {
if um.Kind == types.BlockMacro {
// fallback to paragraph
p, _ := types.NewParagraph([]interface{}{
types.InlineElements{
types.StringElement{Content: um.RawText},
},
}, nil)
return renderParagraph(ctx, p)
}
// fallback to render raw text
_, err = buf.WriteString(um.RawText)
} else {
err = macro.Execute(buf, um)
}
if err != nil {
return nil, err
}
return buf.Bytes(), nil

}
135 changes: 135 additions & 0 deletions pkg/renderer/html5/user_macro_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package html5_test

import (
"html"
texttemplate "text/template"

"github.com/bytesparadise/libasciidoc/pkg/renderer"
. "github.com/onsi/ginkgo"
)

var helloMacroTmpl *texttemplate.Template

var _ = Describe("user macros", func() {

Context("user macros", func() {
It("undefined macro block", func() {

actualContent := "hello::[]"
expectedResult := `<div class="paragraph">
<p>hello::[]</p>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})

It("user macro block", func() {

actualContent := "hello::[]"
expectedResult := `<div class="helloblock">
<div class="content">
<span>hello world</span>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("user macro block with attribute", func() {

actualContent := `hello::[suffix="!!!!"]`
expectedResult := `<div class="helloblock">
<div class="content">
<span>hello world!!!!</span>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("user macro block with value", func() {

actualContent := `hello::John Doe[]`
expectedResult := `<div class="helloblock">
<div class="content">
<span>hello John Doe</span>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("user macro block with value and attributes", func() {

actualContent := `hello::John Doe[prefix="Hi ",suffix="!!"]`
expectedResult := `<div class="helloblock">
<div class="content">
<span>Hi John Doe!!</span>
</div>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("undefined inline macro", func() {

actualContent := "hello:[]"
expectedResult := `<div class="paragraph">
<p>hello:[]</p>
</div>`
verify(GinkgoT(), expectedResult, actualContent)
})

It("inline macro", func() {

actualContent := "AAA hello:[]"
expectedResult := `<div class="paragraph">
<p>AAA <span>hello world</span></p>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("inline macro with attribute", func() {

actualContent := `AAA hello:[suffix="!!!!!"]`
expectedResult := `<div class="paragraph">
<p>AAA <span>hello world!!!!!</span></p>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("inline macro with value", func() {

actualContent := `AAA hello:John Doe[]`
expectedResult := `<div class="paragraph">
<p>AAA <span>hello John Doe</span></p>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

It("inline macro with value and attributes", func() {

actualContent := `AAA hello:John Doe[prefix="Hi ",suffix="!!"]`
expectedResult := `<div class="paragraph">
<p>AAA <span>Hi John Doe!!</span></p>
</div>`
verify(GinkgoT(), expectedResult, actualContent, renderer.DefineMacro(helloMacroTmpl.Name(), helloMacroTmpl))
})

})
})

func init() {
t := texttemplate.New("hello")
t.Funcs(texttemplate.FuncMap{
"escape": html.EscapeString,
})
helloMacroTmpl = texttemplate.Must(t.Parse(`{{- if eq .Kind "block" -}}
<div class="helloblock">
<div class="content">
{{end -}}
<span>
{{- if .Attributes.Has "prefix"}}{{escape (.Attributes.GetAsString "prefix")}} {{else}}hello {{end -}}
{{- if ne .Value ""}}{{escape .Value}}{{else}}world{{- end -}}
{{- escape (.Attributes.GetAsString "suffix") -}}
</span>
{{- if eq .Kind "block"}}
</div>
</div>
{{- end -}}`))
}
11 changes: 10 additions & 1 deletion pkg/renderer/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package renderer

import "time"
import (
"time"
)

//Option the options when rendering a document
type Option func(ctx *Context)
Expand Down Expand Up @@ -38,6 +40,13 @@ func Entrypoint(entrypoint string) Option {
}
}

// DefineMacro defines the given template to a user macro with the given name
func DefineMacro(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) LastUpdated() string {
Expand Down
Loading

0 comments on commit 96b01cf

Please sign in to comment.