Skip to content

Commit

Permalink
feat(renderer): add XHTML5 support (#618)
Browse files Browse the repository at this point in the history
This adds support for XHTML5 output, tests, and the `-b` (or `--bakend`)
flag which can now be used like `-b [html,html5,xhtml,xhtml5]`.

Tests are enclosed, and we leverage as much
as we can from the HTML5 backend.

Also, added test cases to cover both HTML and XHTML5 backends,
as well as invalid backends.

Fixes #601
  • Loading branch information
gdamore authored Jun 17, 2020
1 parent b61a886 commit 4448584
Show file tree
Hide file tree
Showing 47 changed files with 8,767 additions and 21 deletions.
5 changes: 4 additions & 1 deletion cmd/libasciidoc/root_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func NewRootCmd() *cobra.Command {
var outputName string
var logLevel string
var css string
var backend string
var attributes []string

rootCmd := &cobra.Command{
Expand Down Expand Up @@ -55,8 +56,9 @@ func NewRootCmd() *cobra.Command {
configuration.WithFilename(sourcePath),
configuration.WithAttributes(attrs),
configuration.WithCSS(css),
configuration.WithBackEnd(backend),
configuration.WithHeaderFooter(!noHeaderFooter))
_, err := libasciidoc.ConvertFileToHTML(out, config)
_, err := libasciidoc.ConvertFile(out, config)
if err != nil {
return err
}
Expand All @@ -72,6 +74,7 @@ func NewRootCmd() *cobra.Command {
flags.StringVar(&logLevel, "log", "warning", "log level to set [debug|info|warning|error|fatal|panic]")
flags.StringVar(&css, "css", "", "the path to the CSS file to link to the document")
flags.StringArrayVarP(&attributes, "attribute", "a", []string{}, "a document attribute to set in the form of name, name!, or name=value pair")
flags.StringVarP(&backend, "backend", "b", "html5", "backend to format the file")
return rootCmd
}

Expand Down
32 changes: 23 additions & 9 deletions libasciidoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package libasciidoc

import (
"fmt"
"github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/xhtml5"
"io"
"os"
"time"
Expand All @@ -27,10 +29,10 @@ var (
BuildTime = ""
)

// ConvertFileToHTML converts the content of the given filename into an HTML document.
// ConvertFile converts the content of the given filename into an output document.
// The conversion result is written in the given writer `output`, whereas the document metadata (title, etc.) (or an error if a problem occurred) is returned
// as the result of the function call.
func ConvertFileToHTML(output io.Writer, config configuration.Configuration) (types.Metadata, error) {
// as the result of the function call. The output format is determined by config.Backend (HTML5 default).
func ConvertFile(output io.Writer, config configuration.Configuration) (types.Metadata, error) {
file, err := os.Open(config.Filename)
if err != nil {
return types.Metadata{}, errors.Wrapf(err, "error opening %s", config.Filename)
Expand All @@ -42,16 +44,27 @@ func ConvertFileToHTML(output io.Writer, config configuration.Configuration) (ty
return types.Metadata{}, errors.Wrapf(err, "error opening %s", config.Filename)
}
config.LastUpdated = stat.ModTime()
return ConvertToHTML(file, output, config)
return Convert(file, output, config)
}

// ConvertToHTML converts the content of the given reader `r` into a full HTML document, written in the given writer `output`.
// Returns an error if a problem occurred
func ConvertToHTML(r io.Reader, output io.Writer, config configuration.Configuration) (types.Metadata, error) {
// Convert converts the content of the given reader `r` into a full output document, written in the given writer `output`.
// Returns an error if a problem occurred. The default will be HTML5, but depends on the config.BackEnd value.
func Convert(r io.Reader, output io.Writer, config configuration.Configuration) (types.Metadata, error) {

var render func(*renderer.Context, types.Document, io.Writer) (types.Metadata, error)
switch config.BackEnd {
case "html", "html5", "":
render = html5.Render
case "xhtml", "xhtml5":
render = xhtml5.Render
default:
return types.Metadata{}, fmt.Errorf("backend '%s' not supported", config.BackEnd)
}

start := time.Now()
defer func() {
duration := time.Since(start)
log.Debugf("rendered the HTML output in %v", duration)
log.Debugf("rendered the output in %v", duration)
}()
log.Debugf("parsing the asciidoc source...")
doc, err := parser.ParseDocument(r, config) //, parser.Debug(true))
Expand All @@ -70,10 +83,11 @@ func ConvertToHTML(r io.Reader, output io.Writer, config configuration.Configura
}
// render
ctx := renderer.NewContext(doc, config)
metadata, err := html5.Render(ctx, doc, output)
metadata, err := render(ctx, doc, output)
if err != nil {
return types.Metadata{}, err
}
log.Debugf("Done processing document")
return metadata, nil

}
83 changes: 83 additions & 0 deletions libasciidoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,89 @@ Last updated {{.LastUpdated}}
configuration.WithCSS("path/to/style.css"),
configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, lastUpdated))
})

It("should render html", func() {
source := `= Story
Our story begins.`
expectedContent := `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="libasciidoc">
<title>Story</title>
</head>
<body class="article">
<div id="header">
<h1>Story</h1>
</div>
<div id="content">
<div class="paragraph">
<p>Our story begins.</p>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated {{.LastUpdated}}
</div>
</div>
</body>
</html>`
Expect(Render(source,
configuration.WithBackEnd("html5"),
configuration.WithLastUpdated(lastUpdated),
configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, lastUpdated))
})

It("should render xhtml", func() {
source := `= Story
Our story begins.`
expectedContent := `<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="generator" content="libasciidoc"/>
<title>Story</title>
</head>
<body class="article">
<div id="header">
<h1>Story</h1>
</div>
<div id="content">
<div class="paragraph">
<p>Our story begins.</p>
</div>
</div>
<div id="footer">
<div id="footer-text">
Last updated {{.LastUpdated}}
</div>
</div>
</body>
</html>`
Expect(Render(source,
configuration.WithBackEnd("xhtml5"),
configuration.WithLastUpdated(lastUpdated),
configuration.WithHeaderFooter(true))).To(MatchHTMLTemplate(expectedContent, lastUpdated))
})

It("should fail given bogus backend", func() {
source := `= Story
Our story begins.`
doc, err := Render(source,
configuration.WithBackEnd("wordperfect"),
configuration.WithLastUpdated(lastUpdated),
configuration.WithHeaderFooter(true))
Expect(doc).To(BeEmpty())
Expect(err).To(MatchError("backend 'wordperfect' not supported"))
})

})

})
Expand Down
8 changes: 8 additions & 0 deletions pkg/configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Configuration struct {
LastUpdated time.Time
IncludeHeaderFooter bool
CSS string
BackEnd string
macros map[string]MacroTemplate
}

Expand Down Expand Up @@ -90,6 +91,13 @@ func WithCSS(href string) Setting {
}
}

// WithBackEnd sets the backend format, valid values are "html", "html5", "xhtml", "xhtml5", and "" (defaults to html5)
func WithBackEnd(backend string) Setting {
return func(config *Configuration) {
config.BackEnd = backend
}
}

// WithFilename function to set the `filename` setting in the config
func WithFilename(filename string) Setting {
return func(config *Configuration) {
Expand Down
9 changes: 6 additions & 3 deletions pkg/renderer/sgml/paragraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,13 @@ func (r *sgmlRenderer) renderLines(ctx *renderer.Context, lines [][]interface{},
// log.Debugf("rendered line is not the last one in the slice")
var err error
if linesRenderer.hardBreaks {
_, err = buf.WriteString("<br>\n") // TODO: linebreak template
} else {
_, err = buf.WriteString("\n")
if br, err := r.renderLineBreak(); err != nil {
return nil, errors.Wrap(err, "unable to render hardbreak")
} else if _, err = buf.Write(br); err != nil {
return nil, errors.Wrap(err, "unable to write hardbreak")
}
}
_, err = buf.WriteString("\n")
if err != nil {
return nil, errors.Wrap(err, "unable to render lines")
}
Expand Down
91 changes: 91 additions & 0 deletions pkg/renderer/sgml/xhtml5/article.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
= AsciiDoc Article Title
Firstname Lastname <author@asciidoctor.org>
1.0, July 29, 2014, Asciidoctor 1.5 article template
:toc:
:icons: font
:quick-uri: https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/

Content entered directly below the header but before the first section heading is called the preamble.

== First level heading

This is a paragraph with a *bold* word and an _italicized_ word.

.Image caption
image::image-file-name.png[I am the image alt text.]

This is another paragraph.footnote:[I am footnote text and will be displayed at the bottom of the article.]

=== Second level heading

.Unordered list title
* list item 1
** nested list item
*** nested nested list item 1
*** nested nested list item 2
* list item 2

This is a paragraph.

.Example block title
====
Content in an example block is subject to normal substitutions.
====

.Sidebar title
****
Sidebars contain aside text and are subject to normal substitutions.
****

==== Third level heading

[#id-for-listing-block]
.Listing block title
----
Content in a listing block is subject to verbatim substitutions.
Listing block content is commonly used to preserve code input.
----

===== Fourth level heading

.Table title
|===
|Column heading 1 |Column heading 2

|Column 1, row 1
|Column 2, row 1

|Column 1, row 2
|Column 2, row 2
|===

====== Fifth level heading

[quote, firstname lastname, movie title]
____
I am a block quote or a prose excerpt.
I am subject to normal substitutions.
____

[verse, firstname lastname, poem title and more]
____
I am a verse block.
Indents and endlines are preserved in verse blocks.
____

== First level heading

TIP: There are five admonition labels: Tip, Note, Important, Caution and Warning.

// I am a comment and won't be rendered.

. ordered list item
.. nested ordered list item
. ordered list item

The text at the end of this sentence is cross referenced to <<_third_level_heading,the third level heading>>

== First level heading

This is a link to the https://asciidoctor.org/docs/user-manual/[Asciidoctor User Manual].
This is an attribute reference {quick-uri}[which links this text to the Asciidoctor Quick Reference Guide].
32 changes: 32 additions & 0 deletions pkg/renderer/sgml/xhtml5/article_adoc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package xhtml5_test

import (
"bufio"
"bytes"
"github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/xhtml5"
"os"

"github.com/bytesparadise/libasciidoc/pkg/configuration"
"github.com/bytesparadise/libasciidoc/pkg/parser"
"github.com/bytesparadise/libasciidoc/pkg/renderer"
"github.com/davecgh/go-spew/spew"
. "github.com/onsi/ginkgo" //nolint golint
. "github.com/onsi/gomega" //nolint golint
)

var _ = Describe("article.adoc", func() {

It("should render without failure", func() {
f, err := os.Open("article.adoc")
Expect(err).ToNot(HaveOccurred())
reader := bufio.NewReader(f)
config := configuration.NewConfiguration()
doc, err := parser.ParseDocument(reader, config)
Expect(err).ToNot(HaveOccurred())
GinkgoT().Logf("actual document: `%s`", spew.Sdump(doc))
buff := &bytes.Buffer{}
ctx := renderer.NewContext(doc, config)
_, err = xhtml5.Render(ctx, doc, buff)
Expect(err).ToNot(HaveOccurred())
})
})
6 changes: 6 additions & 0 deletions pkg/renderer/sgml/xhtml5/blank_line.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package xhtml5

const (
lineBreakTmpl = "<br/>"
blankLineTmpl = "\n\n"
)
Loading

0 comments on commit 4448584

Please sign in to comment.