From 99b1fd9d774e2e227ad6936b3158bc51b3fc309c Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Tue, 24 Oct 2017 10:36:21 +0200 Subject: [PATCH] feat(parser/renderer): Support for Document Author and Revision, and Preamble (#36) Also, refactor test by using contexts to group Also, update dependencies, including Pigeon (see below) Also, use new 'Entrypoint' option in Pigeon to reduce the structure of the expected output when parsing Also, refactor the options to render a document, using functions to configure the rendering context Signed-off-by: Xavier Coulon --- glide.lock | 14 +- libasciidoc.go | 15 +- libasciidoc_test.go | 32 +- parser/asciidoc-grammar.peg | 179 +- parser/asciidoc_parser.go | 3674 +++++++++++++++--------- parser/asciidoc_parser_test.go | 55 +- parser/blank_line_test.go | 4 +- parser/delimited_block_test.go | 10 +- parser/document_attributes_test.go | 595 ++-- parser/external_link_test.go | 6 +- parser/frontmatter_test.go | 6 +- parser/image_test.go | 26 +- parser/list_test.go | 20 +- parser/literal_block_test.go | 10 +- parser/meta_elements_test.go | 18 +- parser/paragraph_test.go | 8 +- parser/quoted_text_test.go | 515 ++-- parser/section_test.go | 320 +-- {context => renderer}/context.go | 12 +- renderer/html5/delimited_block.go | 4 +- renderer/html5/document_attribute.go | 22 +- renderer/html5/document_header.go | 164 ++ renderer/html5/document_header_test.go | 87 + renderer/html5/image.go | 8 +- renderer/html5/image_test.go | 2 +- renderer/html5/inline_content.go | 6 +- renderer/html5/list_item.go | 8 +- renderer/html5/literal_blocks.go | 4 +- renderer/html5/paragraph.go | 6 +- renderer/html5/paragraph_test.go | 2 +- renderer/html5/quoted_text.go | 6 +- renderer/html5/renderer.go | 82 +- renderer/html5/renderer_test.go | 22 +- renderer/html5/section.go | 163 +- renderer/html5/section_test.go | 67 +- renderer/html5/string.go | 4 +- renderer/options.go | 61 +- types/document_attributes.go | 57 +- types/document_attributes_test.go | 39 + types/grammar_types.go | 427 ++- types/type_utils.go | 42 +- 41 files changed, 4188 insertions(+), 2614 deletions(-) rename {context => renderer}/context.go (78%) create mode 100644 renderer/html5/document_header.go create mode 100644 renderer/html5/document_header_test.go create mode 100644 types/document_attributes_test.go diff --git a/glide.lock b/glide.lock index 04365bd4..7568d77f 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,14 @@ hash: 3a7a6660aa022e0847683a59e6553c85cee1cd88d9aec5d90896d11a1ce7f5b5 -updated: 2017-09-22T15:13:53.6579306+02:00 +updated: 2017-10-16T09:19:56.63025179+02:00 imports: +- name: github.com/davecgh/go-spew + version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 + subpackages: + - spew - name: github.com/go-test/deep version: f49763a6ea0a91026be26f8213bebee726b4185f - name: github.com/mna/pigeon - version: 7397997f31ddb24949618f11bab1588199703721 + version: 0e50ca5df1bdf6740f6780e5ed926f5973a44ddc - name: github.com/onsi/ginkgo version: 9eda700730cba42af70d53180f9dcce9266bc2bc subpackages: @@ -51,7 +55,7 @@ imports: - assert - require - name: golang.org/x/crypto - version: eb71ad9bd329b5ac0fd0148dd99bd62e8be8e035 + version: 81e90905daefcd6fd217b62423c0908922eadb30 subpackages: - ssh/terminal - name: golang.org/x/sys @@ -81,10 +85,6 @@ imports: - name: gopkg.in/yaml.v2 version: a5b47d31c556af34a302ce5d659e6fea44d90de0 testImports: -- name: github.com/davecgh/go-spew - version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 - subpackages: - - spew - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: diff --git a/libasciidoc.go b/libasciidoc.go index 7adfc74e..d802ee59 100644 --- a/libasciidoc.go +++ b/libasciidoc.go @@ -4,7 +4,6 @@ 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" @@ -16,15 +15,14 @@ import ( // ConvertToHTMLBody converts the content of the given reader `r` into an set of
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(ctx context.Context, 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(asciidoc.Wrap(ctx, *document), w, options) + options := []renderer.Option{renderer.IncludeHeaderFooter(false)} + err = htmlrenderer.Render(renderer.Wrap(ctx, *document, options...), w) if err != nil { return nil, errors.Wrapf(err, "error while rendering the document") } @@ -34,14 +32,15 @@ func ConvertToHTMLBody(ctx context.Context, r io.Reader, w io.Writer) (*types.Do // 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(ctx context.Context, r io.Reader, w io.Writer, options renderer.Options) error { +func ConvertToHTML(ctx context.Context, r io.Reader, w io.Writer, options ...renderer.Option) 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(asciidoc.Wrap(ctx, *document), w, options) + // force/override value + options = append(options, renderer.IncludeHeaderFooter(true)) + err = htmlrenderer.Render(renderer.Wrap(ctx, *document, options...), w) if err != nil { return errors.Wrapf(err, "error while rendering the document") } diff --git a/libasciidoc_test.go b/libasciidoc_test.go index 2b95c7ec..f84f24be 100644 --- a/libasciidoc_test.go +++ b/libasciidoc_test.go @@ -32,7 +32,7 @@ var _ = Describe("Rendering documents in HTML", func() { verifyDocumentBody(GinkgoT(), &expectedTitle, expectedContent, source) }) - It("section levels 1 and 2", func() { + It("section levels 0 and 1", func() { source := `= a document title == Section A @@ -50,7 +50,7 @@ a paragraph with *bold content*` verifyDocumentBody(GinkgoT(), &expectedTitle, expectedContent, source) }) - It("section level 2", func() { + It("section level 1 with a paragraph", func() { source := `== Section A a paragraph with *bold content*` @@ -65,14 +65,14 @@ a paragraph with *bold content*` verifyDocumentBody(GinkgoT(), nil, expectedContent, source) }) - It("section levels 1, 2 and 3", func() { + It("section levels 0, 1 and 3", func() { source := `= a document title == Section A a paragraph with *bold content* -=== Section A.a +==== Section A.a.a a paragraph` expectedTitle := "a document title" @@ -82,8 +82,8 @@ a paragraph`

a paragraph with bold content

-
-

Section A.a

+
+

Section A.a.a

a paragraph

@@ -136,10 +136,10 @@ a paragraph with _italic content_` Context("Complete Document ", func() { - It("section levels 1 and 2", func() { + It("section levels 0 and 5", func() { source := `= a document title -== Section A +====== Section A a paragraph with *bold content*` expectedContent := ` @@ -155,15 +155,13 @@ a paragraph with *bold content*`

a document title

-
-

Section A

-
+
+
Section A

a paragraph with bold content

-
`) inlineImageTmpl = newHTMLTemplate("inline image", `{{.Macro.Alt}}`) } -func renderBlockImage(ctx asciidoc.Context, img types.BlockImage) ([]byte, error) { +func renderBlockImage(ctx *renderer.Context, img types.BlockImage) ([]byte, error) { result := bytes.NewBuffer(nil) err := blockImageTmpl.Execute(result, img) if err != nil { @@ -35,7 +35,7 @@ func renderBlockImage(ctx asciidoc.Context, img types.BlockImage) ([]byte, error return result.Bytes(), nil } -func renderInlineImage(ctx asciidoc.Context, img types.InlineImage) ([]byte, error) { +func renderInlineImage(ctx *renderer.Context, img types.InlineImage) ([]byte, error) { result := bytes.NewBuffer(nil) err := inlineImageTmpl.Execute(result, img) if err != nil { diff --git a/renderer/html5/image_test.go b/renderer/html5/image_test.go index 420f614a..d5f0e78e 100644 --- a/renderer/html5/image_test.go +++ b/renderer/html5/image_test.go @@ -44,7 +44,7 @@ var _ = Describe("Rendering Images", func() {
the foo.png image
-
A title to foobar
+
A title to foobar
` verify(GinkgoT(), expected, content) }) diff --git a/renderer/html5/inline_content.go b/renderer/html5/inline_content.go index 7a1a6942..dc63ff51 100644 --- a/renderer/html5/inline_content.go +++ b/renderer/html5/inline_content.go @@ -3,15 +3,15 @@ package html5 import ( "bytes" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" ) -func renderInlineContent(ctx asciidoc.Context, content types.InlineContent) ([]byte, error) { +func renderInlineContent(ctx *renderer.Context, content types.InlineContent) ([]byte, error) { renderedElementsBuff := bytes.NewBuffer(nil) for _, element := range content.Elements { - renderedElement, err := processElement(ctx, element) + renderedElement, err := renderElement(ctx, element) if err != nil { return nil, errors.Wrapf(err, "unable to render paragraph element") } diff --git a/renderer/html5/list_item.go b/renderer/html5/list_item.go index 10f1a4b9..2385d28a 100644 --- a/renderer/html5/list_item.go +++ b/renderer/html5/list_item.go @@ -4,7 +4,7 @@ import ( "bytes" "html/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -30,7 +30,7 @@ func init() { listItemContentTmpl = newHTMLTemplate("list item content", `

{{.}}

`) } -func renderList(ctx asciidoc.Context, list types.List) ([]byte, error) { +func renderList(ctx *renderer.Context, list types.List) ([]byte, error) { renderedElementsBuff := bytes.NewBuffer(nil) for i, item := range list.Items { renderedListItem, err := renderListItem(ctx, *item) @@ -59,7 +59,7 @@ func renderList(ctx asciidoc.Context, list types.List) ([]byte, error) { return result.Bytes(), nil } -func renderListItem(ctx asciidoc.Context, item types.ListItem) ([]byte, error) { +func renderListItem(ctx *renderer.Context, item types.ListItem) ([]byte, error) { renderedItemContent, err := renderListItemContent(ctx, *item.Content) if err != nil { return nil, errors.Wrapf(err, "unable to render list item") @@ -88,7 +88,7 @@ func renderListItem(ctx asciidoc.Context, item types.ListItem) ([]byte, error) { return result.Bytes(), nil } -func renderListItemContent(ctx asciidoc.Context, content types.ListItemContent) ([]byte, error) { +func renderListItemContent(ctx *renderer.Context, content types.ListItemContent) ([]byte, error) { renderedLinesBuff := bytes.NewBuffer(nil) for _, line := range content.Lines { renderedLine, err := renderInlineContent(ctx, *line) diff --git a/renderer/html5/literal_blocks.go b/renderer/html5/literal_blocks.go index 10762518..e4b064c6 100644 --- a/renderer/html5/literal_blocks.go +++ b/renderer/html5/literal_blocks.go @@ -4,7 +4,7 @@ import ( "bytes" "html/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -21,7 +21,7 @@ func init() {
`) } -func renderLiteralBlock(ctx asciidoc.Context, block types.LiteralBlock) ([]byte, error) { +func renderLiteralBlock(ctx *renderer.Context, block types.LiteralBlock) ([]byte, error) { log.Debugf("rendering delimited block with content: %s", block.Content) result := bytes.NewBuffer(nil) err := literalBlockTmpl.Execute(result, block) diff --git a/renderer/html5/paragraph.go b/renderer/html5/paragraph.go index 32049be9..1aa31d86 100644 --- a/renderer/html5/paragraph.go +++ b/renderer/html5/paragraph.go @@ -4,7 +4,7 @@ import ( "bytes" "html/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" ) @@ -15,12 +15,12 @@ var paragraphTmpl *template.Template func init() { paragraphTmpl = newHTMLTemplate("paragraph", `
{{ if .Title}} -
{{.Title.Value}}
{{ end }} +
{{.Title.Value}}
{{ end }}

{{.Lines}}

`) } -func renderParagraph(ctx asciidoc.Context, paragraph types.Paragraph) ([]byte, error) { +func renderParagraph(ctx *renderer.Context, paragraph types.Paragraph) ([]byte, error) { renderedLinesBuff := bytes.NewBuffer(nil) for i, line := range paragraph.Lines { renderedLine, err := renderInlineContent(ctx, *line) diff --git a/renderer/html5/paragraph_test.go b/renderer/html5/paragraph_test.go index a9d62efd..19d3fb93 100644 --- a/renderer/html5/paragraph_test.go +++ b/renderer/html5/paragraph_test.go @@ -19,7 +19,7 @@ with more content afterwards...

.a title *bold content* with more content afterwards...` expected := `
-
a title
+
a title

bold content with more content afterwards...

` verify(GinkgoT(), expected, content) diff --git a/renderer/html5/quoted_text.go b/renderer/html5/quoted_text.go index d07b1eb8..51c9f24d 100644 --- a/renderer/html5/quoted_text.go +++ b/renderer/html5/quoted_text.go @@ -4,7 +4,7 @@ import ( "bytes" "html/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -21,10 +21,10 @@ func init() { monospaceTextTmpl = newHTMLTemplate("monospace text", "{{.}}") } -func renderQuotedText(ctx asciidoc.Context, t types.QuotedText) ([]byte, error) { +func renderQuotedText(ctx *renderer.Context, t types.QuotedText) ([]byte, error) { elementsBuffer := bytes.NewBuffer(nil) for _, element := range t.Elements { - b, err := processElement(ctx, element) + b, err := renderElement(ctx, element) if err != nil { return nil, errors.Wrapf(err, "unable to render text quote") } diff --git a/renderer/html5/renderer.go b/renderer/html5/renderer.go index 2b10a923..0e32a96d 100644 --- a/renderer/html5/renderer.go +++ b/renderer/html5/renderer.go @@ -1,90 +1,26 @@ package html5 import ( - "bytes" - "html/template" "io" - texttemplate "text/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" ) -var documentTmpl *texttemplate.Template - -func init() { - documentTmpl = newTextTemplate("root document", - ` - - - - - -{{ if .Generator }}{{ end }} -{{.Title}} - - -
-{{.Content}} -
- - -`) -} - // Render renders the given document in HTML and writes the result in the given `writer` -func Render(ctx asciidoc.Context, output io.Writer, options renderer.Options) error { - includeHeaderFooter, err := options.IncludeHeaderFooter() - if err != nil { - return errors.Wrap(err, "error while rendering the HTML document") - } - - lastUpdated, err := options.LastUpdated() - if err != nil { - return errors.Wrap(err, "error while rendering the HTML document") - } - - if *includeHeaderFooter { - // use a temporary writer for the document's content - renderedElementsBuff := bytes.NewBuffer(nil) - processElements(ctx, renderedElementsBuff) - renderedHTMLElements := template.HTML(renderedElementsBuff.String()) - title := "undefined" - if ctx.Document.Attributes.GetTitle() != nil { - title = *ctx.Document.Attributes.GetTitle() - } - err := documentTmpl.Execute(output, struct { - Generator string - Title string - Content template.HTML - LastUpdated string - }{ - Generator: "libasciidoc", // TODO: externalize this value and include the lib version ? - Title: title, - Content: renderedHTMLElements, - LastUpdated: *lastUpdated, - }) - if err != nil { - return errors.Wrap(err, "error while rendering the HTML document") - } - return nil +func Render(ctx *renderer.Context, output io.Writer) error { + if ctx.IncludeHeaderFooter() { + return renderFullDocument(ctx, output) } - return processElements(ctx, output) + return renderElements(ctx, output) } -func processElements(ctx asciidoc.Context, output io.Writer) error { +func renderElements(ctx *renderer.Context, output io.Writer) error { hasContent := false for _, element := range ctx.Document.Elements { - content, err := processElement(ctx, element) + content, err := renderElement(ctx, element) if err != nil { return errors.Wrapf(err, "failed to render the document") } @@ -102,10 +38,12 @@ func processElements(ctx asciidoc.Context, output io.Writer) error { return nil } -func processElement(ctx asciidoc.Context, element types.DocElement) ([]byte, error) { +func renderElement(ctx *renderer.Context, element types.DocElement) ([]byte, error) { switch element.(type) { case *types.Section: return renderSection(ctx, *element.(*types.Section)) + case *types.Preamble: + return renderPreamble(ctx, *element.(*types.Preamble)) case *types.List: return renderList(ctx, *element.(*types.List)) case *types.Paragraph: @@ -126,7 +64,7 @@ func processElement(ctx asciidoc.Context, element types.DocElement) ([]byte, err return renderStringElement(ctx, *element.(*types.StringElement)) case *types.DocumentAttributeDeclaration: // 'process' function do not return any rendered content, but may return an error - return nil, processAttributeDeclaration(ctx, *element.(*types.DocumentAttributeDeclaration)) + return nil, processAttributeDeclaration(ctx, element.(*types.DocumentAttributeDeclaration)) case *types.DocumentAttributeReset: // 'process' function do not return any rendered content, but may return an error return nil, processAttributeReset(ctx, *element.(*types.DocumentAttributeReset)) diff --git a/renderer/html5/renderer_test.go b/renderer/html5/renderer_test.go index 6b45bcb8..e13b7c68 100644 --- a/renderer/html5/renderer_test.go +++ b/renderer/html5/renderer_test.go @@ -5,30 +5,34 @@ import ( "context" "strings" - asciidoc "github.com/bytesparadise/libasciidoc/context" "github.com/bytesparadise/libasciidoc/parser" - . "github.com/bytesparadise/libasciidoc/renderer/html5" + "github.com/bytesparadise/libasciidoc/renderer" + "github.com/bytesparadise/libasciidoc/renderer/html5" "github.com/bytesparadise/libasciidoc/types" + "github.com/davecgh/go-spew/spew" . "github.com/onsi/ginkgo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func verify(t GinkgoTInterface, expected, content string) { - expected = strings.Replace(expected, "\t", "", -1) +func verify(t GinkgoTInterface, expected, content string, options ...renderer.Option) { t.Logf("processing '%s'", content) reader := strings.NewReader(content) doc, err := parser.ParseReader("", reader) require.Nil(t, err, "Error found while parsing the document") actualDocument := doc.(*types.Document) - t.Logf("Actual document:\n%s", actualDocument.String(1)) + t.Logf("actual document: `%s`", spew.Sdump(actualDocument)) + rendererCtx := renderer.Wrap(context.Background(), *actualDocument, options...) buff := bytes.NewBuffer(nil) - ctx := asciidoc.Wrap(context.Background(), *actualDocument) - err = Render(ctx, buff, nil) - t.Log("Done processing document") + err = html5.Render(rendererCtx, buff) + t.Log("* Done processing document:") require.Nil(t, err) require.Empty(t, err) - result := string(buff.Bytes()) + result := buff.String() + expected = strings.Replace(expected, "\t", "", -1) + if strings.Contains(expected, "{{.LastUpdated}}") { + expected = strings.Replace(expected, "{{.LastUpdated}}", rendererCtx.LastUpdated(), 1) + } t.Logf("** Actual output:\n`%s`\n", result) t.Logf("** Expected output:\n`%s`\n", expected) // remove tabs that can be inserted by VSCode while formatting the tests code assert.Equal(t, expected, result) diff --git a/renderer/html5/section.go b/renderer/html5/section.go index a01684a1..fdc3cabd 100644 --- a/renderer/html5/section.go +++ b/renderer/html5/section.go @@ -5,180 +5,131 @@ import ( "html/template" "strconv" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) -// var section1HeaderTmpl *template.Template -var otherSectionHeaderTmpl *template.Template +var preambleTmpl *template.Template +var sectionHeaderTmpl *template.Template var section1ContentTmpl *template.Template -var section2ContentTmpl *template.Template var otherSectionContentTmpl *template.Template // initializes the templates func init() { - section1ContentTmpl = newHTMLTemplate("section 1", - `{{ if .Preamble }}
+ preambleTmpl = newHTMLTemplate("preamble", + `
-{{.Preamble}} -
+{{.}}
-{{end}}{{ if .Elements }}{{.Elements}}{{end}}`) - section2ContentTmpl = newHTMLTemplate("section 2", +
`) + section1ContentTmpl = newHTMLTemplate("section 1", `
-{{.Heading}} +{{.SectionTitle}}
{{ if .Elements }} {{.Elements}}{{end}}
`) otherSectionContentTmpl = newHTMLTemplate("other section", `
-{{.Heading}}{{ if .Elements }} +{{.SectionTitle}}{{ if .Elements }} {{.Elements}}{{end}}
`) - // section1HeaderTmpl = newHTMLTemplate("section 1 heading", - // ``) - otherSectionHeaderTmpl = newHTMLTemplate("other heading", + sectionHeaderTmpl = newHTMLTemplate("other sectionTitle", `{{.Content}}`) } -func renderSection(ctx asciidoc.Context, section types.Section) ([]byte, error) { - switch section.Heading.Level { - case 1: - return renderSectionLevel1(ctx, section) - default: - return renderOtherSection(ctx, section) - } -} - -func renderSectionLevel1(ctx asciidoc.Context, section types.Section) ([]byte, error) { - // only applies if the first element (if exists) is not a nested section - var preambleElements []types.DocElement - var otherElements []types.DocElement - // log.Debugf("Preparing Preamble elements...") - for i, element := range section.Elements { - log.Debugf(" %T", element) - - if _, ok := element.(*types.Section); ok { - if i > 0 { - preambleElements = section.Elements[:i] - } else { - preambleElements = make([]types.DocElement, 0) - } - otherElements = section.Elements[i:] - break - } - } - // log.Debugf("Preamble elements: %d", len(preambleElements)) - renderedPreambleElementsBuff := bytes.NewBuffer(nil) - for i, element := range preambleElements { - renderedElement, err := processElement(ctx, element) - if err != nil { - return nil, errors.Wrapf(err, "unable to render preamble element") - } - renderedPreambleElementsBuff.Write(renderedElement) - if i < len(preambleElements)-1 { - renderedPreambleElementsBuff.WriteString("\n") - } - } - renderedHTMLPreamble := template.HTML(renderedPreambleElementsBuff.String()) +func renderPreamble(ctx *renderer.Context, preamble types.Preamble) ([]byte, error) { + log.Debugf("Rendering preamble...") renderedElementsBuff := bytes.NewBuffer(nil) - for i, element := range otherElements { - renderedElement, err := processElement(ctx, element) + for i, element := range preamble.Elements { + renderedElement, err := renderElement(ctx, element) if err != nil { - return nil, errors.Wrapf(err, "unable to render section element") + return nil, errors.Wrapf(err, "unable to render preamble") } renderedElementsBuff.Write(renderedElement) - if i < len(otherElements)-1 { + if i < len(preamble.Elements)-1 { renderedElementsBuff.WriteString("\n") } } - renderedHTMLElements := template.HTML(renderedElementsBuff.String()) result := bytes.NewBuffer(nil) - err := section1ContentTmpl.Execute(result, struct { - Class string - Preamble template.HTML - Elements template.HTML - }{ - Class: "sect" + strconv.Itoa(section.Heading.Level-1), - Preamble: renderedHTMLPreamble, - Elements: renderedHTMLElements, - }) + err := preambleTmpl.Execute(result, template.HTML(renderedElementsBuff.String())) if err != nil { return nil, errors.Wrapf(err, "error while rendering section") } - // log.Debugf("rendered section: %s", result.Bytes()) + log.Debugf("rendered preamble: %s", result.Bytes()) return result.Bytes(), nil } -func renderOtherSection(ctx asciidoc.Context, section types.Section) ([]byte, error) { - renderedHeading, err := renderHeading(ctx, section.Heading) +func renderSection(ctx *renderer.Context, section types.Section) ([]byte, error) { + log.Debugf("Rendering section level %d", section.Level) + renderedSectionTitle, err := renderSectionTitle(ctx, section.Level, section.SectionTitle) if err != nil { - return nil, errors.Wrapf(err, "error while rendering section heading") - } - renderedElementsBuff := bytes.NewBuffer(nil) - for i, element := range section.Elements { - renderedElement, err := processElement(ctx, element) - if err != nil { - return nil, errors.Wrapf(err, "unable to render section element") - } - renderedElementsBuff.Write(renderedElement) - if i < len(section.Elements)-1 { - renderedElementsBuff.WriteString("\n") - } + return nil, errors.Wrapf(err, "error while rendering section sectionTitle") } + renderedSectionElements, err := renderSectionElements(ctx, section.Elements) result := bytes.NewBuffer(nil) // select the appropriate template for the section var tmpl *template.Template - if section.Heading.Level == 1 { + if section.Level == 1 { tmpl = section1ContentTmpl - } else if section.Heading.Level == 2 { - tmpl = section2ContentTmpl } else { tmpl = otherSectionContentTmpl } - renderedHTMLHeading := template.HTML(renderedHeading) - renderedHTMLElements := template.HTML(renderedElementsBuff.String()) + renderedHTMLSectionTitle := template.HTML(renderedSectionTitle) + renderedHTMLElements := template.HTML(renderedSectionElements) err = tmpl.Execute(result, struct { - Class string - Heading template.HTML - Elements template.HTML + Class string + SectionTitle template.HTML + Elements template.HTML }{ - Class: "sect" + strconv.Itoa(section.Heading.Level-1), - Heading: renderedHTMLHeading, - Elements: renderedHTMLElements, + Class: "sect" + strconv.Itoa(section.Level), + SectionTitle: renderedHTMLSectionTitle, + Elements: renderedHTMLElements, }) if err != nil { return nil, errors.Wrapf(err, "error while rendering section") } - // log.Debugf("rendered section: %s", result.Bytes()) + log.Debugf("rendered section: %s", result.Bytes()) return result.Bytes(), nil } -func renderHeading(ctx asciidoc.Context, heading types.Heading) ([]byte, error) { +func renderSectionTitle(ctx *renderer.Context, level int, sectionTitle types.SectionTitle) ([]byte, error) { result := bytes.NewBuffer(nil) - renderedContent, err := processElement(ctx, heading.Content) + renderedContent, err := renderElement(ctx, sectionTitle.Content) if err != nil { - return nil, errors.Wrapf(err, "error while rendering heading content") + return nil, errors.Wrapf(err, "error while rendering sectionTitle content") } content := template.HTML(string(renderedContent)) - err = otherSectionHeaderTmpl.Execute(result, struct { + err = sectionHeaderTmpl.Execute(result, struct { Level int ID string Content template.HTML }{ - Level: heading.Level, - ID: heading.ID.Value, + Level: level + 1, + ID: sectionTitle.ID.Value, Content: content, }) if err != nil { - return nil, errors.Wrapf(err, "error while rendering heading") + return nil, errors.Wrapf(err, "error while rendering sectionTitle") } - // log.Debugf("rendered heading: %s", result.Bytes()) + // log.Debugf("rendered sectionTitle: %s", result.Bytes()) return result.Bytes(), nil } + +func renderSectionElements(ctx *renderer.Context, elements []types.DocElement) ([]byte, error) { + renderedElementsBuff := bytes.NewBuffer(nil) + for i, element := range elements { + renderedElement, err := renderElement(ctx, element) + if err != nil { + return nil, errors.Wrapf(err, "unable to render section element") + } + renderedElementsBuff.Write(renderedElement) + if i < len(elements)-1 { + renderedElementsBuff.WriteString("\n") + } + } + return renderedElementsBuff.Bytes(), nil +} diff --git a/renderer/html5/section_test.go b/renderer/html5/section_test.go index 226d239c..195bce9d 100644 --- a/renderer/html5/section_test.go +++ b/renderer/html5/section_test.go @@ -3,20 +3,21 @@ package html5_test import . "github.com/onsi/ginkgo" var _ = Describe("Rendering sections", func() { - Context("Headings only", func() { - It("heading level 1", func() { + Context("Sections only", func() { + + It("header section", func() { content := "= a title" - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `` verify(GinkgoT(), expected, content) }) - It("heading level 2", func() { + It("section level 1 alone", func() { content := "== a title" - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's <title> element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `<div class="sect1"> <h2 id="_a_title">a title</h2> <div class="sectionbody"> @@ -25,27 +26,17 @@ var _ = Describe("Rendering sections", func() { verify(GinkgoT(), expected, content) }) - It("heading level 3", func() { + It("section level 2 alone", func() { content := "=== a title" - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's <title> element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `<div class="sect2"> <h3 id="_a_title">a title</h3> </div>` verify(GinkgoT(), expected, content) }) - It("heading level 2 with just bold content", func() { - content := `== *2 spaces and bold content*` - expected := `<div class="sect1"> -<h2 id="__strong_2_spaces_and_bold_content_strong"><strong>2 spaces and bold content</strong></h2> -<div class="sectionbody"> -</div> -</div>` - verify(GinkgoT(), expected, content) - }) - - It("heading level 2 with just bold content", func() { + It("section level 1 with just bold content", func() { content := `== *2 spaces and bold content*` expected := `<div class="sect1"> <h2 id="__strong_2_spaces_and_bold_content_strong"><strong>2 spaces and bold content</strong></h2> @@ -55,7 +46,7 @@ var _ = Describe("Rendering sections", func() { verify(GinkgoT(), expected, content) }) - It("heading level 3 with nested bold content", func() { + It("section level 2 with nested bold content", func() { content := `=== a section title, with *bold content*` expected := `<div class="sect2"> <h3 id="_a_section_title_with_strong_bold_content_strong">a section title, with <strong>bold content</strong></h3> @@ -63,7 +54,7 @@ var _ = Describe("Rendering sections", func() { verify(GinkgoT(), expected, content) }) - It("heading level 2 with custom ID", func() { + It("section level 1 with custom ID", func() { content := `[#custom_id] == a section title, with *bold content*` expected := `<div class="sect1"> @@ -77,14 +68,14 @@ var _ = Describe("Rendering sections", func() { Context("Section with elements", func() { - It("heading level 2 with 2 paragraphs", func() { + It("section level 1 with 2 paragraphs", func() { content := `== a title and a first paragraph and a second paragraph` - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's <title> element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `<div class="sect1"> <h2 id="_a_title">a title</h2> <div class="sectionbody"> @@ -99,7 +90,19 @@ and a second paragraph` verify(GinkgoT(), expected, content) }) - It("preamble then section level 2", func() { + It("section with just a paragraph", func() { + content := `= a title + +a paragraph` + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element + expected := `<div class="paragraph"> +<p>a paragraph</p> +</div>` + verify(GinkgoT(), expected, content) + }) + + It("header with preamble then section level 1", func() { content := `= a title a preamble @@ -109,8 +112,8 @@ splitted in 2 paragraphs == section 1 with some text` - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's <title> element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `<div id="preamble"> <div class="sectionbody"> <div class="paragraph"> @@ -132,7 +135,7 @@ with some text` verify(GinkgoT(), expected, content) }) - It("preamble then 2 sections level 2", func() { + It("header with preamble then 2 sections level 1", func() { content := `= a title a preamble @@ -146,8 +149,8 @@ with some text == section 2 with some text, too` - // top-level heading is not rendered per-say, - // but the heading will be used to set the HTML page's <title> element + // top-level section is not rendered per-say, + // but the section will be used to set the HTML page's <title> element expected := `<div id="preamble"> <div class="sectionbody"> <div class="paragraph"> diff --git a/renderer/html5/string.go b/renderer/html5/string.go index ee562e2f..af5c19d6 100644 --- a/renderer/html5/string.go +++ b/renderer/html5/string.go @@ -4,7 +4,7 @@ import ( "bytes" "html/template" - asciidoc "github.com/bytesparadise/libasciidoc/context" + "github.com/bytesparadise/libasciidoc/renderer" "github.com/bytesparadise/libasciidoc/types" "github.com/pkg/errors" ) @@ -16,7 +16,7 @@ func init() { stringElementTmpl = newHTMLTemplate("string element", "{{.}}") } -func renderStringElement(ctx asciidoc.Context, str types.StringElement) ([]byte, error) { +func renderStringElement(ctx *renderer.Context, str types.StringElement) ([]byte, error) { result := bytes.NewBuffer(nil) err := stringElementTmpl.Execute(result, str.Content) if err != nil { diff --git a/renderer/options.go b/renderer/options.go index 9a11c9a2..8b1e508c 100644 --- a/renderer/options.go +++ b/renderer/options.go @@ -1,51 +1,52 @@ package renderer -import ( - "time" +import "time" - "github.com/pkg/errors" -) - -//Options the options when rendering a document -type Options map[string]interface{} +//Option the options when rendering a document +type Option func(ctx *Context) const ( //LastUpdated 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 - LastUpdated string = "LastUpdated" + keyLastUpdated string = "LastUpdated" //IncludeHeaderFooter a bool value to indicate if the header and footer should be rendered - IncludeHeaderFooter string = "IncludeHeaderFooter" + keyIncludeHeaderFooter string = "IncludeHeaderFooter" + // LastUpdatedFormat the time format for the `last updated` document attribute + LastUpdatedFormat string = "2006/01/02 15:04:05 MST" ) +// LastUpdated function to set the `last updated` option in the renderer context (default is `time.Now()`) +func LastUpdated(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 IncludeHeaderFooter(value bool) Option { + return func(ctx *Context) { + ctx.options[keyIncludeHeaderFooter] = value + } +} + // 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 (o Options) LastUpdated() (*string, error) { - if lastUpdated, ok := o[LastUpdated]; ok { - switch lastUpdated := lastUpdated.(type) { - case string: - return &lastUpdated, nil - case time.Time: - result := lastUpdated.Format("2006/01/02 15:04:05 MST") - return &result, nil - default: - return nil, errors.Errorf("`LastUpdated` option is not in a valid format: %T", lastUpdated) +func (ctx *Context) LastUpdated() string { + if lastUpdated, found := ctx.options[keyLastUpdated]; found { + if lastUpdated, typeMatch := lastUpdated.(time.Time); typeMatch { + return lastUpdated.Format(LastUpdatedFormat) } } - result := time.Now().Format("2006/01/02 15:04:05 MST") - return &result, nil + return time.Now().Format(LastUpdatedFormat) } // IncludeHeaderFooter returns the value of the 'LastUpdated' Option if it was present, // otherwise it returns `false`` -func (o Options) IncludeHeaderFooter() (*bool, error) { - if includeHeaderFooter, ok := o[IncludeHeaderFooter]; ok { - switch includeHeaderFooter := includeHeaderFooter.(type) { - case bool: - return &includeHeaderFooter, nil - default: - return nil, errors.Errorf("`IncludeHeaderFooter` option is not in a valid format: %T", includeHeaderFooter) +func (ctx *Context) IncludeHeaderFooter() bool { + if includeHeaderFooter, found := ctx.options[keyIncludeHeaderFooter]; found { + if includeHeaderFooter, typeMatch := includeHeaderFooter.(bool); typeMatch { + return includeHeaderFooter } } - result := false - return &result, nil + return false } diff --git a/types/document_attributes.go b/types/document_attributes.go index 05e6d201..99a5f3e2 100644 --- a/types/document_attributes.go +++ b/types/document_attributes.go @@ -1,12 +1,20 @@ package types +import "reflect" + // DocumentAttributes the document attributes type DocumentAttributes map[string]interface{} const ( - title string = "title" + title string = "doctitle" ) +// HasAuthors returns `true` if the document has one or more authors, `false` otherwise. +func (m DocumentAttributes) HasAuthors() bool { + _, author := m["author"] + return author +} + // GetTitle retrieves the document title in its metadata, or returns nil if the title was not specified func (m DocumentAttributes) GetTitle() *string { if t, ok := m[title]; ok { @@ -16,23 +24,33 @@ func (m DocumentAttributes) GetTitle() *string { return nil } -// SetTitle sets the title in the document attributes -func (m DocumentAttributes) SetTitle(t string) { - m[title] = t -} - -// AddAll adds all given attributes -func (m DocumentAttributes) AddAll(attributes map[string]interface{}) { - for name, value := range attributes { - // TODO: raise a warning if there was already a name/value - m[name] = value +// Add adds the given attribute +// TODO: raise a warning if there was already a name/value +func (m DocumentAttributes) Add(key string, value interface{}) { + // do not add nil values + if value == nil { + return + } + v := reflect.ValueOf(value) + k := v.Kind() + // if the argument is a pointer, then retrive the value it points to + if k == reflect.Ptr { + if v.Elem().IsValid() { + m[key] = v.Elem().Interface() + } + } else { + m[key] = value } } -// Add adds the given attribute -func (m DocumentAttributes) Add(a DocumentAttributeDeclaration) { - // TODO: raise a warning if there was already a name/value - m[a.Name] = a.Value +// AddAttribute adds the given attribute +// TODO: raise a warning if there was already a name/value +func (m DocumentAttributes) AddAttribute(attr *DocumentAttributeDeclaration) { + // do not add nil values + if attr == nil { + return + } + m.Add(attr.Name, attr.Value) } // Reset resets the given attribute @@ -40,11 +58,12 @@ func (m DocumentAttributes) Reset(a DocumentAttributeReset) { delete(m, a.Name) } -// Get gets the given value for the given attribute, or nil if none was found -func (m DocumentAttributes) Get(a DocumentAttributeSubstitution) interface{} { +// GetAsString gets the string value for the given key, or nil if none was found +func (m DocumentAttributes) GetAsString(key string) *string { // TODO: raise a warning if there was no entry found - if value, ok := m[a.Name]; ok { - return &value + if value, found := m[key]; found { + strValue := value.(string) + return &strValue } return nil } diff --git a/types/document_attributes_test.go b/types/document_attributes_test.go new file mode 100644 index 00000000..fdd3bef6 --- /dev/null +++ b/types/document_attributes_test.go @@ -0,0 +1,39 @@ +package types_test + +import ( + . "github.com/bytesparadise/libasciidoc/types" + . "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" +) + +var _ = Describe("Document Attributes", func() { + + It("normal value", func() { + // given + attributes := DocumentAttributes{} + // when + attributes.Add("foo", "bar") + // then + assert.Equal(GinkgoT(), "bar", attributes["foo"]) + }) + + It("pointer to value", func() { + // given + attributes := DocumentAttributes{} + // when + bar := "bar" + attributes.Add("foo", &bar) + // then + assert.Equal(GinkgoT(), "bar", attributes["foo"]) + }) + + It("nil value", func() { + // given + attributes := DocumentAttributes{} + // when + attributes.Add("foo", nil) + // then + _, found := attributes["foo"] + assert.False(GinkgoT(), found) + }) +}) diff --git a/types/grammar_types.go b/types/grammar_types.go index ba74191a..c719ce91 100644 --- a/types/grammar_types.go +++ b/types/grammar_types.go @@ -3,6 +3,7 @@ package types import ( "bytes" "fmt" + "os" "path/filepath" "strings" @@ -10,6 +11,7 @@ import ( "reflect" + "github.com/davecgh/go-spew/spew" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -49,47 +51,40 @@ type Visitor interface { // Document the top-level structure for a document type Document struct { - FrontMatter *FrontMatter - Attributes *DocumentAttributes - Elements []DocElement + Attributes DocumentAttributes + Elements []DocElement } // NewDocument initializes a new `Document` from the given lines -func NewDocument(frontmatter *FrontMatter, blocks []interface{}) (*Document, error) { +func NewDocument(frontmatter, header interface{}, blocks []interface{}) (*Document, error) { log.Debugf("Initializing a new Document with %d blocks(s)", len(blocks)) for i, block := range blocks { log.Debugf("Line #%d: %T", i, block) } elements := convertBlocksToDocElements(blocks) - document := &Document{Elements: elements} - document.initAttributes() - if frontmatter != nil { - document.Attributes.AddAll(frontmatter.Content) + document := &Document{ + Attributes: make(map[string]interface{}), + Elements: elements, } - return document, nil -} - -// initAttributes initializes the Document's attributes -func (d *Document) initAttributes() { - d.Attributes = &DocumentAttributes{} - // look-up the document title in the (first) section of level 1 - var headSection *Section - for _, element := range d.Elements { - if section, ok := element.(*Section); ok { - if section.Heading.Level == 1 { - headSection = section - } + if frontmatter != nil { + for attrName, attrValue := range frontmatter.(*FrontMatter).Content { + document.Attributes[attrName] = attrValue } } - if headSection != nil { - d.Attributes.SetTitle(headSection.Heading.PlainString()) + if header != nil { + for attrName, attrValue := range header.(*DocumentHeader).Content { + document.Attributes[attrName] = attrValue + } } - + return document, nil } // String implements the DocElement#String() method func (d *Document) String(indentLevel int) string { result := bytes.NewBuffer(nil) + for attrName, attrValue := range d.Attributes { + result.WriteString(fmt.Sprintf("\n%s: %v", attrName, attrValue)) + } for i := range d.Elements { result.WriteString(fmt.Sprintf("\n%s", d.Elements[i].String(0))) } @@ -97,6 +92,267 @@ func (d *Document) String(indentLevel int) string { return result.String() } +// ------------------------------------------ +// Document Header +// ------------------------------------------ + +// DocumentHeader the document header +type DocumentHeader struct { + Content DocumentAttributes +} + +// NewDocumentHeader initializes a new DocumentHeader +func NewDocumentHeader(header, authors, revision interface{}, otherAttributes []interface{}) (*DocumentHeader, error) { + content := DocumentAttributes{} + if header != nil { + content["doctitle"] = header.(*SectionTitle).PlainString() + } + log.Debugf("Initializing a new DocumentHeader with content '%v', authors '%+v' and revision '%+v'", content, authors, revision) + if authors != nil { + for i, author := range authors.([]*DocumentAuthor) { + if i == 0 { + content.Add("firstname", author.FirstName) + content.Add("middlename", author.MiddleName) + content.Add("lastname", author.LastName) + content.Add("author", author.FullName) + content.Add("authorinitials", author.Initials) + content.Add("email", author.Email) + } else { + content.Add(fmt.Sprintf("firstname_%d", i+1), author.FirstName) + content.Add(fmt.Sprintf("middlename_%d", i+1), author.MiddleName) + content.Add(fmt.Sprintf("lastname_%d", i+1), author.LastName) + content.Add(fmt.Sprintf("author_%d", i+1), author.FullName) + content.Add(fmt.Sprintf("authorinitials_%d", i+1), author.Initials) + content.Add(fmt.Sprintf("email_%d", i+1), author.Email) + } + } + } + if revision != nil { + rev := revision.(*DocumentRevision) + content.Add("revnumber", rev.Revnumber) + content.Add("revdate", rev.Revdate) + content.Add("revremark", rev.Revremark) + } + for _, attr := range otherAttributes { + if attr, ok := attr.(*DocumentAttributeDeclaration); ok { + content.AddAttribute(attr) + } + } + return &DocumentHeader{ + Content: content, + }, nil +} + +// ------------------------------------------ +// Document Author +// ------------------------------------------ + +// DocumentAuthor a document author +type DocumentAuthor struct { + FullName string + Initials string + FirstName *string + MiddleName *string + LastName *string + Email *string +} + +// NewDocumentAuthors converts the given authors into an array of `DocumentAuthor` +func NewDocumentAuthors(authors []interface{}) ([]*DocumentAuthor, error) { + log.Debugf("Initializing a new array of document authors from `%+v`", authors) + result := make([]*DocumentAuthor, len(authors)) + for i, author := range authors { + switch author.(type) { + case *DocumentAuthor: + result[i] = author.(*DocumentAuthor) + default: + return nil, errors.Errorf("unexpected type of author: %T", author) + } + } + return result, nil +} + +//NewDocumentAuthor initializes a new DocumentAuthor +func NewDocumentAuthor(namePart1, namePart2, namePart3, emailAddress interface{}) (*DocumentAuthor, error) { + var part1, part2, part3, email *string + var err error + if namePart1 != nil { + part1, err = Stringify(namePart1.([]interface{}), + func(s string) (string, error) { + return strings.TrimSpace(s), nil + }, + func(s string) (string, error) { + return strings.Replace(s, "_", " ", -1), nil + }, + ) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentAuthor") + } + } + if namePart2 != nil { + part2, err = Stringify(namePart2.([]interface{}), + func(s string) (string, error) { + return strings.TrimSpace(s), nil + }, + func(s string) (string, error) { + return strings.Replace(s, "_", " ", -1), nil + }, + ) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentAuthor") + } + } + if namePart3 != nil { + part3, err = Stringify(namePart3.([]interface{}), + func(s string) (string, error) { + return strings.TrimSpace(s), nil + }, + func(s string) (string, error) { + return strings.Replace(s, "_", " ", -1), nil + }, + ) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentAuthor") + } + } + if emailAddress != nil { + email, err = Stringify(emailAddress.([]interface{}), + func(s string) (string, error) { + return strings.TrimPrefix(s, "<"), nil + }, func(s string) (string, error) { + return strings.TrimSuffix(s, ">"), nil + }, func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentAuthor") + } + } + result := new(DocumentAuthor) + if part2 != nil && part3 != nil { + result.FirstName = part1 + result.MiddleName = part2 + result.LastName = part3 + result.FullName = fmt.Sprintf("%s %s %s", *part1, *part2, *part3) + result.Initials = initials(*result.FirstName, *result.MiddleName, *result.LastName) + } else if part2 != nil { + result.FirstName = part1 + result.LastName = part2 + result.FullName = fmt.Sprintf("%s %s", *part1, *part2) + result.Initials = initials(*result.FirstName, *result.LastName) + } else { + result.FirstName = part1 + result.FullName = *part1 + result.Initials = initials(*result.FirstName) + } + result.Email = email + log.Debugf("Initialized a new document author: `%v`", result.String()) + return result, nil +} + +func initials(firstPart string, otherParts ...string) string { + result := fmt.Sprintf("%s", firstPart[0:1]) + if otherParts != nil { + for _, otherPart := range otherParts { + result = result + otherPart[0:1] + } + } + return result +} + +func (a *DocumentAuthor) String() string { + email := "" + if a.Email != nil { + email = *a.Email + } + return fmt.Sprintf("%s (%s)", a.FullName, email) +} + +// ------------------------------------------ +// Document Revision +// ------------------------------------------ + +// DocumentRevision a document revision +type DocumentRevision struct { + Revnumber *string + Revdate *string + Revremark *string +} + +// NewDocumentRevision intializes a new DocumentRevision +func NewDocumentRevision(revnumber, revdate, revremark interface{}) (*DocumentRevision, error) { + // log.Debugf("Initializing document revision with revnumber=%v, revdate=%v, revremark=%v", revnumber, revdate, revremark) + // stringify, then remove the "v" prefix and trim spaces + var number, date, remark *string + var err error + if revnumber != nil { + number, err = Stringify(revnumber.([]interface{}), + func(s string) (string, error) { + return strings.TrimPrefix(s, "v"), nil + }, func(s string) (string, error) { + return strings.TrimPrefix(s, "V"), nil + }, func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentRevision") + } + } + if revdate != nil { + // stringify, then remove the "," prefix and trim spaces + date, err = Stringify(revdate.([]interface{}), func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentRevision") + } + // do not keep empty values + if *date == "" { + date = nil + } + } + if revremark != nil { + // then we need to strip the heading "," and spaces + remark, err = Stringify(revremark.([]interface{}), + func(s string) (string, error) { + return strings.TrimPrefix(s, ":"), nil + }, func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) + if err != nil { + return nil, errors.Wrapf(err, "error while initializing a DocumentRevision") + } + // do not keep empty values + if *remark == "" { + remark = nil + } + } + // log.Debugf("Initializing a new DocumentRevision with revnumber='%v', revdate='%v' and revremark='%v'", *n, *d, *r) + result := DocumentRevision{ + Revnumber: number, + Revdate: date, + Revremark: remark, + } + log.Debugf("Initialized a new document revision: `%s`", result.String()) + return &result, nil +} + +func (r *DocumentRevision) String() string { + number := "" + if r.Revnumber != nil { + number = *r.Revnumber + } + date := "" + if r.Revdate != nil { + date = *r.Revdate + } + remark := "" + if r.Revremark != nil { + remark = *r.Revremark + } + return fmt.Sprintf("%v, %v: %v", number, date, remark) +} + // ------------------------------------------ // Document Attributes // ------------------------------------------ @@ -109,11 +365,17 @@ type DocumentAttributeDeclaration struct { // NewDocumentAttributeDeclaration initializes a new DocumentAttributeDeclaration func NewDocumentAttributeDeclaration(name []interface{}, value []interface{}) (*DocumentAttributeDeclaration, error) { - attrName, err := Stringify(name) + attrName, err := Stringify(name, + func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) if err != nil { return nil, errors.Wrapf(err, "error while initializing a DocumentAttributeDeclaration") } - attrValue, err := Stringify(value) + attrValue, err := Stringify(value, + func(s string) (string, error) { + return strings.TrimSpace(s), nil + }) if err != nil { return nil, errors.Wrapf(err, "error while initializing a DocumentAttributeDeclaration") } @@ -184,6 +446,31 @@ func (a *DocumentAttributeSubstitution) Accept(v Visitor) error { return v.Visit(a) } +// ------------------------------------------ +// Preamble +// ------------------------------------------ + +// Preamble the structure for document Preamble +type Preamble struct { + Elements []DocElement +} + +// NewPreamble initializes a new Preamble from the given elements +func NewPreamble(elements []interface{}) (*Preamble, error) { + log.Debugf("Initialiazing new Preamble with %d elements", len(elements)) + + return &Preamble{Elements: convertBlocksToDocElements(elements)}, nil +} + +// String implements the DocElement#String() method +func (p *Preamble) String(indentLevel int) string { + result := bytes.NewBuffer(nil) + for _, element := range p.Elements { + result.WriteString(fmt.Sprintf("%s", element.String(indentLevel+1))) + } + return result.String() +} + // ------------------------------------------ // Front Matter // ------------------------------------------ @@ -209,30 +496,32 @@ func NewYamlFrontMatter(content []interface{}) (*FrontMatter, error) { } // ------------------------------------------ -// Section +// Sections // ------------------------------------------ // Section the structure for a section type Section struct { - Heading Heading - Elements []DocElement + Level int + SectionTitle SectionTitle + Elements []DocElement } -// NewSection initializes a new `Section` from the given heading and elements -func NewSection(heading *Heading, blocks []interface{}) (*Section, error) { +// NewSection initializes a new `Section` from the given section title and elements +func NewSection(level int, sectionTitle *SectionTitle, blocks []interface{}) (*Section, error) { // log.Debugf("Initializing a new Section with %d block(s)", len(blocks)) elements := convertBlocksToDocElements(blocks) - log.Debugf("Initialized a new Section of level %d with %d block(s)", heading.Level, len(blocks)) + log.Debugf("Initialized a new Section of level %d with %d block(s)", level, len(blocks)) return &Section{ - Heading: *heading, - Elements: elements, + Level: level, + SectionTitle: *sectionTitle, + Elements: elements, }, nil } // String implements the DocElement#String() method func (s *Section) String(indentLevel int) string { result := bytes.NewBuffer(nil) - result.WriteString(fmt.Sprintf("%s<Section %d> '%s'\n", indent(indentLevel), s.Heading.Level, s.Heading.Content.String(0))) + result.WriteString(fmt.Sprintf("%s<Section %d> '%s'\n", indent(indentLevel), s.Level, s.SectionTitle.Content.String(0))) for _, element := range s.Elements { result.WriteString(fmt.Sprintf("%s", element.String(indentLevel+1))) } @@ -240,41 +529,40 @@ func (s *Section) String(indentLevel int) string { } // ------------------------------------------ -// Heading +// SectionTitle // ------------------------------------------ -// Heading the structure for the headings -type Heading struct { +// SectionTitle the structure for the section titles +type SectionTitle struct { ID *ElementID - Level int Content *InlineContent } -// NewHeading initializes a new `Heading from the given level and content, with the optional attributes. +// NewSectionTitle initializes a new `SectionTitle`` from the given level and content, with the optional attributes. // In the attributes, only the ElementID is retained -func NewHeading(level int, inlineContent *InlineContent, attributes []interface{}) (*Heading, error) { +func NewSectionTitle(inlineContent *InlineContent, attributes []interface{}) (*SectionTitle, error) { // counting the lenght of the 'level' value (ie, the number of `=` chars) id, _, _ := newElementAttributes(attributes) - // make a default id from the heading's inline content + // make a default id from the sectionTitle's inline content if id == nil { replacement, err := ReplaceNonAlphanumerics(inlineContent, "_") if err != nil { - return nil, errors.Wrapf(err, "unable to generate default ID while instanciating a new Heading element") + return nil, errors.Wrapf(err, "unable to generate default ID while instanciating a new SectionTitle element") } id, _ = NewElementID(*replacement) } - heading := Heading{Level: level, Content: inlineContent, ID: id} - log.Debugf("Initialized a new Heading: %s", heading.String(0)) - return &heading, nil + sectionTitle := SectionTitle{Content: inlineContent, ID: id} + log.Debugf("Initialized a new SectionTitle: %s", sectionTitle.String(0)) + return §ionTitle, nil } // String implements the DocElement#String() method -func (h *Heading) String(indentLevel int) string { - return fmt.Sprintf("%s<Heading %d> %s", indent(indentLevel), h.Level, h.Content.String(0)) +func (h *SectionTitle) String(indentLevel int) string { + return fmt.Sprintf("%s<SectionTitle> %s", indent(indentLevel), h.Content.String(0)) } -// PlainString returns a plain string version of all elements in this Heading's Content, without any rendering -func (h *Heading) PlainString() string { +// PlainString returns a plain string version of all elements in this SectionTitle's Content, without any rendering +func (h *SectionTitle) PlainString() string { result := bytes.NewBuffer(nil) for i, element := range h.Content.Elements { result.WriteString(element.PlainString()) @@ -438,7 +726,8 @@ func NewParagraph(text []byte, lines []interface{}, attributes []interface{}) (* typedLines := make([]*InlineContent, 0) for _, line := range lines { - typedLines = append(typedLines, line.(*InlineContent)) + // each `line` element is an array with the actual `InlineContent` + `EOF` + typedLines = append(typedLines, line.([]interface{})[0].(*InlineContent)) } return &Paragraph{ Lines: typedLines, @@ -475,7 +764,10 @@ func NewInlineContent(text []byte, elements []interface{}) (*InlineContent, erro mergedInlineElements[i] = element.(InlineElement) } result := &InlineContent{Elements: mergedInlineElements} - log.Debugf("Initialized new InlineContent with %d element(s): %s", len(result.Elements), result.String(0)) + if log.GetLevel() == log.DebugLevel { + log.Debugf("Initialized a new InlineContent with %d elements:", len(result.Elements)) + spew.Fdump(os.Stdout, result) + } return result, nil } @@ -511,7 +803,7 @@ func (c *InlineContent) Accept(v Visitor) error { } err = v.AfterVisit(c) if err != nil { - return errors.Wrapf(err, "error while post-visiting heading") + return errors.Wrapf(err, "error while post-visiting sectionTitle") } return nil } @@ -684,16 +976,20 @@ type DelimitedBlock struct { // NewDelimitedBlock initializes a new `DelimitedBlock` of the given kind with the given content func NewDelimitedBlock(kind DelimitedBlockKind, content []interface{}) (*DelimitedBlock, error) { - c, err := Stringify(content) + blockContent, err := Stringify(content, + // remove "\n" or "\r\n", depending on the OS. + func(s string) (string, error) { + return strings.TrimSuffix(s, "\n"), nil + }, func(s string) (string, error) { + return strings.TrimSuffix(s, "\r"), nil + }) if err != nil { return nil, errors.Wrapf(err, "unable to initialize a new delimited block") } - // remove "\n" or "\r\n", depending on the OS. - blockContent := strings.TrimSuffix(strings.TrimSuffix(*c, "\n"), "\r") - log.Debugf("Initialized a new DelimitedBlock with content=`%s`", blockContent) + log.Debugf("Initialized a new DelimitedBlock with content=`%s`", *blockContent) return &DelimitedBlock{ Kind: kind, - Content: blockContent, + Content: *blockContent, }, nil } @@ -729,7 +1025,7 @@ type LiteralBlock struct { } // NewLiteralBlock initializes a new `DelimitedBlock` of the given kind with the given content, -// along with the given heading spaces +// along with the given sectionTitle spaces func NewLiteralBlock(spaces, content []interface{}) (*LiteralBlock, error) { // concatenates the spaces with the actual content in a single 'stringified' value // log.Debugf("Initializing a new LiteralBlock with spaces='%v' and content=`%v`", spaces, content) @@ -994,13 +1290,18 @@ func NewExternalLink(url, text []interface{}) (*ExternalLink, error) { if err != nil { return nil, errors.Wrapf(err, "failed to initialize a new ExternalLink element") } - textStr, err := Stringify(text) + textStr, err := Stringify(text, // remove "\n" or "\r\n", depending on the OS. + // remove heading "[" and traingin "]" + func(s string) (string, error) { + return strings.TrimPrefix(s, "["), nil + }, + func(s string) (string, error) { + return strings.TrimSuffix(s, "]"), nil + }) if err != nil { return nil, errors.Wrapf(err, "failed to initialize a new ExternalLink element") } - // the text includes the surrounding '[' and ']' which should be removed - trimmedText := strings.TrimPrefix(strings.TrimSuffix(*textStr, "]"), "[") - return &ExternalLink{URL: *urlStr, Text: trimmedText}, nil + return &ExternalLink{URL: *urlStr, Text: *textStr}, nil } // String implements the DocElement#String() method diff --git a/types/type_utils.go b/types/type_utils.go index 33958caf..4daa4c0c 100644 --- a/types/type_utils.go +++ b/types/type_utils.go @@ -6,6 +6,7 @@ import ( "unicode" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) func indent(indentLevel int) string { @@ -27,16 +28,25 @@ func toInlineElements(elements []interface{}) ([]InlineElement, error) { // convertBlocksToDocElements converts the given blocks to DocElement and exclude `BlankLine` func convertBlocksToDocElements(blocks []interface{}) []DocElement { - elements := make([]DocElement, len(blocks)) - j := 0 - for i := range blocks { - // exclude blank lines from here, we won't need them in the rendering anyways - if _, ok := blocks[i].(*BlankLine); !ok { - elements[j] = blocks[i].(DocElement) - j++ + log.Debugf("Converting %+v into DocElements...", blocks) + elements := make([]DocElement, 0) + for _, block := range blocks { + if b, ok := block.(DocElement); ok { + if preamble, ok := b.(*Preamble); ok { + if len(preamble.Elements) > 0 { + // exclude empty preamble + elements = append(elements, b) + } + } else if _, ok := b.(*BlankLine); !ok { + // exclude blank lines from here, we won't need them in the rendering anyways + elements = append(elements, b) + } + } else if block, ok := block.([]interface{}); ok { + result := convertBlocksToDocElements(block) + elements = append(elements, result...) } } - return elements[:j] // exclude allocated nil values + return elements // exclude allocated nil values } func merge(elements []interface{}, extraElements ...interface{}) []interface{} { @@ -93,8 +103,11 @@ func appendBuffer(elements []interface{}, buff *bytes.Buffer) ([]interface{}, *b return elements, buff } -//Stringify convert the given elements into a string -func Stringify(elements []interface{}) (*string, error) { +type StringifyFuncs func(s string) (string, error) + +//Stringify convert the given elements into a string, then applies the optional `funcs` to convert the string before returning it. +// These StringifyFuncs can be used to trim the content, for example +func Stringify(elements []interface{}, funcs ...StringifyFuncs) (*string, error) { mergedElements := merge(elements) b := make([]byte, 0) buff := bytes.NewBuffer(b) @@ -127,6 +140,13 @@ func Stringify(elements []interface{}) (*string, error) { } result := buff.String() + for _, f := range funcs { + var err error + result, err = f(result) + if err != nil { + return nil, errors.Wrapf(err, "Failed to postprocess the stringified content") + } + } // log.Debugf("stringified %v -> '%s' (%v characters)", elements, result, len(result)) return &result, nil } @@ -144,7 +164,7 @@ func NewReplaceNonAlphanumericsFunc(replacement string) NormalizationFunc { return func(source string) ([]byte, error) { buf := bytes.NewBuffer(nil) lastCharIsSpace := false - for _, r := range strings.TrimLeft(source, " ") { // ignore heading spaces + for _, r := range strings.TrimLeft(source, " ") { // ignore header spaces if unicode.Is(unicode.Letter, r) || unicode.Is(unicode.Number, r) { _, err := buf.WriteString(strings.ToLower(string(r))) if err != nil {