Skip to content

Commit

Permalink
feat(parser/renderer): support for document attributes (#22)
Browse files Browse the repository at this point in the history
Support document attribute in the form: ":name: value" or
":name:" at any location in the document.
For now, document attributes are silently ignored during
rendering.

Rename "Metadata" -> "Attributes" for document and elements.

Signed-off-by: Xavier Coulon <[email protected]>
  • Loading branch information
xcoulon authored Sep 9, 2017
1 parent 59e4119 commit 362892a
Show file tree
Hide file tree
Showing 20 changed files with 465 additions and 135 deletions.
4 changes: 2 additions & 2 deletions libasciidoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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.DocumentMetadata, error) {
func ConvertToHTMLBody(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")
Expand All @@ -28,7 +28,7 @@ func ConvertToHTMLBody(r io.Reader, w io.Writer) (*types.DocumentMetadata, error
return nil, errors.Wrapf(err, "error while rendering the document")
}
log.Debugf("Done processing document")
return document.Metadata, nil
return document.Attributes, nil
}

// ConvertToHTML converts the content of the given reader `r` into a full HTML document, written in the given writer `w`.
Expand Down
54 changes: 34 additions & 20 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 <- List / BlockImage / DelimitedBlock / Paragraph / MetadataElement / BlankLine
StandaloneBlock <- DocumentAttribute / List / BlockImage / DelimitedBlock / Paragraph / ElementAttribute / BlankLine //TODO: should Paragraph be the last type ?

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

Expand Down Expand Up @@ -75,37 +75,51 @@ Section6Block <- !Section1 !Section2 !Section3 !Section4 !Section5 !Section6 con
// ------------------------------------------
Heading <- Heading1 / Heading2 / Heading3 / Heading4 / Heading5 / Heading6

Heading1 <- metadata:(MetadataElement)* level:("=") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(1, content.(*types.InlineContent), metadata.([]interface{}))
Heading1 <- attributes:(ElementAttribute)* level:("=") WS+ content:InlineContent (BlankLine? / EOF) { //TODO: replace `(BlankLine? / EOF)` with `EOL` to allow for immediate attributes or any other content ?
return types.NewHeading(1, content.(*types.InlineContent), attributes.([]interface{}))
}

Heading2 <- metadata:(MetadataElement)* level:("==") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(2, content.(*types.InlineContent), metadata.([]interface{}))
Heading2 <- attributes:(ElementAttribute)* level:("==") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(2, content.(*types.InlineContent), attributes.([]interface{}))
}

Heading3 <- metadata:(MetadataElement)* level:("===") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(3, content.(*types.InlineContent), metadata.([]interface{}))
Heading3 <- attributes:(ElementAttribute)* level:("===") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(3, content.(*types.InlineContent), attributes.([]interface{}))
}

Heading4 <- metadata:(MetadataElement)* level:("====") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(4, content.(*types.InlineContent), metadata.([]interface{}))
Heading4 <- attributes:(ElementAttribute)* level:("====") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(4, content.(*types.InlineContent), attributes.([]interface{}))
}

Heading5 <- metadata:(MetadataElement)* level:("=====") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(5, content.(*types.InlineContent), metadata.([]interface{}))
Heading5 <- attributes:(ElementAttribute)* level:("=====") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(5, content.(*types.InlineContent), attributes.([]interface{}))
}

Heading6 <- metadata:(MetadataElement)* level:("======") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(6, content.(*types.InlineContent), metadata.([]interface{}))
Heading6 <- attributes:(ElementAttribute)* level:("======") WS+ content:InlineContent (BlankLine? / EOF) {
return types.NewHeading(6, content.(*types.InlineContent), attributes.([]interface{}))
}


// ------------------------------------------
// Document Attribute
// ------------------------------------------
DocumentAttribute <- DocumentAttributeWithNameOnly / DocumentAttributeWithNameAndValue

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

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

// ------------------------------------------
// List Items
// ------------------------------------------
List <- metadata:(MetadataElement)*
List <- attributes:(ElementAttribute)*
// list items can be followed by an optional, single blank line
elements:(ListItem BlankLine?)+ {
return types.NewList(elements.([]interface{}), metadata.([]interface{}))
return types.NewList(elements.([]interface{}), attributes.([]interface{}))
}

ListItem <- WS* level:('*'+ / '-') WS+ content:(ListItemContent) {
Expand All @@ -119,8 +133,8 @@ ListItemContent <- lines:(!(WS* ('*'+ / '-') WS+) InlineContent)+ {
// Paragraphs
// ------------------------------------------
// a paragraph is a group of line ending with a blank line (or end of file)
Paragraph <- metadata:(MetadataElement)* lines:(InlineContent)+ {
return types.NewParagraph(c.text, lines.([]interface{}), metadata.([]interface{}))
Paragraph <- attributes:(ElementAttribute)* lines:(InlineContent)+ {
return types.NewParagraph(c.text, lines.([]interface{}), attributes.([]interface{}))
}

// an inline content element may begin and end with spaces,
Expand Down Expand Up @@ -166,9 +180,9 @@ ExternalLink <- url:(URL_SCHEME URL) text:('[' (URL_TEXT)* ']')? {
// ------------------------------------------
// Images
// ------------------------------------------
BlockImage <- metadata:(MetadataElement)* image:BlockImageMacro WS* EOL {
BlockImage <- attributes:(ElementAttribute)* image:BlockImageMacro WS* EOL {
// here we can ignore the blank line in the returned element
return types.NewBlockImage(c.text, *image.(*types.ImageMacro), metadata.([]interface{}))
return types.NewBlockImage(c.text, *image.(*types.ImageMacro), attributes.([]interface{}))
}

BlockImageMacro <- "image::" path:(URL) '[' attributes:(URL_TEXT?) ']' {
Expand Down Expand Up @@ -201,7 +215,7 @@ SourceBlockLine <- (!EOL .)* NEWLINE
// ------------------------------------------
// Meta Elements
// ------------------------------------------
MetadataElement <- meta:(ElementLink / ElementID / ElementTitle)
ElementAttribute <- meta:(ElementLink / ElementID / ElementTitle)

// a link attached to an element, such as a BlockImage
ElementLink <- "[" WS* "link" WS* "=" WS* path:URL WS* "]" EOL {
Expand Down
3 changes: 1 addition & 2 deletions parser/asciidoc_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var _ = Describe("Parsing content", func() {
"\n" +
"a paragraph with *bold content*"
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{
Attributes: &types.DocumentAttributes{
"title": "a heading",
},
Elements: []types.DocElement{
Expand Down Expand Up @@ -85,7 +85,6 @@ func verify(t GinkgoTInterface, expectedDocument *types.Document, content string
}
require.Nil(t, err)
actualDocument := result.(*types.Document)
t.Logf("actual document structure: %+v", actualDocument.Elements)
t.Logf("actual document: `%s`", actualDocument.String(0))
t.Logf("expected document: `%s`", expectedDocument.String(0))
assert.EqualValues(t, *expectedDocument, *actualDocument)
Expand Down
4 changes: 2 additions & 2 deletions parser/blank_line_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var _ = Describe("Blank lines", func() {
second paragraph`
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{},
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.Paragraph{
Lines: []*types.InlineContent{
Expand Down Expand Up @@ -43,7 +43,7 @@ second paragraph`
second paragraph
`
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{},
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.Paragraph{
Lines: []*types.InlineContent{
Expand Down
6 changes: 3 additions & 3 deletions parser/delimited_block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var _ = Describe("Delimited Blocks", func() {
content := "some source code"
actualContent := "```\n" + content + "\n```"
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{},
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DelimitedBlock{
Kind: types.SourceBlock,
Expand All @@ -26,7 +26,7 @@ var _ = Describe("Delimited Blocks", func() {
content := "some source code\nwith an empty line\n\nin the middle"
actualContent := "```\n" + content + "\n```"
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{},
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DelimitedBlock{
Kind: types.SourceBlock,
Expand All @@ -41,7 +41,7 @@ var _ = Describe("Delimited Blocks", func() {
content := ""
actualContent := "```\n" + content + "```"
expectedDocument := &types.Document{
Metadata: &types.DocumentMetadata{},
Attributes: &types.DocumentAttributes{},
Elements: []types.DocElement{
&types.DelimitedBlock{
Kind: types.SourceBlock,
Expand Down
205 changes: 205 additions & 0 deletions parser/document_attributes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package parser_test

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

var _ = Describe("Parsing Document Attributes", func() {

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

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

actualContent := `= a heading
:toc:
:date: 2017-01-01
:author: Xavier
a paragraph`
expectedDocument := &types.Document{
Attributes: &types.DocumentAttributes{
"title": "a heading",
},
Elements: []types.DocElement{
&types.Section{
Heading: types.Heading{
Level: 1,
Content: &types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a heading"},
},
},
ID: &types.ElementID{
Value: "_a_heading",
},
},
Elements: []types.DocElement{
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
&types.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph"},
},
},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("attributes and paragraph without blank line in-between", func() {

actualContent := `:toc:
:date: 2017-01-01
:author: Xavier
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.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph"},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("contiguous attributes and paragraph with blank line in-between", func() {

actualContent := `:toc:
:date: 2017-01-01
:author: Xavier
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.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph"},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("splitted attributes and paragraph with blank line in-between", func() {

actualContent := `:toc:
:date: 2017-01-01
:author: Xavier
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.Paragraph{
Lines: []*types.InlineContent{
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: "a paragraph"},
},
},
},
},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})

It("no heading and attributes in body", func() {

actualContent := `a paragraph
:toc:
: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: "a paragraph"},
},
},
},
},
&types.DocumentAttribute{Name: "toc"},
&types.DocumentAttribute{Name: "date", Value: "2017-01-01"},
&types.DocumentAttribute{Name: "author", Value: "Xavier"},
},
}
verify(GinkgoT(), expectedDocument, actualContent)
})
})

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

actualContent := `a paragraph
:toc:
: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: "a paragraph"},
},
},
&types.InlineContent{
Elements: []types.InlineElement{
&types.StringElement{Content: ":toc:"},
},
},
&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 362892a

Please sign in to comment.