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 {