Skip to content

Commit

Permalink
feat(parser/renderer): support for document attributes reset and subs…
Browse files Browse the repository at this point in the history
…titutions (#23)

Also: introduce a custom `Context` type that includes the document
being processed, giving access to document attributes when declaration,
reset and subsitutions elements are processed/rendered.

Also, rename type `DocumentAttribute` -> `DocumentAttributeDeclaration` while
introducing `DocumentAttributeSubstitution` and `DocumentAttributeReset` types.

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon authored Sep 16, 2017
1 parent 362892a commit f24fbd5
Show file tree
Hide file tree
Showing 20 changed files with 454 additions and 146 deletions.
43 changes: 43 additions & 0 deletions context/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package context

import (
"context"
"time"

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

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

// Wrap wraps the given `ctx` context into a new context which will contain the given `document` document.
func Wrap(ctx context.Context, document types.Document) Context {
return Context{
context: ctx,
Document: document,
}
}

// Deadline wrapper implementation of context.Context.Deadline()
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
return ctx.context.Deadline()
}

// Done wrapper implementation of context.Context.Done()
func (ctx *Context) Done() <-chan struct{} {
return ctx.Done()
}

// Err wrapper implementation of context.Context.Err()
func (ctx *Context) Err() error {
return ctx.Err()
}

// Value wrapper implementation of context.Context.Value(interface{})
func (ctx *Context) Value(key interface{}) interface{} {
return ctx.Value(key)
}
10 changes: 5 additions & 5 deletions libasciidoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"

asciidoc "github.com/bytesparadise/libasciidoc/context"
"github.com/bytesparadise/libasciidoc/parser"
"github.com/bytesparadise/libasciidoc/renderer"
htmlrenderer "github.com/bytesparadise/libasciidoc/renderer/html5"
Expand All @@ -15,15 +16,15 @@ import (
// ConvertToHTMLBody converts the content of the given reader `r` into an set of <DIV> elements for an HTML/BODY document.
// The conversion result is written in the given writer `w`, whereas the document metadata (title, etc.) (or an error if a problem occurred) is returned
// as the result of the function call.
func ConvertToHTMLBody(r io.Reader, w io.Writer) (*types.DocumentAttributes, error) {
func ConvertToHTMLBody(ctx context.Context, r io.Reader, w io.Writer) (*types.DocumentAttributes, error) {
doc, err := parser.ParseReader("", r)
if err != nil {
return nil, errors.Wrapf(err, "error while parsing the document")
}
document := doc.(*types.Document)
options := renderer.Options{}
options[renderer.IncludeHeaderFooter] = false // force value
err = htmlrenderer.Render(context.Background(), *document, w, options)
err = htmlrenderer.Render(asciidoc.Wrap(ctx, *document), w, options)
if err != nil {
return nil, errors.Wrapf(err, "error while rendering the document")
}
Expand All @@ -33,15 +34,14 @@ func ConvertToHTMLBody(r io.Reader, w io.Writer) (*types.DocumentAttributes, err

// ConvertToHTML converts the content of the given reader `r` into a full HTML document, written in the given writer `w`.
// Returns an error if a problem occurred
func ConvertToHTML(r io.Reader, w io.Writer, options renderer.Options) error {

func ConvertToHTML(ctx context.Context, r io.Reader, w io.Writer, options renderer.Options) error {
doc, err := parser.ParseReader("", r)
if err != nil {
return errors.Wrapf(err, "error while parsing the document")
}
document := doc.(*types.Document)
options[renderer.IncludeHeaderFooter] = true // force value
err = htmlrenderer.Render(context.Background(), *document, w, options)
err = htmlrenderer.Render(asciidoc.Wrap(ctx, *document), w, options)
if err != nil {
return errors.Wrapf(err, "error while rendering the document")
}
Expand Down
9 changes: 5 additions & 4 deletions libasciidoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package libasciidoc_test

import (
"bytes"
"context"
"strings"
"time"

Expand Down Expand Up @@ -179,8 +180,8 @@ Last updated {{.LastUpdated}}
func verifyDocumentBody(t GinkgoTInterface, expectedTitle *string, expectedContent, source string) {
t.Logf("processing '%s'", source)
sourceReader := strings.NewReader(source)
resultWriter := bytes.NewBuffer(make([]byte, 0))
metadata, err := ConvertToHTMLBody(sourceReader, resultWriter)
resultWriter := bytes.NewBuffer(nil)
metadata, err := ConvertToHTMLBody(context.Background(), sourceReader, resultWriter)
require.Nil(t, err, "Error found while parsing the document")
require.NotNil(t, metadata)
t.Log("Done processing document")
Expand All @@ -199,10 +200,10 @@ func verifyDocumentBody(t GinkgoTInterface, expectedTitle *string, expectedConte
func verifyCompleteDocument(t GinkgoTInterface, expectedContent, source string) {
t.Logf("processing '%s'", source)
sourceReader := strings.NewReader(source)
resultWriter := bytes.NewBuffer(make([]byte, 0))
resultWriter := bytes.NewBuffer(nil)
options := renderer.Options{}
options[renderer.LastUpdated] = time.Now()
err := ConvertToHTML(sourceReader, resultWriter, options)
err := ConvertToHTML(context.Background(), sourceReader, resultWriter, options)
require.Nil(t, err, "Error found while parsing the document")
t.Log("Done processing document")
result := string(resultWriter.Bytes())
Expand Down
44 changes: 33 additions & 11 deletions parser/asciidoc-grammar.peg
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ DocumentBlock <- !EOF content:(Section / StandaloneBlock) {
return content.(types.DocElement), nil
}

StandaloneBlock <- DocumentAttribute / List / BlockImage / DelimitedBlock / Paragraph / ElementAttribute / BlankLine //TODO: should Paragraph be the last type ?
StandaloneBlock <- DocumentAttributeDeclaration / DocumentAttributeReset / List / BlockImage / DelimitedBlock / Paragraph / ElementAttribute / BlankLine //TODO: should Paragraph be the last type ?

Section <- Section1 / Section2 / Section3 / Section4 / Section5 / Section6

Expand Down Expand Up @@ -101,18 +101,38 @@ Heading6 <- attributes:(ElementAttribute)* level:("======") WS+ content:InlineCo


// ------------------------------------------
// Document Attribute
// Document Attributes
// ------------------------------------------
DocumentAttribute <- DocumentAttributeWithNameOnly / DocumentAttributeWithNameAndValue
DocumentAttributeDeclaration <- DocumentAttributeDeclarationWithNameOnly / DocumentAttributeDeclarationWithNameAndValue

DocumentAttributeWithNameOnly <- ":" name:((!NEWLINE !":" !WS .)+) ":" WS* EOL {
return types.NewDocumentAttribute(name.([]interface{}), nil)
DocumentAttributeDeclarationWithNameOnly <- ":" name:(AttributeName) ":" WS* EOL {
return types.NewDocumentAttributeDeclaration(name.([]interface{}), nil)
}

DocumentAttributeWithNameAndValue <- ":" name:((!NEWLINE !":" !WS .)+) ":" WS+ value:(!NEWLINE .)* EOL {
return types.NewDocumentAttribute(name.([]interface{}), value.([]interface{}))
DocumentAttributeDeclarationWithNameAndValue <- ":" name:(AttributeName) ":" WS+ value:(!NEWLINE .)* EOL {
return types.NewDocumentAttributeDeclaration(name.([]interface{}), value.([]interface{}))
}

DocumentAttributeReset <- DocumentAttributeResetWithHeadingBangSymbol / DocumentAttributeResetWithTrailingBangSymbol

DocumentAttributeResetWithHeadingBangSymbol <- ":!" name:(AttributeName) ":" WS* EOL {
return types.NewDocumentAttributeReset(name.([]interface{}))
}

DocumentAttributeResetWithTrailingBangSymbol <- ":" name:(AttributeName) "!:" WS* EOL {
return types.NewDocumentAttributeReset(name.([]interface{}))
}


DocumentAttributeSubstitution <- "{" name:(AttributeName) "}" {
return types.NewDocumentAttributeSubstitution(name.([]interface{}))
}

// AttributeName must be at least one character long,
// must begin with a word character (A-Z, a-z, 0-9 or _) and
// must only contain word characters and hyphens ('-').
AttributeName <- ([A-Z] / [a-z] / [0-9] / "_") ([A-Z] / [a-z] / [0-9] / "-")*

// ------------------------------------------
// List Items
// ------------------------------------------
Expand All @@ -137,14 +157,16 @@ Paragraph <- attributes:(ElementAttribute)* lines:(InlineContent)+ {
return types.NewParagraph(c.text, lines.([]interface{}), attributes.([]interface{}))
}

// an inline content element may begin and end with spaces,
// but it must contain at least an image, a quoted text, an external link or a word
InlineContent <- elements:(WS* (InlineImage / QuotedText / ExternalLink / Word) WS*)+ EOL {
// an inline content element may start with and end with spaces,
// but it must contain at least an inline element (image, quoted text, external link, document attribute substitution, word, etc.)
InlineContent <- elements:(WS* InlineElement WS*)+ EOL {
return types.NewInlineContent(c.text, elements.([]interface{}))
}

InlineElement <- InlineImage / QuotedText / ExternalLink / DocumentAttributeSubstitution / Word

// ------------------------------------------
// Quote Texts (bold, italic and monospace)
// Quoted Texts (bold, italic and monospace)
// ------------------------------------------
QuotedText <- BoldText / ItalicText / MonospaceText

Expand Down
134 changes: 118 additions & 16 deletions parser/document_attributes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ var _ = Describe("Parsing Document Attributes", func() {

Context("Valid document attributes", func() {

It("valid attribute names", func() {

actualContent := `:a:
:author: Xavier
:_author: Xavier
:Author: Xavier
:0Author: Xavier
:Auth0r: Xavier`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttributeDeclaration{Name: "a"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "_author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "Author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "0Author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "Auth0r", Value: "Xavier"},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("heading section with attributes", func() {

actualContent := `= a heading
Expand All @@ -35,9 +57,9 @@ a paragraph`
},
},
Elements: []types.DocElement{
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "toc"},
&types.DocumentAttributeDeclaration{Name: "date", Value: "2017-01-01"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Expand All @@ -63,9 +85,9 @@ a paragraph`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "toc"},
&types.DocumentAttributeDeclaration{Name: "date", Value: "2017-01-01"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Expand All @@ -90,9 +112,9 @@ a paragraph`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "toc"},
&types.DocumentAttributeDeclaration{Name: "date", Value: "2017-01-01"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Expand All @@ -118,9 +140,9 @@ a paragraph`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "toc"},
&types.DocumentAttributeDeclaration{Name: "date", Value: "2017-01-01"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Expand Down Expand Up @@ -154,16 +176,70 @@ a paragraph`
},
},
},
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.DocumentAttributeDeclaration{Name: "toc"},
&types.DocumentAttributeDeclaration{Name: "date", Value: "2017-01-01"},
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("paragraph with attribute substitution", func() {

actualContent := `:author: Xavier
a paragraph written by {author}.`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph written by "},
&types.DocumentAttributeSubstitution{Name: "author"},
&types.StringElement{Content: "."},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("paragraph with attribute resets", func() {

actualContent := `:author: Xavier
:!author1:
:author2!:
a paragraph written by {author}.`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DocumentAttributeDeclaration{Name: "author", Value: "Xavier"},
&types.DocumentAttributeReset{Name: "author1"},
&types.DocumentAttributeReset{Name: "author2"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph written by "},
&types.DocumentAttributeSubstitution{Name: "author"},
&types.StringElement{Content: "."},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})
})

Context("Valid document attributes", func() {
Context("Invalid document attributes", func() {
It("paragraph and without blank line in between", func() {

actualContent := `a paragraph
Expand Down Expand Up @@ -201,5 +277,31 @@ a paragraph`
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("invalid attribute names", func() {

actualContent := `:@date: 2017-01-01
:{author}: Xavier`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: ":@date: 2017-01-01"},
},
},
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: ":{author}: Xavier"},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})
})
})
Loading

0 comments on commit f24fbd5

Please sign in to comment.