Skip to content

Commit 182f139

Browse files
committed
Add Pandoc support, refactor external helpers
Recognize the Pandoc format under the file extension .pandoc or .pdc, and shell out to pandoc as an external helper to format Pandoc content. Add a configuration option, externalHelperArguments, that can be used to override the additional arguments passed to external helpers. Refactor out repeated code with external helpers. Change the error output formatting. I did not see any of the external helpers print the string "<input>" to represent stdin as a file; just prepending the file name to error output is more general and doesn't sacrifice that much in terms of readability.
1 parent 23ba779 commit 182f139

File tree

7 files changed

+125
-67
lines changed

7 files changed

+125
-67
lines changed

docs/content/content-management/formats.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ date: 2017-01-10
66
publishdate: 2017-01-10
77
lastmod: 2017-04-06
88
categories: [content management]
9-
keywords: [markdown,asciidoc,mmark,content format]
9+
keywords: [markdown,asciidoc,mmark,pandoc,content format]
1010
menu:
1111
docs:
1212
parent: "content-management"
@@ -195,12 +195,33 @@ With this setup, everything is in place for a natural usage of MathJax on pages
195195

196196
## Additional Formats Through External Helpers
197197

198-
Hugo has new concept called _external helpers_. It means that you can write your content using [Asciidoc][ascii], [reStructuredText][rest]. If you have files with associated extensions, Hugo will call external commands to generate the content. ([See the Hugo source code for external helpers][helperssource].)
198+
Hugo has a new concept called _external helpers_. It means that you can write your content using [Asciidoc][ascii], [reStructuredText][rest], or [pandoc]. If you have files with associated extensions, Hugo will call external commands to generate the content. ([See the Hugo source code for external helpers][helperssource].)
199199

200200
For example, for Asciidoc files, Hugo will try to call the `asciidoctor` or `asciidoc` command. This means that you will have to install the associated tool on your machine to be able to use these formats. ([See the Asciidoctor docs for installation instructions](http://asciidoctor.org/docs/install-toolchain/)).
201201

202202
To use these formats, just use the standard extension and the front matter exactly as you would do with natively supported `.md` files.
203203

204+
Hugo attempts to pass reasonable default arguments to these external helpers by default:
205+
206+
- `asciidoc`: `--no-header-footer --safe -`
207+
- `asciidoctor`: `--no-header-footer --safe --trace -`
208+
- `rst2html`: `--leave-comments --initial-header-level=2`
209+
- `pandoc`: (none)
210+
211+
You can override these arguments by adding entries under the `externalHelperArguments` variable in your [site configuration][config]. For example, if you wanted to use `pandoc` to render your markup with the flag `--strip-comments`, you might put the following in `config.toml`:
212+
213+
```
214+
[externalHelperArguments]
215+
pandoc: --strip-comments
216+
```
217+
218+
Or in `config.yaml`:
219+
220+
```
221+
externalHelperArguments:
222+
pandoc: --strip-comments
223+
```
224+
204225
{{% warning "Performance of External Helpers" %}}
205226
Because additional formats are external commands generation performance will rely heavily on the performance of the external tool you are using. As this feature is still in its infancy, feedback is welcome.
206227
{{% /warning %}}
@@ -235,6 +256,7 @@ Markdown syntax is simple enough to learn in a single sitting. The following are
235256
[mmark]: https://github.com/miekg/mmark
236257
[mmarkgh]: https://github.com/miekg/mmark/wiki/Syntax
237258
[org]: http://orgmode.org/
259+
[pandoc]: http://www.pandoc.org/
238260
[Pygments]: http://pygments.org/
239261
[rest]: http://docutils.sourceforge.net/rst.html
240262
[sc]: /content-management/shortcodes/

helpers/content.go

Lines changed: 85 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ type ContentSpec struct {
4646
footnoteReturnLinkContents string
4747
// SummaryLength is the length of the summary that Hugo extracts from a content.
4848
summaryLength int
49+
// map of format to args that should be passed to the external helper,
50+
// as either a string parsed as a list of whitespace-separated tokens
51+
// or as list of strings
52+
externalHelperArguments map[string]interface{}
4953

5054
Highlight func(code, lang, optsStr string) (string, error)
5155
defatultPygmentsOpts map[string]string
@@ -61,6 +65,7 @@ func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
6165
footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"),
6266
footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"),
6367
summaryLength: cfg.GetInt("summaryLength"),
68+
externalHelperArguments: cfg.GetStringMap("externalHelperArguments"),
6469

6570
cfg: cfg,
6671
}
@@ -439,6 +444,23 @@ type RenderingContext struct {
439444
Cfg config.Provider
440445
}
441446

447+
func (c ContentSpec) getExternalHelperArguments(fmt string) []string {
448+
args := c.externalHelperArguments[fmt]
449+
if args == nil {
450+
return nil
451+
}
452+
switch args := args.(type) {
453+
case []string:
454+
return args
455+
case string:
456+
return strings.Fields(args)
457+
default:
458+
jww.ERROR.Printf("Could not understand external helper arguments %v", args)
459+
jww.ERROR.Println("Falling back to default arguments")
460+
return nil
461+
}
462+
}
463+
442464
// RenderBytes renders a []byte.
443465
func (c ContentSpec) RenderBytes(ctx *RenderingContext) []byte {
444466
switch ctx.PageFmt {
@@ -447,13 +469,15 @@ func (c ContentSpec) RenderBytes(ctx *RenderingContext) []byte {
447469
case "markdown":
448470
return c.markdownRender(ctx)
449471
case "asciidoc":
450-
return getAsciidocContent(ctx)
472+
return getAsciidocContent(ctx, c.getExternalHelperArguments("asciidoc"))
451473
case "mmark":
452474
return c.mmarkRender(ctx)
453475
case "rst":
454-
return getRstContent(ctx)
476+
return getRstContent(ctx, c.getExternalHelperArguments("rst"))
455477
case "org":
456478
return orgRender(ctx, c)
479+
case "pandoc":
480+
return getPandocContent(ctx, c.getExternalHelperArguments("pandoc"))
457481
}
458482
}
459483

@@ -578,11 +602,6 @@ func getAsciidocExecPath() string {
578602
return path
579603
}
580604

581-
// HasAsciidoc returns whether Asciidoc is installed on this computer.
582-
func HasAsciidoc() bool {
583-
return getAsciidocExecPath() != ""
584-
}
585-
586605
func getAsciidoctorExecPath() string {
587606
path, err := exec.LookPath("asciidoctor")
588607
if err != nil {
@@ -591,56 +610,38 @@ func getAsciidoctorExecPath() string {
591610
return path
592611
}
593612

594-
// HasAsciidoctor returns whether Asciidoctor is installed on this computer.
595-
func HasAsciidoctor() bool {
596-
return getAsciidoctorExecPath() != ""
613+
// HasAsciidoc returns whether Asciidoc or Asciidoctor is installed on this computer.
614+
func HasAsciidoc() bool {
615+
return (getAsciidoctorExecPath() != "" ||
616+
getAsciidocExecPath() != "")
597617
}
598618

599619
// getAsciidocContent calls asciidoctor or asciidoc as an external helper
600620
// to convert AsciiDoc content to HTML.
601-
func getAsciidocContent(ctx *RenderingContext) []byte {
602-
content := ctx.Content
603-
cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
604-
621+
func getAsciidocContent(ctx *RenderingContext, args []string) []byte {
605622
var isAsciidoctor bool
606623
path := getAsciidoctorExecPath()
607624
if path == "" {
608625
path = getAsciidocExecPath()
609626
if path == "" {
610627
jww.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n",
611628
" Leaving AsciiDoc content unrendered.")
612-
return content
629+
return ctx.Content
613630
}
614631
} else {
615632
isAsciidoctor = true
616633
}
617634

618635
jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
619-
args := []string{"--no-header-footer", "--safe"}
620-
if isAsciidoctor {
621-
// asciidoctor-specific arg to show stack traces on errors
622-
args = append(args, "--trace")
623-
}
624-
args = append(args, "-")
625-
cmd := exec.Command(path, args...)
626-
cmd.Stdin = bytes.NewReader(cleanContent)
627-
var out, cmderr bytes.Buffer
628-
cmd.Stdout = &out
629-
cmd.Stderr = &cmderr
630-
err := cmd.Run()
631-
// asciidoctor has exit code 0 even if there are errors in stderr
632-
// -> log stderr output regardless of state of err
633-
for _, item := range strings.Split(string(cmderr.Bytes()), "\n") {
634-
item := strings.TrimSpace(item)
635-
if item != "" {
636-
jww.ERROR.Println(strings.Replace(item, "<stdin>", ctx.DocumentName, 1))
636+
if args == nil {
637+
args = []string{"--no-header-footer", "--safe"}
638+
if isAsciidoctor {
639+
// asciidoctor-specific arg to show stack traces on errors
640+
args = append(args, "--trace")
637641
}
642+
args = append(args, "-")
638643
}
639-
if err != nil {
640-
jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
641-
}
642-
643-
return normalizeExternalHelperLineFeeds(out.Bytes())
644+
return externallyRenderContent(ctx, path, args)
644645
}
645646

646647
// HasRst returns whether rst2html is installed on this computer.
@@ -672,41 +673,23 @@ func getPythonExecPath() string {
672673

673674
// getRstContent calls the Python script rst2html as an external helper
674675
// to convert reStructuredText content to HTML.
675-
func getRstContent(ctx *RenderingContext) []byte {
676-
content := ctx.Content
677-
cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
678-
676+
func getRstContent(ctx *RenderingContext, args []string) []byte {
679677
python := getPythonExecPath()
680678
path := getRstExecPath()
681679

682680
if path == "" {
683681
jww.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
684682
" Leaving reStructuredText content unrendered.")
685-
return content
683+
return ctx.Content
686684

687685
}
688-
689686
jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
690-
cmd := exec.Command(python, path, "--leave-comments", "--initial-header-level=2")
691-
cmd.Stdin = bytes.NewReader(cleanContent)
692-
var out, cmderr bytes.Buffer
693-
cmd.Stdout = &out
694-
cmd.Stderr = &cmderr
695-
err := cmd.Run()
696-
// By default rst2html exits w/ non-zero exit code only if severe, i.e.
697-
// halting errors occurred. -> log stderr output regardless of state of err
698-
for _, item := range strings.Split(string(cmderr.Bytes()), "\n") {
699-
item := strings.TrimSpace(item)
700-
if item != "" {
701-
jww.ERROR.Println(strings.Replace(item, "<stdin>", ctx.DocumentName, 1))
702-
}
703-
}
704-
if err != nil {
705-
jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
687+
if args == nil {
688+
args = []string{path, "--leave-comments", "--initial-header-level=2"}
689+
} else {
690+
args = append([]string{path}, args...)
706691
}
707-
708-
result := normalizeExternalHelperLineFeeds(out.Bytes())
709-
692+
result := externallyRenderContent(ctx, python, args)
710693
// TODO(bep) check if rst2html has a body only option.
711694
bodyStart := bytes.Index(result, []byte("<body>\n"))
712695
if bodyStart < 0 {
@@ -724,9 +707,48 @@ func getRstContent(ctx *RenderingContext) []byte {
724707
return result[bodyStart+7 : bodyEnd]
725708
}
726709

710+
// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
711+
func getPandocContent(ctx *RenderingContext, args []string) []byte {
712+
content := ctx.Content
713+
path, err := exec.LookPath("pandoc")
714+
if err != nil {
715+
jww.ERROR.Println("pandoc not found in $PATH: Please install.\n",
716+
" Leaving pandoc content unrendered.")
717+
return content
718+
}
719+
// the default args we want to pass to pandoc are already empty, so we
720+
// don't need to check for args == nil
721+
return externallyRenderContent(ctx, path, args)
722+
}
723+
727724
func orgRender(ctx *RenderingContext, c ContentSpec) []byte {
728725
content := ctx.Content
729726
cleanContent := bytes.Replace(content, []byte("# more"), []byte(""), 1)
730727
return goorgeous.Org(cleanContent,
731728
c.getHTMLRenderer(blackfriday.HTML_TOC, ctx))
732729
}
730+
731+
func externallyRenderContent(ctx *RenderingContext, path string, args []string) []byte {
732+
content := ctx.Content
733+
cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
734+
735+
cmd := exec.Command(path, args...)
736+
cmd.Stdin = bytes.NewReader(cleanContent)
737+
var out, cmderr bytes.Buffer
738+
cmd.Stdout = &out
739+
cmd.Stderr = &cmderr
740+
err := cmd.Run()
741+
// Most external helpers exit w/ non-zero exit code only if severe, i.e.
742+
// halting errors occurred. -> log stderr output regardless of state of err
743+
for _, item := range strings.Split(string(cmderr.Bytes()), "\n") {
744+
item := strings.TrimSpace(item)
745+
if item != "" {
746+
jww.ERROR.Printf("%s: %s", ctx.DocumentName, item)
747+
}
748+
}
749+
if err != nil {
750+
jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
751+
}
752+
753+
return normalizeExternalHelperLineFeeds(out.Bytes())
754+
}

helpers/general.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func GuessType(in string) string {
7878
return "mmark"
7979
case "rst":
8080
return "rst"
81+
case "pandoc", "pdc":
82+
return "pandoc"
8183
case "html", "htm":
8284
return "html"
8385
case "org":

helpers/general_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ func TestGuessType(t *testing.T) {
3434
{"adoc", "asciidoc"},
3535
{"ad", "asciidoc"},
3636
{"rst", "rst"},
37+
{"pandoc", "pandoc"},
38+
{"pdc", "pandoc"},
3739
{"mmark", "mmark"},
3840
{"html", "html"},
3941
{"htm", "html"},

hugolib/handler_page.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func init() {
2525
RegisterHandler(new(htmlHandler))
2626
RegisterHandler(new(asciidocHandler))
2727
RegisterHandler(new(rstHandler))
28+
RegisterHandler(new(pandocHandler))
2829
RegisterHandler(new(mmarkHandler))
2930
RegisterHandler(new(orgHandler))
3031
}
@@ -104,6 +105,15 @@ func (h rstHandler) PageConvert(p *Page) HandledResult {
104105
return commonConvert(p)
105106
}
106107

108+
type pandocHandler struct {
109+
basicPageHandler
110+
}
111+
112+
func (h pandocHandler) Extensions() []string { return []string{"pandoc", "pdc"} }
113+
func (h pandocHandler) PageConvert(p *Page) HandledResult {
114+
return commonConvert(p)
115+
}
116+
107117
type mmarkHandler struct {
108118
basicPageHandler
109119
}

hugolib/page_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
561561
}{
562562
{"md", func() bool { return true }},
563563
{"mmark", func() bool { return true }},
564-
{"ad", func() bool { return helpers.HasAsciidoctor() || helpers.HasAsciidoc() }},
564+
{"ad", func() bool { return helpers.HasAsciidoc() }},
565565
// TODO(bep) figure a way to include this without too much work.{"html", func() bool { return true }},
566566
{"rst", func() bool { return helpers.HasRst() }},
567567
}

hugolib/shortcode_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ tags:
565565
th := testHelper{s.Cfg, s.Fs, t}
566566

567567
for _, test := range tests {
568-
if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoctor() && !helpers.HasAsciidoc() {
568+
if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
569569
fmt.Println("Skip Asciidoc test case as no Asciidoc present.")
570570
continue
571571
} else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() {

0 commit comments

Comments
 (0)