Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser/renderer): add user macro feature #347

Merged
merged 2 commits into from
May 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
odknt marked this conversation as resolved.
Show resolved Hide resolved
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() {
odknt marked this conversation as resolved.
Show resolved Hide resolved

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