shithub: hugo

Download patch

ref: 5f6b6ec68936ebbbf590894c02a1a3ecad30735f
parent: 366ee4d8da1c2b0c1751e9bf6d54638439735296
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Fri Aug 16 11:55:03 EDT 2019

Prepare for Goldmark

This commmit prepares for the addition of Goldmark as the new Markdown renderer in Hugo.

This introduces a new `markup` package with some common interfaces and each implementation in its own package.

See #5963

diff: cannot open b/markup/asciidoc//null: file does not exist: 'b/markup/asciidoc//null' diff: cannot open b/markup/blackfriday//null: file does not exist: 'b/markup/blackfriday//null' diff: cannot open b/markup/converter//null: file does not exist: 'b/markup/converter//null' diff: cannot open b/markup/internal//null: file does not exist: 'b/markup/internal//null' diff: cannot open b/markup/mmark//null: file does not exist: 'b/markup/mmark//null' diff: cannot open b/markup/org//null: file does not exist: 'b/markup/org//null' diff: cannot open b/markup/pandoc//null: file does not exist: 'b/markup/pandoc//null' diff: cannot open b/markup/rst//null: file does not exist: 'b/markup/rst//null' diff: cannot open b/markup//null: file does not exist: 'b/markup//null'
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -223,7 +223,7 @@
 		return nil, err
 	}
 
-	contentSpec, err := helpers.NewContentSpec(cfg.Language)
+	contentSpec, err := helpers.NewContentSpec(cfg.Language, logger, ps.BaseFs.Content.Fs)
 	if err != nil {
 		return nil, err
 	}
@@ -277,7 +277,7 @@
 		return nil, err
 	}
 
-	d.ContentSpec, err = helpers.NewContentSpec(l)
+	d.ContentSpec, err = helpers.NewContentSpec(l, d.Log, d.BaseFs.Content.Fs)
 	if err != nil {
 		return nil, err
 	}
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -19,22 +19,18 @@
 
 import (
 	"bytes"
-	"fmt"
 	"html/template"
-	"os/exec"
-	"runtime"
 	"unicode"
 	"unicode/utf8"
 
-	"github.com/gohugoio/hugo/common/maps"
-	"github.com/gohugoio/hugo/hugolib/filesystems"
-	"github.com/niklasfasching/go-org/org"
+	"github.com/gohugoio/hugo/common/loggers"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
+	"github.com/gohugoio/hugo/markup"
+
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/config"
-	"github.com/miekg/mmark"
-	"github.com/mitchellh/mapstructure"
-	"github.com/russross/blackfriday"
 	"github.com/spf13/afero"
 	jww "github.com/spf13/jwalterweatherman"
 
@@ -52,9 +48,9 @@
 
 // ContentSpec provides functionality to render markdown content.
 type ContentSpec struct {
-	BlackFriday                *BlackFriday
-	footnoteAnchorPrefix       string
-	footnoteReturnLinkContents string
+	Converters       markup.ConverterProvider
+	MardownConverter converter.Converter // Markdown converter with no document context
+
 	// SummaryLength is the length of the summary that Hugo extracts from a content.
 	summaryLength int
 
@@ -70,16 +66,13 @@
 
 // NewContentSpec returns a ContentSpec initialized
 // with the appropriate fields from the given config.Provider.
-func NewContentSpec(cfg config.Provider) (*ContentSpec, error) {
-	bf := newBlackfriday(cfg.GetStringMap("blackfriday"))
+func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero.Fs) (*ContentSpec, error) {
+
 	spec := &ContentSpec{
-		BlackFriday:                bf,
-		footnoteAnchorPrefix:       cfg.GetString("footnoteAnchorPrefix"),
-		footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"),
-		summaryLength:              cfg.GetInt("summaryLength"),
-		BuildFuture:                cfg.GetBool("buildFuture"),
-		BuildExpired:               cfg.GetBool("buildExpired"),
-		BuildDrafts:                cfg.GetBool("buildDrafts"),
+		summaryLength: cfg.GetInt("summaryLength"),
+		BuildFuture:   cfg.GetBool("buildFuture"),
+		BuildExpired:  cfg.GetBool("buildExpired"),
+		BuildDrafts:   cfg.GetBool("buildDrafts"),
 
 		Cfg: cfg,
 	}
@@ -109,99 +102,29 @@
 		spec.Highlight = h.chromaHighlight
 	}
 
-	return spec, nil
-}
-
-// BlackFriday holds configuration values for BlackFriday rendering.
-type BlackFriday struct {
-	Smartypants           bool
-	SmartypantsQuotesNBSP bool
-	AngledQuotes          bool
-	Fractions             bool
-	HrefTargetBlank       bool
-	NofollowLinks         bool
-	NoreferrerLinks       bool
-	SmartDashes           bool
-	LatexDashes           bool
-	TaskLists             bool
-	PlainIDAnchors        bool
-	Extensions            []string
-	ExtensionsMask        []string
-	SkipHTML              bool
-}
-
-// NewBlackfriday creates a new Blackfriday filled with site config or some sane defaults.
-func newBlackfriday(config map[string]interface{}) *BlackFriday {
-	defaultParam := map[string]interface{}{
-		"smartypants":           true,
-		"angledQuotes":          false,
-		"smartypantsQuotesNBSP": false,
-		"fractions":             true,
-		"hrefTargetBlank":       false,
-		"nofollowLinks":         false,
-		"noreferrerLinks":       false,
-		"smartDashes":           true,
-		"latexDashes":           true,
-		"plainIDAnchors":        true,
-		"taskLists":             true,
-		"skipHTML":              false,
+	converterProvider, err := markup.NewConverterProvider(converter.ProviderConfig{
+		Cfg:       cfg,
+		ContentFs: contentFs,
+		Logger:    logger,
+		Highlight: spec.Highlight,
+	})
+	if err != nil {
+		return nil, err
 	}
 
-	maps.ToLower(defaultParam)
-
-	siteConfig := make(map[string]interface{})
-
-	for k, v := range defaultParam {
-		siteConfig[k] = v
+	spec.Converters = converterProvider
+	p := converterProvider.Get("markdown")
+	conv, err := p.New(converter.DocumentContext{})
+	if err != nil {
+		return nil, err
 	}
+	spec.MardownConverter = conv
 
-	for k, v := range config {
-		siteConfig[k] = v
-	}
-
-	combinedConfig := &BlackFriday{}
-	if err := mapstructure.Decode(siteConfig, combinedConfig); err != nil {
-		jww.FATAL.Printf("Failed to get site rendering config\n%s", err.Error())
-	}
-
-	return combinedConfig
+	return spec, nil
 }
 
-var blackfridayExtensionMap = map[string]int{
-	"noIntraEmphasis":        blackfriday.EXTENSION_NO_INTRA_EMPHASIS,
-	"tables":                 blackfriday.EXTENSION_TABLES,
-	"fencedCode":             blackfriday.EXTENSION_FENCED_CODE,
-	"autolink":               blackfriday.EXTENSION_AUTOLINK,
-	"strikethrough":          blackfriday.EXTENSION_STRIKETHROUGH,
-	"laxHtmlBlocks":          blackfriday.EXTENSION_LAX_HTML_BLOCKS,
-	"spaceHeaders":           blackfriday.EXTENSION_SPACE_HEADERS,
-	"hardLineBreak":          blackfriday.EXTENSION_HARD_LINE_BREAK,
-	"tabSizeEight":           blackfriday.EXTENSION_TAB_SIZE_EIGHT,
-	"footnotes":              blackfriday.EXTENSION_FOOTNOTES,
-	"noEmptyLineBeforeBlock": blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK,
-	"headerIds":              blackfriday.EXTENSION_HEADER_IDS,
-	"titleblock":             blackfriday.EXTENSION_TITLEBLOCK,
-	"autoHeaderIds":          blackfriday.EXTENSION_AUTO_HEADER_IDS,
-	"backslashLineBreak":     blackfriday.EXTENSION_BACKSLASH_LINE_BREAK,
-	"definitionLists":        blackfriday.EXTENSION_DEFINITION_LISTS,
-	"joinLines":              blackfriday.EXTENSION_JOIN_LINES,
-}
-
 var stripHTMLReplacer = strings.NewReplacer("\n", " ", "</p>", "\n", "<br>", "\n", "<br />", "\n")
 
-var mmarkExtensionMap = map[string]int{
-	"tables":                 mmark.EXTENSION_TABLES,
-	"fencedCode":             mmark.EXTENSION_FENCED_CODE,
-	"autolink":               mmark.EXTENSION_AUTOLINK,
-	"laxHtmlBlocks":          mmark.EXTENSION_LAX_HTML_BLOCKS,
-	"spaceHeaders":           mmark.EXTENSION_SPACE_HEADERS,
-	"hardLineBreak":          mmark.EXTENSION_HARD_LINE_BREAK,
-	"footnotes":              mmark.EXTENSION_FOOTNOTES,
-	"noEmptyLineBeforeBlock": mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK,
-	"headerIds":              mmark.EXTENSION_HEADER_IDS,
-	"autoHeaderIds":          mmark.EXTENSION_AUTO_HEADER_IDS,
-}
-
 // StripHTML accepts a string, strips out all HTML tags and returns it.
 func StripHTML(s string) string {
 
@@ -250,181 +173,6 @@
 	return template.HTML(string(b))
 }
 
-// getHTMLRenderer creates a new Blackfriday HTML Renderer with the given configuration.
-func (c *ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer {
-	renderParameters := blackfriday.HtmlRendererParameters{
-		FootnoteAnchorPrefix:       c.footnoteAnchorPrefix,
-		FootnoteReturnLinkContents: c.footnoteReturnLinkContents,
-	}
-
-	b := len(ctx.DocumentID) != 0
-
-	if ctx.Config == nil {
-		panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID))
-	}
-
-	if b && !ctx.Config.PlainIDAnchors {
-		renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix
-		renderParameters.HeaderIDSuffix = ":" + ctx.DocumentID
-	}
-
-	htmlFlags := defaultFlags
-	htmlFlags |= blackfriday.HTML_USE_XHTML
-	htmlFlags |= blackfriday.HTML_FOOTNOTE_RETURN_LINKS
-
-	if ctx.Config.Smartypants {
-		htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS
-	}
-
-	if ctx.Config.SmartypantsQuotesNBSP {
-		htmlFlags |= blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP
-	}
-
-	if ctx.Config.AngledQuotes {
-		htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES
-	}
-
-	if ctx.Config.Fractions {
-		htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS
-	}
-
-	if ctx.Config.HrefTargetBlank {
-		htmlFlags |= blackfriday.HTML_HREF_TARGET_BLANK
-	}
-
-	if ctx.Config.NofollowLinks {
-		htmlFlags |= blackfriday.HTML_NOFOLLOW_LINKS
-	}
-
-	if ctx.Config.NoreferrerLinks {
-		htmlFlags |= blackfriday.HTML_NOREFERRER_LINKS
-	}
-
-	if ctx.Config.SmartDashes {
-		htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES
-	}
-
-	if ctx.Config.LatexDashes {
-		htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES
-	}
-
-	if ctx.Config.SkipHTML {
-		htmlFlags |= blackfriday.HTML_SKIP_HTML
-	}
-
-	return &HugoHTMLRenderer{
-		cs:               c,
-		RenderingContext: ctx,
-		Renderer:         blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
-	}
-}
-
-func getMarkdownExtensions(ctx *RenderingContext) int {
-	// Default Blackfriday common extensions
-	commonExtensions := 0 |
-		blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
-		blackfriday.EXTENSION_TABLES |
-		blackfriday.EXTENSION_FENCED_CODE |
-		blackfriday.EXTENSION_AUTOLINK |
-		blackfriday.EXTENSION_STRIKETHROUGH |
-		blackfriday.EXTENSION_SPACE_HEADERS |
-		blackfriday.EXTENSION_HEADER_IDS |
-		blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
-		blackfriday.EXTENSION_DEFINITION_LISTS
-
-	// Extra Blackfriday extensions that Hugo enables by default
-	flags := commonExtensions |
-		blackfriday.EXTENSION_AUTO_HEADER_IDS |
-		blackfriday.EXTENSION_FOOTNOTES
-
-	if ctx.Config == nil {
-		panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID))
-	}
-
-	for _, extension := range ctx.Config.Extensions {
-		if flag, ok := blackfridayExtensionMap[extension]; ok {
-			flags |= flag
-		}
-	}
-	for _, extension := range ctx.Config.ExtensionsMask {
-		if flag, ok := blackfridayExtensionMap[extension]; ok {
-			flags &= ^flag
-		}
-	}
-	return flags
-}
-
-func (c *ContentSpec) markdownRender(ctx *RenderingContext) []byte {
-	if ctx.RenderTOC {
-		return blackfriday.Markdown(ctx.Content,
-			c.getHTMLRenderer(blackfriday.HTML_TOC, ctx),
-			getMarkdownExtensions(ctx))
-	}
-	return blackfriday.Markdown(ctx.Content, c.getHTMLRenderer(0, ctx),
-		getMarkdownExtensions(ctx))
-}
-
-// getMmarkHTMLRenderer creates a new mmark HTML Renderer with the given configuration.
-func (c *ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContext) mmark.Renderer {
-	renderParameters := mmark.HtmlRendererParameters{
-		FootnoteAnchorPrefix:       c.footnoteAnchorPrefix,
-		FootnoteReturnLinkContents: c.footnoteReturnLinkContents,
-	}
-
-	b := len(ctx.DocumentID) != 0
-
-	if ctx.Config == nil {
-		panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID))
-	}
-
-	if b && !ctx.Config.PlainIDAnchors {
-		renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix
-		// renderParameters.HeaderIDSuffix = ":" + ctx.DocumentId
-	}
-
-	htmlFlags := defaultFlags
-	htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS
-
-	return &HugoMmarkHTMLRenderer{
-		cs:       c,
-		Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
-		Cfg:      c.Cfg,
-	}
-}
-
-func getMmarkExtensions(ctx *RenderingContext) int {
-	flags := 0
-	flags |= mmark.EXTENSION_TABLES
-	flags |= mmark.EXTENSION_FENCED_CODE
-	flags |= mmark.EXTENSION_AUTOLINK
-	flags |= mmark.EXTENSION_SPACE_HEADERS
-	flags |= mmark.EXTENSION_CITATION
-	flags |= mmark.EXTENSION_TITLEBLOCK_TOML
-	flags |= mmark.EXTENSION_HEADER_IDS
-	flags |= mmark.EXTENSION_AUTO_HEADER_IDS
-	flags |= mmark.EXTENSION_UNIQUE_HEADER_IDS
-	flags |= mmark.EXTENSION_FOOTNOTES
-	flags |= mmark.EXTENSION_SHORT_REF
-	flags |= mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
-	flags |= mmark.EXTENSION_INCLUDE
-
-	if ctx.Config == nil {
-		panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID))
-	}
-
-	for _, extension := range ctx.Config.Extensions {
-		if flag, ok := mmarkExtensionMap[extension]; ok {
-			flags |= flag
-		}
-	}
-	return flags
-}
-
-func (c *ContentSpec) mmarkRender(ctx *RenderingContext) []byte {
-	return mmark.Parse(ctx.Content, c.getMmarkHTMLRenderer(0, ctx),
-		getMmarkExtensions(ctx)).Bytes()
-}
-
 // ExtractTOC extracts Table of Contents from content.
 func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
 	if !bytes.Contains(content, []byte("<nav>")) {
@@ -464,38 +212,12 @@
 	return
 }
 
-// RenderingContext holds contextual information, like content and configuration,
-// for a given content rendering.
-// By creating you must set the Config, otherwise it will panic.
-type RenderingContext struct {
-	BaseFs       *filesystems.BaseFs
-	Content      []byte
-	PageFmt      string
-	DocumentID   string
-	DocumentName string
-	Config       *BlackFriday
-	RenderTOC    bool
-	Cfg          config.Provider
-}
-
-// RenderBytes renders a []byte.
-func (c *ContentSpec) RenderBytes(ctx *RenderingContext) []byte {
-	switch ctx.PageFmt {
-	default:
-		return c.markdownRender(ctx)
-	case "markdown":
-		return c.markdownRender(ctx)
-	case "asciidoc":
-		return getAsciidocContent(ctx)
-	case "mmark":
-		return c.mmarkRender(ctx)
-	case "rst":
-		return getRstContent(ctx)
-	case "org":
-		return orgRender(ctx, c)
-	case "pandoc":
-		return getPandocContent(ctx)
+func (c *ContentSpec) RenderMarkdown(src []byte) ([]byte, error) {
+	b, err := c.MardownConverter.Convert(converter.RenderContext{Src: src})
+	if err != nil {
+		return nil, err
 	}
+	return b.Bytes(), nil
 }
 
 // TotalWords counts instance of one or more consecutive white space
@@ -621,182 +343,4 @@
 	}
 
 	return strings.Join(words[:c.summaryLength], " "), true
-}
-
-func getAsciidocExecPath() string {
-	path, err := exec.LookPath("asciidoc")
-	if err != nil {
-		return ""
-	}
-	return path
-}
-
-func getAsciidoctorExecPath() string {
-	path, err := exec.LookPath("asciidoctor")
-	if err != nil {
-		return ""
-	}
-	return path
-}
-
-// HasAsciidoc returns whether Asciidoc or Asciidoctor is installed on this computer.
-func HasAsciidoc() bool {
-	return (getAsciidoctorExecPath() != "" ||
-		getAsciidocExecPath() != "")
-}
-
-// getAsciidocContent calls asciidoctor or asciidoc as an external helper
-// to convert AsciiDoc content to HTML.
-func getAsciidocContent(ctx *RenderingContext) []byte {
-	var isAsciidoctor bool
-	path := getAsciidoctorExecPath()
-	if path == "" {
-		path = getAsciidocExecPath()
-		if path == "" {
-			jww.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n",
-				"                 Leaving AsciiDoc content unrendered.")
-			return ctx.Content
-		}
-	} else {
-		isAsciidoctor = true
-	}
-
-	jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
-	args := []string{"--no-header-footer", "--safe"}
-	if isAsciidoctor {
-		// asciidoctor-specific arg to show stack traces on errors
-		args = append(args, "--trace")
-	}
-	args = append(args, "-")
-	return externallyRenderContent(ctx, path, args)
-}
-
-// HasRst returns whether rst2html is installed on this computer.
-func HasRst() bool {
-	return getRstExecPath() != ""
-}
-
-func getRstExecPath() string {
-	path, err := exec.LookPath("rst2html")
-	if err != nil {
-		path, err = exec.LookPath("rst2html.py")
-		if err != nil {
-			return ""
-		}
-	}
-	return path
-}
-
-func getPythonExecPath() string {
-	path, err := exec.LookPath("python")
-	if err != nil {
-		path, err = exec.LookPath("python.exe")
-		if err != nil {
-			return ""
-		}
-	}
-	return path
-}
-
-// getRstContent calls the Python script rst2html as an external helper
-// to convert reStructuredText content to HTML.
-func getRstContent(ctx *RenderingContext) []byte {
-	path := getRstExecPath()
-
-	if path == "" {
-		jww.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
-			"                 Leaving reStructuredText content unrendered.")
-		return ctx.Content
-
-	}
-	jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
-	var result []byte
-	// certain *nix based OSs wrap executables in scripted launchers
-	// invoking binaries on these OSs via python interpreter causes SyntaxError
-	// invoke directly so that shebangs work as expected
-	// handle Windows manually because it doesn't do shebangs
-	if runtime.GOOS == "windows" {
-		python := getPythonExecPath()
-		args := []string{path, "--leave-comments", "--initial-header-level=2"}
-		result = externallyRenderContent(ctx, python, args)
-	} else {
-		args := []string{"--leave-comments", "--initial-header-level=2"}
-		result = externallyRenderContent(ctx, path, args)
-	}
-	// TODO(bep) check if rst2html has a body only option.
-	bodyStart := bytes.Index(result, []byte("<body>\n"))
-	if bodyStart < 0 {
-		bodyStart = -7 //compensate for length
-	}
-
-	bodyEnd := bytes.Index(result, []byte("\n</body>"))
-	if bodyEnd < 0 || bodyEnd >= len(result) {
-		bodyEnd = len(result) - 1
-		if bodyEnd < 0 {
-			bodyEnd = 0
-		}
-	}
-
-	return result[bodyStart+7 : bodyEnd]
-}
-
-// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
-func getPandocContent(ctx *RenderingContext) []byte {
-	path, err := exec.LookPath("pandoc")
-	if err != nil {
-		jww.ERROR.Println("pandoc not found in $PATH: Please install.\n",
-			"                 Leaving pandoc content unrendered.")
-		return ctx.Content
-	}
-	args := []string{"--mathjax"}
-	return externallyRenderContent(ctx, path, args)
-}
-
-func orgRender(ctx *RenderingContext, c *ContentSpec) []byte {
-	config := org.New()
-	config.Log = jww.WARN
-	config.ReadFile = func(filename string) ([]byte, error) {
-		return afero.ReadFile(ctx.BaseFs.Content.Fs, filename)
-	}
-	writer := org.NewHTMLWriter()
-	writer.HighlightCodeBlock = func(source, lang string) string {
-		highlightedSource, err := c.Highlight(source, lang, "")
-		if err != nil {
-			jww.ERROR.Printf("Could not highlight source as lang %s. Using raw source.", lang)
-			return source
-		}
-		return highlightedSource
-	}
-
-	html, err := config.Parse(bytes.NewReader(ctx.Content), ctx.DocumentName).Write(writer)
-	if err != nil {
-		jww.ERROR.Printf("Could not render org: %s. Using unrendered content.", err)
-		return ctx.Content
-	}
-	return []byte(html)
-}
-
-func externallyRenderContent(ctx *RenderingContext, path string, args []string) []byte {
-	content := ctx.Content
-	cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
-
-	cmd := exec.Command(path, args...)
-	cmd.Stdin = bytes.NewReader(cleanContent)
-	var out, cmderr bytes.Buffer
-	cmd.Stdout = &out
-	cmd.Stderr = &cmderr
-	err := cmd.Run()
-	// Most external helpers exit w/ non-zero exit code only if severe, i.e.
-	// halting errors occurred. -> log stderr output regardless of state of err
-	for _, item := range strings.Split(cmderr.String(), "\n") {
-		item := strings.TrimSpace(item)
-		if item != "" {
-			jww.ERROR.Printf("%s: %s", ctx.DocumentName, item)
-		}
-	}
-	if err != nil {
-		jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
-	}
-
-	return normalizeExternalHelperLineFeeds(out.Bytes())
 }
--- a/helpers/content_renderer.go
+++ /dev/null
@@ -1,108 +1,0 @@
-// Copyright 2016 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package helpers
-
-import (
-	"bytes"
-	"strings"
-
-	"github.com/gohugoio/hugo/config"
-	"github.com/miekg/mmark"
-	"github.com/russross/blackfriday"
-)
-
-// HugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
-// Enabling Hugo to customise the rendering experience
-type HugoHTMLRenderer struct {
-	cs *ContentSpec
-	*RenderingContext
-	blackfriday.Renderer
-}
-
-// BlockCode renders a given text as a block of code.
-// Pygments is used if it is setup to handle code fences.
-func (r *HugoHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
-	if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
-		opts := r.Cfg.GetString("pygmentsOptions")
-		str := strings.Trim(string(text), "\n\r")
-		highlighted, _ := r.cs.Highlight(str, lang, opts)
-		out.WriteString(highlighted)
-	} else {
-		r.Renderer.BlockCode(out, text, lang)
-	}
-}
-
-// ListItem adds task list support to the Blackfriday renderer.
-func (r *HugoHTMLRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
-	if !r.Config.TaskLists {
-		r.Renderer.ListItem(out, text, flags)
-		return
-	}
-
-	switch {
-	case bytes.HasPrefix(text, []byte("[ ] ")):
-		text = append([]byte(`<label><input type="checkbox" disabled class="task-list-item">`), text[3:]...)
-		text = append(text, []byte(`</label>`)...)
-
-	case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")):
-		text = append([]byte(`<label><input type="checkbox" checked disabled class="task-list-item">`), text[3:]...)
-		text = append(text, []byte(`</label>`)...)
-	}
-
-	r.Renderer.ListItem(out, text, flags)
-}
-
-// List adds task list support to the Blackfriday renderer.
-func (r *HugoHTMLRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
-	if !r.Config.TaskLists {
-		r.Renderer.List(out, text, flags)
-		return
-	}
-	marker := out.Len()
-	r.Renderer.List(out, text, flags)
-	if out.Len() > marker {
-		list := out.Bytes()[marker:]
-		if bytes.Contains(list, []byte("task-list-item")) {
-			// Find the index of the first >, it might be 3 or 4 depending on whether
-			// there is a new line at the start, but this is safer than just hardcoding it.
-			closingBracketIndex := bytes.Index(list, []byte(">"))
-			// Rewrite the buffer from the marker
-			out.Truncate(marker)
-			// Safely assuming closingBracketIndex won't be -1 since there is a list
-			// May be either dl, ul or ol
-			list := append(list[:closingBracketIndex], append([]byte(` class="task-list"`), list[closingBracketIndex:]...)...)
-			out.Write(list)
-		}
-	}
-}
-
-// HugoMmarkHTMLRenderer wraps a mmark.Renderer, typically a mmark.html,
-// enabling Hugo to customise the rendering experience.
-type HugoMmarkHTMLRenderer struct {
-	cs *ContentSpec
-	mmark.Renderer
-	Cfg config.Provider
-}
-
-// BlockCode renders a given text as a block of code.
-// Pygments is used if it is setup to handle code fences.
-func (r *HugoMmarkHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure bool, callouts bool) {
-	if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
-		str := strings.Trim(string(text), "\n\r")
-		highlighted, _ := r.cs.Highlight(str, lang, "")
-		out.WriteString(highlighted)
-	} else {
-		r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts)
-	}
-}
--- a/helpers/content_renderer_test.go
+++ /dev/null
@@ -1,141 +1,0 @@
-// Copyright 2019 The Hugo Authors. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package helpers
-
-import (
-	"bytes"
-	"regexp"
-	"testing"
-
-	qt "github.com/frankban/quicktest"
-	"github.com/spf13/viper"
-)
-
-// Renders a codeblock using Blackfriday
-func (c *ContentSpec) render(input string) string {
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	render := c.getHTMLRenderer(0, ctx)
-
-	buf := &bytes.Buffer{}
-	render.BlockCode(buf, []byte(input), "html")
-	return buf.String()
-}
-
-// Renders a codeblock using Mmark
-func (c *ContentSpec) renderWithMmark(input string) string {
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	render := c.getMmarkHTMLRenderer(0, ctx)
-
-	buf := &bytes.Buffer{}
-	render.BlockCode(buf, []byte(input), "html", []byte(""), false, false)
-	return buf.String()
-}
-
-func TestCodeFence(t *testing.T) {
-	c := qt.New(t)
-
-	type test struct {
-		enabled         bool
-		input, expected string
-	}
-
-	// Pygments 2.0 and 2.1 have slightly different outputs so only do partial matching
-	data := []test{
-		{true, "<html></html>", `(?s)^<div class="highlight">\n?<pre.*><code class="language-html" data-lang="html">.*?</code></pre>\n?</div>\n?$`},
-		{false, "<html></html>", `(?s)^<pre.*><code class="language-html">.*?</code></pre>\n$`},
-	}
-
-	for _, useClassic := range []bool{false, true} {
-		for i, d := range data {
-			v := viper.New()
-			v.Set("pygmentsStyle", "monokai")
-			v.Set("pygmentsUseClasses", true)
-			v.Set("pygmentsCodeFences", d.enabled)
-			v.Set("pygmentsUseClassic", useClassic)
-
-			cs, err := NewContentSpec(v)
-			c.Assert(err, qt.IsNil)
-
-			result := cs.render(d.input)
-
-			expectedRe, err := regexp.Compile(d.expected)
-
-			if err != nil {
-				t.Fatal("Invalid regexp", err)
-			}
-			matched := expectedRe.MatchString(result)
-
-			if !matched {
-				t.Errorf("Test %d failed. BlackFriday enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
-			}
-
-			result = cs.renderWithMmark(d.input)
-			matched = expectedRe.MatchString(result)
-			if !matched {
-				t.Errorf("Test %d failed. Mmark enabled:%t, Expected:\n%q got:\n%q", i, d.enabled, d.expected, result)
-			}
-		}
-	}
-}
-
-func TestBlackfridayTaskList(t *testing.T) {
-	c := newTestContentSpec()
-
-	for i, this := range []struct {
-		markdown        string
-		taskListEnabled bool
-		expect          string
-	}{
-		{`
-TODO:
-
-- [x] On1
-- [X] On2
-- [ ] Off
-
-END
-`, true, `<p>TODO:</p>
-
-<ul class="task-list">
-<li><label><input type="checkbox" checked disabled class="task-list-item"> On1</label></li>
-<li><label><input type="checkbox" checked disabled class="task-list-item"> On2</label></li>
-<li><label><input type="checkbox" disabled class="task-list-item"> Off</label></li>
-</ul>
-
-<p>END</p>
-`},
-		{`- [x] On1`, false, `<ul>
-<li>[x] On1</li>
-</ul>
-`},
-		{`* [ ] Off
-
-END`, true, `<ul class="task-list">
-<li><label><input type="checkbox" disabled class="task-list-item"> Off</label></li>
-</ul>
-
-<p>END</p>
-`},
-	} {
-		blackFridayConfig := c.BlackFriday
-		blackFridayConfig.TaskLists = this.taskListEnabled
-		ctx := &RenderingContext{Content: []byte(this.markdown), PageFmt: "markdown", Config: blackFridayConfig}
-
-		result := string(c.RenderBytes(ctx))
-
-		if result != this.expect {
-			t.Errorf("[%d] got \n%v but expected \n%v", i, result, this.expect)
-		}
-	}
-}
--- a/helpers/content_test.go
+++ b/helpers/content_test.go
@@ -19,11 +19,13 @@
 	"strings"
 	"testing"
 
+	"github.com/spf13/afero"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
 	"github.com/spf13/viper"
 
 	qt "github.com/frankban/quicktest"
-	"github.com/miekg/mmark"
-	"github.com/russross/blackfriday"
 )
 
 const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>"
@@ -108,7 +110,7 @@
 	cfg.Set("buildExpired", true)
 	cfg.Set("buildDrafts", true)
 
-	spec, err := NewContentSpec(cfg)
+	spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs())
 
 	c.Assert(err, qt.IsNil)
 	c.Assert(spec.summaryLength, qt.Equals, 32)
@@ -199,233 +201,6 @@
 		if d.truncated != truncated {
 			t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated)
 		}
-	}
-}
-
-func TestGetHTMLRendererFlags(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	renderer := c.getHTMLRenderer(blackfriday.HTML_USE_XHTML, ctx)
-	flags := renderer.GetFlags()
-	if flags&blackfriday.HTML_USE_XHTML != blackfriday.HTML_USE_XHTML {
-		t.Errorf("Test flag: %d was not found amongs set flags:%d; Result: %d", blackfriday.HTML_USE_XHTML, flags, flags&blackfriday.HTML_USE_XHTML)
-	}
-}
-
-func TestGetHTMLRendererAllFlags(t *testing.T) {
-	c := newTestContentSpec()
-
-	type data struct {
-		testFlag int
-	}
-
-	allFlags := []data{
-		{blackfriday.HTML_USE_XHTML},
-		{blackfriday.HTML_FOOTNOTE_RETURN_LINKS},
-		{blackfriday.HTML_USE_SMARTYPANTS},
-		{blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP},
-		{blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES},
-		{blackfriday.HTML_SMARTYPANTS_FRACTIONS},
-		{blackfriday.HTML_HREF_TARGET_BLANK},
-		{blackfriday.HTML_NOFOLLOW_LINKS},
-		{blackfriday.HTML_NOREFERRER_LINKS},
-		{blackfriday.HTML_SMARTYPANTS_DASHES},
-		{blackfriday.HTML_SMARTYPANTS_LATEX_DASHES},
-	}
-	defaultFlags := blackfriday.HTML_USE_XHTML
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Config.AngledQuotes = true
-	ctx.Config.Fractions = true
-	ctx.Config.HrefTargetBlank = true
-	ctx.Config.NofollowLinks = true
-	ctx.Config.NoreferrerLinks = true
-	ctx.Config.LatexDashes = true
-	ctx.Config.PlainIDAnchors = true
-	ctx.Config.SmartDashes = true
-	ctx.Config.Smartypants = true
-	ctx.Config.SmartypantsQuotesNBSP = true
-	renderer := c.getHTMLRenderer(defaultFlags, ctx)
-	actualFlags := renderer.GetFlags()
-	var expectedFlags int
-	//OR-ing flags together...
-	for _, d := range allFlags {
-		expectedFlags |= d.testFlag
-	}
-	if expectedFlags != actualFlags {
-		t.Errorf("Expected flags (%d) did not equal actual (%d) flags.", expectedFlags, actualFlags)
-	}
-}
-
-func TestGetHTMLRendererAnchors(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.DocumentID = "testid"
-	ctx.Config.PlainIDAnchors = false
-
-	actualRenderer := c.getHTMLRenderer(0, ctx)
-	headerBuffer := &bytes.Buffer{}
-	footnoteBuffer := &bytes.Buffer{}
-	expectedFootnoteHref := []byte("href=\"#fn:testid:href\"")
-	expectedHeaderID := []byte("<h1 id=\"id:testid\"></h1>\n")
-
-	actualRenderer.Header(headerBuffer, func() bool { return true }, 1, "id")
-	actualRenderer.FootnoteRef(footnoteBuffer, []byte("href"), 1)
-
-	if !bytes.Contains(footnoteBuffer.Bytes(), expectedFootnoteHref) {
-		t.Errorf("Footnote anchor prefix not applied. Actual:%s Expected:%s", footnoteBuffer.String(), expectedFootnoteHref)
-	}
-
-	if !bytes.Equal(headerBuffer.Bytes(), expectedHeaderID) {
-		t.Errorf("Header Id Postfix not applied. Actual:%s Expected:%s", headerBuffer.String(), expectedHeaderID)
-	}
-}
-
-func TestGetMmarkHTMLRenderer(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.DocumentID = "testid"
-	ctx.Config.PlainIDAnchors = false
-	actualRenderer := c.getMmarkHTMLRenderer(0, ctx)
-
-	headerBuffer := &bytes.Buffer{}
-	footnoteBuffer := &bytes.Buffer{}
-	expectedFootnoteHref := []byte("href=\"#fn:testid:href\"")
-	expectedHeaderID := []byte("<h1 id=\"id\"></h1>")
-
-	actualRenderer.FootnoteRef(footnoteBuffer, []byte("href"), 1)
-	actualRenderer.Header(headerBuffer, func() bool { return true }, 1, "id")
-
-	if !bytes.Contains(footnoteBuffer.Bytes(), expectedFootnoteHref) {
-		t.Errorf("Footnote anchor prefix not applied. Actual:%s Expected:%s", footnoteBuffer.String(), expectedFootnoteHref)
-	}
-
-	if bytes.Equal(headerBuffer.Bytes(), expectedHeaderID) {
-		t.Errorf("Header Id Postfix applied. Actual:%s Expected:%s", headerBuffer.String(), expectedHeaderID)
-	}
-}
-
-func TestGetMarkdownExtensionsMasksAreRemovedFromExtensions(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Config.Extensions = []string{"headerId"}
-	ctx.Config.ExtensionsMask = []string{"noIntraEmphasis"}
-
-	actualFlags := getMarkdownExtensions(ctx)
-	if actualFlags&blackfriday.EXTENSION_NO_INTRA_EMPHASIS == blackfriday.EXTENSION_NO_INTRA_EMPHASIS {
-		t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_NO_INTRA_EMPHASIS)
-	}
-}
-
-func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
-	type data struct {
-		testFlag int
-	}
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Config.Extensions = []string{""}
-	ctx.Config.ExtensionsMask = []string{""}
-	allExtensions := []data{
-		{blackfriday.EXTENSION_NO_INTRA_EMPHASIS},
-		{blackfriday.EXTENSION_TABLES},
-		{blackfriday.EXTENSION_FENCED_CODE},
-		{blackfriday.EXTENSION_AUTOLINK},
-		{blackfriday.EXTENSION_STRIKETHROUGH},
-		// {blackfriday.EXTENSION_LAX_HTML_BLOCKS},
-		{blackfriday.EXTENSION_SPACE_HEADERS},
-		// {blackfriday.EXTENSION_HARD_LINE_BREAK},
-		// {blackfriday.EXTENSION_TAB_SIZE_EIGHT},
-		{blackfriday.EXTENSION_FOOTNOTES},
-		// {blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK},
-		{blackfriday.EXTENSION_HEADER_IDS},
-		// {blackfriday.EXTENSION_TITLEBLOCK},
-		{blackfriday.EXTENSION_AUTO_HEADER_IDS},
-		{blackfriday.EXTENSION_BACKSLASH_LINE_BREAK},
-		{blackfriday.EXTENSION_DEFINITION_LISTS},
-	}
-
-	actualFlags := getMarkdownExtensions(ctx)
-	for _, e := range allExtensions {
-		if actualFlags&e.testFlag != e.testFlag {
-			t.Errorf("Flag %v was not found in the list of extensions.", e)
-		}
-	}
-}
-
-func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Config.Extensions = []string{"definitionLists"}
-	ctx.Config.ExtensionsMask = []string{""}
-
-	actualFlags := getMarkdownExtensions(ctx)
-	if actualFlags&blackfriday.EXTENSION_DEFINITION_LISTS != blackfriday.EXTENSION_DEFINITION_LISTS {
-		t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_DEFINITION_LISTS)
-	}
-}
-
-func TestGetMarkdownRenderer(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Content = []byte("testContent")
-	actualRenderedMarkdown := c.markdownRender(ctx)
-	expectedRenderedMarkdown := []byte("<p>testContent</p>\n")
-	if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) {
-		t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown)
-	}
-}
-
-func TestGetMarkdownRendererWithTOC(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{RenderTOC: true, Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Content = []byte("testContent")
-	actualRenderedMarkdown := c.markdownRender(ctx)
-	expectedRenderedMarkdown := []byte("<nav>\n</nav>\n\n<p>testContent</p>\n")
-	if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) {
-		t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown)
-	}
-}
-
-func TestGetMmarkExtensions(t *testing.T) {
-	//TODO: This is doing the same just with different marks...
-	type data struct {
-		testFlag int
-	}
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Config.Extensions = []string{"tables"}
-	ctx.Config.ExtensionsMask = []string{""}
-	allExtensions := []data{
-		{mmark.EXTENSION_TABLES},
-		{mmark.EXTENSION_FENCED_CODE},
-		{mmark.EXTENSION_AUTOLINK},
-		{mmark.EXTENSION_SPACE_HEADERS},
-		{mmark.EXTENSION_CITATION},
-		{mmark.EXTENSION_TITLEBLOCK_TOML},
-		{mmark.EXTENSION_HEADER_IDS},
-		{mmark.EXTENSION_AUTO_HEADER_IDS},
-		{mmark.EXTENSION_UNIQUE_HEADER_IDS},
-		{mmark.EXTENSION_FOOTNOTES},
-		{mmark.EXTENSION_SHORT_REF},
-		{mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK},
-		{mmark.EXTENSION_INCLUDE},
-	}
-
-	actualFlags := getMmarkExtensions(ctx)
-	for _, e := range allExtensions {
-		if actualFlags&e.testFlag != e.testFlag {
-			t.Errorf("Flag %v was not found in the list of extensions.", e)
-		}
-	}
-}
-
-func TestMmarkRender(t *testing.T) {
-	c := newTestContentSpec()
-	ctx := &RenderingContext{Cfg: c.Cfg, Config: c.BlackFriday}
-	ctx.Content = []byte("testContent")
-	actualRenderedMarkdown := c.mmarkRender(ctx)
-	expectedRenderedMarkdown := []byte("<p>testContent</p>\n")
-	if !bytes.Equal(actualRenderedMarkdown, expectedRenderedMarkdown) {
-		t.Errorf("Actual rendered Markdown (%s) did not match expected markdown (%s)", actualRenderedMarkdown, expectedRenderedMarkdown)
 	}
 }
 
--- a/helpers/pygments_test.go
+++ b/helpers/pygments_test.go
@@ -45,7 +45,7 @@
 		v := viper.New()
 		v.Set("pygmentsStyle", this.pygmentsStyle)
 		v.Set("pygmentsUseClasses", this.pygmentsUseClasses)
-		spec, err := NewContentSpec(v)
+		spec, err := NewContentSpec(v, nil, nil)
 		c.Assert(err, qt.IsNil)
 
 		result1, err := spec.createPygmentsOptionsString(this.in)
@@ -94,7 +94,7 @@
 			v.Set("pygmentsUseClasses", b)
 		}
 
-		spec, err := NewContentSpec(v)
+		spec, err := NewContentSpec(v, nil, nil)
 		c.Assert(err, qt.IsNil)
 
 		result, err := spec.createPygmentsOptionsString(this.in)
@@ -138,7 +138,7 @@
 
 	v := viper.New()
 	v.Set("pygmentsUseClasses", true)
-	spec, err := NewContentSpec(v)
+	spec, err := NewContentSpec(v, nil, nil)
 	c.Assert(err, qt.IsNil)
 
 	result, err := spec.Highlight(`echo "Hello"`, "bash", "")
@@ -206,7 +206,7 @@
 			v.Set("pygmentsUseClasses", b)
 		}
 
-		spec, err := NewContentSpec(v)
+		spec, err := NewContentSpec(v, nil, nil)
 		c.Assert(err, qt.IsNil)
 
 		opts, err := spec.parsePygmentsOpts(this.in)
@@ -288,7 +288,7 @@
 }
 `
 
-	spec, err := NewContentSpec(v)
+	spec, err := NewContentSpec(v, nil, nil)
 	c.Assert(err, qt.IsNil)
 
 	for i := 0; i < b.N; i++ {
--- a/helpers/testhelpers_test.go
+++ b/helpers/testhelpers_test.go
@@ -1,6 +1,8 @@
 package helpers
 
 import (
+	"github.com/gohugoio/hugo/common/loggers"
+	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 
 	"github.com/gohugoio/hugo/hugofs"
@@ -56,7 +58,7 @@
 
 func newTestContentSpec() *ContentSpec {
 	v := viper.New()
-	spec, err := NewContentSpec(v)
+	spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
 	}
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -564,11 +564,6 @@
 
 func loadDefaultSettingsFor(v *viper.Viper) error {
 
-	c, err := helpers.NewContentSpec(v)
-	if err != nil {
-		return err
-	}
-
 	v.RegisterAlias("indexes", "taxonomies")
 
 	/*
@@ -616,7 +611,6 @@
 	v.SetDefault("paginate", 10)
 	v.SetDefault("paginatePath", "page")
 	v.SetDefault("summaryLength", 70)
-	v.SetDefault("blackfriday", c.BlackFriday)
 	v.SetDefault("rssLimit", -1)
 	v.SetDefault("sectionPagesMenu", "")
 	v.SetDefault("disablePathToLower", false)
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -23,6 +23,8 @@
 	"sort"
 	"strings"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
 	"github.com/gohugoio/hugo/common/maps"
 
 	"github.com/gohugoio/hugo/hugofs/files"
@@ -65,7 +67,7 @@
 type pageContext interface {
 	posOffset(offset int) text.Position
 	wrapError(err error) error
-	getRenderingConfig() *helpers.BlackFriday
+	getContentConverter() converter.Converter
 }
 
 // wrapErr adds some context to the given error if possible.
@@ -299,13 +301,6 @@
 	return p.translations
 }
 
-func (p *pageState) getRenderingConfig() *helpers.BlackFriday {
-	if p.m.renderingConfig == nil {
-		return p.s.ContentSpec.BlackFriday
-	}
-	return p.m.renderingConfig
-}
-
 func (ps *pageState) initCommonProviders(pp pagePaths) error {
 	if ps.IsPage() {
 		ps.posNextPrev = &nextPrev{init: ps.s.init.prevNext}
@@ -514,6 +509,10 @@
 		herrors.SimpleLineMatcher)
 
 	return err
+}
+
+func (p *pageState) getContentConverter() converter.Converter {
+	return p.m.contentConverter
 }
 
 func (p *pageState) addResources(r ...resource.Resource) {
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -21,6 +21,8 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
 	"github.com/gohugoio/hugo/hugofs/files"
 
 	"github.com/gohugoio/hugo/common/hugo"
@@ -29,7 +31,6 @@
 
 	"github.com/gohugoio/hugo/source"
 	"github.com/markbates/inflect"
-	"github.com/mitchellh/mapstructure"
 	"github.com/pkg/errors"
 
 	"github.com/gohugoio/hugo/common/maps"
@@ -123,7 +124,7 @@
 
 	s *Site
 
-	renderingConfig *helpers.BlackFriday
+	contentConverter converter.Converter
 }
 
 func (p *pageMeta) Aliases() []string {
@@ -598,7 +599,7 @@
 			p.markup = helpers.GuessType(p.File().Ext())
 		}
 		if p.markup == "" {
-			p.markup = "unknown"
+			p.markup = "markdown"
 		}
 	}
 
@@ -637,17 +638,28 @@
 		}
 	}
 
-	bfParam := getParamToLower(p, "blackfriday")
-	if bfParam != nil {
-		p.renderingConfig = p.s.ContentSpec.BlackFriday
+	if !p.f.IsZero() && p.markup != "html" {
+		var renderingConfigOverrides map[string]interface{}
+		bfParam := getParamToLower(p, "blackfriday")
+		if bfParam != nil {
+			renderingConfigOverrides = cast.ToStringMap(bfParam)
+		}
 
-		// Create a copy so we can modify it.
-		bf := *p.s.ContentSpec.BlackFriday
-		p.renderingConfig = &bf
-		pageParam := cast.ToStringMap(bfParam)
-		if err := mapstructure.Decode(pageParam, &p.renderingConfig); err != nil {
-			return errors.WithMessage(err, "failed to decode rendering config")
+		cp := p.s.ContentSpec.Converters.Get(p.markup)
+		if cp == nil {
+			return errors.Errorf("no content renderer found for markup %q", p.markup)
 		}
+
+		cpp, err := cp.New(converter.DocumentContext{
+			DocumentID:      p.f.UniqueID(),
+			DocumentName:    p.f.Path(),
+			ConfigOverrides: renderingConfigOverrides,
+		})
+
+		if err != nil {
+			return err
+		}
+		p.contentConverter = cpp
 	}
 
 	return nil
--- a/hugolib/page__output.go
+++ b/hugolib/page__output.go
@@ -45,8 +45,10 @@
 		paginatorProvider = pag
 	}
 
-	var contentProvider page.ContentProvider = page.NopPage
-	var tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
+	var (
+		contentProvider         page.ContentProvider         = page.NopPage
+		tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
+	)
 
 	if cp != nil {
 		contentProvider = cp
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -23,6 +23,8 @@
 	"sync"
 	"unicode/utf8"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
 	"github.com/gohugoio/hugo/lazy"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
@@ -97,7 +99,12 @@
 
 			if p.renderable {
 				if !isHTML {
-					cp.workContent = cp.renderContent(p, cp.workContent)
+					r, err := cp.renderContent(cp.workContent)
+					if err != nil {
+						return err
+					}
+					cp.workContent = r.Bytes()
+
 					tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
 					cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
 					cp.workContent = tmpContent
@@ -140,13 +147,16 @@
 						}
 					}
 				} else if cp.p.m.summary != "" {
-					html := cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
-						Content: []byte(cp.p.m.summary), RenderTOC: false, PageFmt: cp.p.m.markup,
-						Cfg:        p.Language(),
-						BaseFs:     p.s.BaseFs,
-						DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
-						Config: cp.p.getRenderingConfig()})
-					html = cp.p.s.ContentSpec.TrimShortHTML(html)
+					b, err := cp.p.getContentConverter().Convert(
+						converter.RenderContext{
+							Src: []byte(cp.p.m.summary),
+						},
+					)
+
+					if err != nil {
+						return err
+					}
+					html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
 					cp.summary = helpers.BytesToHTML(html)
 				}
 			}
@@ -311,13 +321,12 @@
 
 }
 
-func (cp *pageContentOutput) renderContent(p page.Page, content []byte) []byte {
-	return cp.p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
-		Content: content, RenderTOC: true, PageFmt: cp.p.m.markup,
-		Cfg:        p.Language(),
-		BaseFs:     cp.p.s.BaseFs,
-		DocumentID: p.File().UniqueID(), DocumentName: p.File().Path(),
-		Config: cp.p.getRenderingConfig()})
+func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) {
+	return cp.p.getContentConverter().Convert(
+		converter.RenderContext{
+			Src:       content,
+			RenderTOC: true,
+		})
 }
 
 func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -18,6 +18,10 @@
 	"html/template"
 	"os"
 
+	"github.com/gohugoio/hugo/markup/rst"
+
+	"github.com/gohugoio/hugo/markup/asciidoc"
+
 	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/common/loggers"
@@ -378,8 +382,8 @@
 	}{
 		{"md", func() bool { return true }},
 		{"mmark", func() bool { return true }},
-		{"ad", func() bool { return helpers.HasAsciidoc() }},
-		{"rst", func() bool { return helpers.HasRst() }},
+		{"ad", func() bool { return asciidoc.Supports() }},
+		{"rst", func() bool { return rst.Supports() }},
 	}
 
 	for _, e := range engines {
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -21,6 +21,8 @@
 	"html/template"
 	"path"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
 	"github.com/gohugoio/hugo/common/herrors"
 	"github.com/pkg/errors"
 
@@ -43,7 +45,6 @@
 	"github.com/gohugoio/hugo/output"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
-	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/tpl"
 )
 
@@ -347,13 +348,19 @@
 		// Pre Hugo 0.55 this was the behaviour even for the outer-most
 		// shortcode.
 		if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) {
-			newInner := s.ContentSpec.RenderBytes(&helpers.RenderingContext{
-				Content:      []byte(inner),
-				PageFmt:      p.m.markup,
-				Cfg:          p.Language(),
-				DocumentID:   p.File().UniqueID(),
-				DocumentName: p.File().Path(),
-				Config:       p.getRenderingConfig()})
+			var err error
+
+			b, err := p.getContentConverter().Convert(
+				converter.RenderContext{
+					Src: []byte(inner),
+				},
+			)
+
+			if err != nil {
+				return "", false, err
+			}
+
+			newInner := b.Bytes()
 
 			// If the type is “” (unknown) or “markdown”, we assume the markdown
 			// generation has been performed. Given the input: `a line`, markdown
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -18,6 +18,9 @@
 	"path/filepath"
 	"reflect"
 
+	"github.com/gohugoio/hugo/markup/asciidoc"
+	"github.com/gohugoio/hugo/markup/rst"
+
 	"github.com/spf13/viper"
 
 	"github.com/gohugoio/hugo/parser/pageparser"
@@ -27,7 +30,6 @@
 	"testing"
 
 	"github.com/gohugoio/hugo/deps"
-	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/tpl"
 	"github.com/spf13/cast"
 
@@ -538,6 +540,19 @@
 			"<h1>Hugo!</h1>"},
 	}
 
+	temp := tests[:0]
+	for _, test := range tests {
+		if strings.HasSuffix(test.contentPath, ".ad") && !asciidoc.Supports() {
+			t.Log("Skip Asciidoc test case as no Asciidoc present.")
+			continue
+		} else if strings.HasSuffix(test.contentPath, ".rst") && !rst.Supports() {
+			t.Log("Skip Rst test case as no rst2html present.")
+			continue
+		}
+		temp = append(temp, test)
+	}
+	tests = temp
+
 	sources := make([][2]string, len(tests))
 
 	for i, test := range tests {
@@ -578,11 +593,6 @@
 		test := test
 		t.Run(fmt.Sprintf("test=%d;contentPath=%s", i, test.contentPath), func(t *testing.T) {
 			t.Parallel()
-			if strings.HasSuffix(test.contentPath, ".ad") && !helpers.HasAsciidoc() {
-				t.Skip("Skip Asciidoc test case as no Asciidoc present.")
-			} else if strings.HasSuffix(test.contentPath, ".rst") && !helpers.HasRst() {
-				t.Skip("Skip Rst test case as no rst2html present.")
-			}
 
 			th := newTestHelper(s.Cfg, s.Fs, t)
 
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -28,6 +28,8 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/markup/converter"
+
 	"github.com/gohugoio/hugo/hugofs/files"
 
 	"github.com/gohugoio/hugo/common/maps"
@@ -758,17 +760,23 @@
 	}
 
 	if refURL.Fragment != "" {
+		_ = target
 		link = link + "#" + refURL.Fragment
 
-		if pctx, ok := target.(pageContext); ok && !target.File().IsZero() && !pctx.getRenderingConfig().PlainIDAnchors {
+		if pctx, ok := target.(pageContext); ok {
 			if refURL.Path != "" {
-				link = link + ":" + target.File().UniqueID()
+				if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
+					link = link + di.AnchorSuffix()
+				}
 			}
-		} else if pctx, ok := p.(pageContext); ok && !p.File().IsZero() && !pctx.getRenderingConfig().PlainIDAnchors {
-			link = link + ":" + p.File().UniqueID()
+		} else if pctx, ok := p.(pageContext); ok {
+			if di, ok := pctx.getContentConverter().(converter.DocumentInfo); ok {
+				link = link + di.AnchorSuffix()
+			}
 		}
 
 	}
+
 	return link, nil
 }
 
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -1018,6 +1018,7 @@
 }
 
 func checkLinkCase(site *Site, link string, currentPage page.Page, relative bool, outputFormat string, expected string, t *testing.T, i int) {
+	t.Helper()
 	if out, err := site.refLink(link, currentPage, relative, outputFormat); err != nil || out != expected {
 		t.Fatalf("[%d] Expected %q from %q to resolve to %q, got %q - error: %s", i, link, currentPage.Path(), expected, out, err)
 	}
--- /dev/null
+++ b/markup/asciidoc/convert.go
@@ -1,0 +1,97 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package asciidoc converts Asciidoc to HTML using Asciidoc or Asciidoctor
+// external binaries.
+package asciidoc
+
+import (
+	"os/exec"
+
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provider{}
+
+type provider struct {
+}
+
+func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		return &asciidocConverter{
+			ctx: ctx,
+			cfg: cfg,
+		}, nil
+	}
+	return n, nil
+}
+
+type asciidocConverter struct {
+	ctx converter.DocumentContext
+	cfg converter.ProviderConfig
+}
+
+func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil
+}
+
+// getAsciidocContent calls asciidoctor or asciidoc as an external helper
+// to convert AsciiDoc content to HTML.
+func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
+	var isAsciidoctor bool
+	path := getAsciidoctorExecPath()
+	if path == "" {
+		path = getAsciidocExecPath()
+		if path == "" {
+			a.cfg.Logger.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n",
+				"                 Leaving AsciiDoc content unrendered.")
+			return src
+		}
+	} else {
+		isAsciidoctor = true
+	}
+
+	a.cfg.Logger.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
+	args := []string{"--no-header-footer", "--safe"}
+	if isAsciidoctor {
+		// asciidoctor-specific arg to show stack traces on errors
+		args = append(args, "--trace")
+	}
+	args = append(args, "-")
+	return internal.ExternallyRenderContent(a.cfg, ctx, src, path, args)
+}
+
+func getAsciidocExecPath() string {
+	path, err := exec.LookPath("asciidoc")
+	if err != nil {
+		return ""
+	}
+	return path
+}
+
+func getAsciidoctorExecPath() string {
+	path, err := exec.LookPath("asciidoctor")
+	if err != nil {
+		return ""
+	}
+	return path
+}
+
+// Supports returns whether Asciidoc or Asciidoctor is installed on this computer.
+func Supports() bool {
+	return (getAsciidoctorExecPath() != "" ||
+		getAsciidocExecPath() != "")
+}
--- /dev/null
+++ b/markup/asciidoc/convert_test.go
@@ -1,0 +1,38 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package asciidoc
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConvert(t *testing.T) {
+	if !Supports() {
+		t.Skip("asciidoc/asciidoctor not installed")
+	}
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<div class=\"paragraph\">\n<p>testContent</p>\n</div>\n")
+}
--- /dev/null
+++ b/markup/blackfriday/convert.go
@@ -1,0 +1,224 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package blackfriday converts Markdown to HTML using Blackfriday v1.
+package blackfriday
+
+import (
+	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/gohugoio/hugo/markup/internal"
+	"github.com/russross/blackfriday"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provider{}
+
+type provider struct {
+}
+
+func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	defaultBlackFriday, err := internal.NewBlackfriday(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	defaultExtensions := getMarkdownExtensions(defaultBlackFriday)
+
+	pygmentsCodeFences := cfg.Cfg.GetBool("pygmentsCodeFences")
+	pygmentsCodeFencesGuessSyntax := cfg.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")
+	pygmentsOptions := cfg.Cfg.GetString("pygmentsOptions")
+
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		b := defaultBlackFriday
+		extensions := defaultExtensions
+
+		if ctx.ConfigOverrides != nil {
+			var err error
+			b, err = internal.UpdateBlackFriday(b, ctx.ConfigOverrides)
+			if err != nil {
+				return nil, err
+			}
+			extensions = getMarkdownExtensions(b)
+		}
+
+		return &blackfridayConverter{
+			ctx:        ctx,
+			bf:         b,
+			extensions: extensions,
+			cfg:        cfg,
+
+			pygmentsCodeFences:            pygmentsCodeFences,
+			pygmentsCodeFencesGuessSyntax: pygmentsCodeFencesGuessSyntax,
+			pygmentsOptions:               pygmentsOptions,
+		}, nil
+	}
+
+	return n, nil
+
+}
+
+type blackfridayConverter struct {
+	ctx        converter.DocumentContext
+	bf         *internal.BlackFriday
+	extensions int
+
+	pygmentsCodeFences            bool
+	pygmentsCodeFencesGuessSyntax bool
+	pygmentsOptions               string
+
+	cfg converter.ProviderConfig
+}
+
+func (c *blackfridayConverter) AnchorSuffix() string {
+	if c.bf.PlainIDAnchors {
+		return ""
+	}
+	return ":" + c.ctx.DocumentID
+}
+
+func (c *blackfridayConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	r := c.getHTMLRenderer(ctx.RenderTOC)
+
+	return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil
+
+}
+
+func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
+	flags := getFlags(renderTOC, c.bf)
+
+	documentID := c.ctx.DocumentID
+
+	renderParameters := blackfriday.HtmlRendererParameters{
+		FootnoteAnchorPrefix:       c.bf.FootnoteAnchorPrefix,
+		FootnoteReturnLinkContents: c.bf.FootnoteReturnLinkContents,
+	}
+
+	if documentID != "" && !c.bf.PlainIDAnchors {
+		renderParameters.FootnoteAnchorPrefix = documentID + ":" + renderParameters.FootnoteAnchorPrefix
+		renderParameters.HeaderIDSuffix = ":" + documentID
+	}
+
+	return &hugoHTMLRenderer{
+		c:        c,
+		Renderer: blackfriday.HtmlRendererWithParameters(flags, "", "", renderParameters),
+	}
+}
+
+func getFlags(renderTOC bool, cfg *internal.BlackFriday) int {
+
+	var flags int
+
+	if renderTOC {
+		flags = blackfriday.HTML_TOC
+	}
+
+	flags |= blackfriday.HTML_USE_XHTML
+	flags |= blackfriday.HTML_FOOTNOTE_RETURN_LINKS
+
+	if cfg.Smartypants {
+		flags |= blackfriday.HTML_USE_SMARTYPANTS
+	}
+
+	if cfg.SmartypantsQuotesNBSP {
+		flags |= blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP
+	}
+
+	if cfg.AngledQuotes {
+		flags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES
+	}
+
+	if cfg.Fractions {
+		flags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS
+	}
+
+	if cfg.HrefTargetBlank {
+		flags |= blackfriday.HTML_HREF_TARGET_BLANK
+	}
+
+	if cfg.NofollowLinks {
+		flags |= blackfriday.HTML_NOFOLLOW_LINKS
+	}
+
+	if cfg.NoreferrerLinks {
+		flags |= blackfriday.HTML_NOREFERRER_LINKS
+	}
+
+	if cfg.SmartDashes {
+		flags |= blackfriday.HTML_SMARTYPANTS_DASHES
+	}
+
+	if cfg.LatexDashes {
+		flags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES
+	}
+
+	if cfg.SkipHTML {
+		flags |= blackfriday.HTML_SKIP_HTML
+	}
+
+	return flags
+}
+
+func getMarkdownExtensions(cfg *internal.BlackFriday) int {
+	// Default Blackfriday common extensions
+	commonExtensions := 0 |
+		blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
+		blackfriday.EXTENSION_TABLES |
+		blackfriday.EXTENSION_FENCED_CODE |
+		blackfriday.EXTENSION_AUTOLINK |
+		blackfriday.EXTENSION_STRIKETHROUGH |
+		blackfriday.EXTENSION_SPACE_HEADERS |
+		blackfriday.EXTENSION_HEADER_IDS |
+		blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
+		blackfriday.EXTENSION_DEFINITION_LISTS
+
+	// Extra Blackfriday extensions that Hugo enables by default
+	flags := commonExtensions |
+		blackfriday.EXTENSION_AUTO_HEADER_IDS |
+		blackfriday.EXTENSION_FOOTNOTES
+
+	for _, extension := range cfg.Extensions {
+		if flag, ok := blackfridayExtensionMap[extension]; ok {
+			flags |= flag
+		}
+	}
+	for _, extension := range cfg.ExtensionsMask {
+		if flag, ok := blackfridayExtensionMap[extension]; ok {
+			flags &= ^flag
+		}
+	}
+	return flags
+}
+
+var blackfridayExtensionMap = map[string]int{
+	"noIntraEmphasis":        blackfriday.EXTENSION_NO_INTRA_EMPHASIS,
+	"tables":                 blackfriday.EXTENSION_TABLES,
+	"fencedCode":             blackfriday.EXTENSION_FENCED_CODE,
+	"autolink":               blackfriday.EXTENSION_AUTOLINK,
+	"strikethrough":          blackfriday.EXTENSION_STRIKETHROUGH,
+	"laxHtmlBlocks":          blackfriday.EXTENSION_LAX_HTML_BLOCKS,
+	"spaceHeaders":           blackfriday.EXTENSION_SPACE_HEADERS,
+	"hardLineBreak":          blackfriday.EXTENSION_HARD_LINE_BREAK,
+	"tabSizeEight":           blackfriday.EXTENSION_TAB_SIZE_EIGHT,
+	"footnotes":              blackfriday.EXTENSION_FOOTNOTES,
+	"noEmptyLineBeforeBlock": blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK,
+	"headerIds":              blackfriday.EXTENSION_HEADER_IDS,
+	"titleblock":             blackfriday.EXTENSION_TITLEBLOCK,
+	"autoHeaderIds":          blackfriday.EXTENSION_AUTO_HEADER_IDS,
+	"backslashLineBreak":     blackfriday.EXTENSION_BACKSLASH_LINE_BREAK,
+	"definitionLists":        blackfriday.EXTENSION_DEFINITION_LISTS,
+	"joinLines":              blackfriday.EXTENSION_JOIN_LINES,
+}
+
+var (
+	_ converter.DocumentInfo = (*blackfridayConverter)(nil)
+)
--- /dev/null
+++ b/markup/blackfriday/convert_test.go
@@ -1,0 +1,194 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package blackfriday
+
+import (
+	"testing"
+
+	"github.com/spf13/viper"
+
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/russross/blackfriday"
+)
+
+func TestGetMarkdownExtensionsMasksAreRemovedFromExtensions(t *testing.T) {
+	c := qt.New(t)
+	b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
+	c.Assert(err, qt.IsNil)
+
+	b.Extensions = []string{"headerId"}
+	b.ExtensionsMask = []string{"noIntraEmphasis"}
+
+	actualFlags := getMarkdownExtensions(b)
+	if actualFlags&blackfriday.EXTENSION_NO_INTRA_EMPHASIS == blackfriday.EXTENSION_NO_INTRA_EMPHASIS {
+		t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_NO_INTRA_EMPHASIS)
+	}
+}
+
+func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
+	type data struct {
+		testFlag int
+	}
+
+	c := qt.New(t)
+	b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
+	c.Assert(err, qt.IsNil)
+
+	b.Extensions = []string{""}
+	b.ExtensionsMask = []string{""}
+	allExtensions := []data{
+		{blackfriday.EXTENSION_NO_INTRA_EMPHASIS},
+		{blackfriday.EXTENSION_TABLES},
+		{blackfriday.EXTENSION_FENCED_CODE},
+		{blackfriday.EXTENSION_AUTOLINK},
+		{blackfriday.EXTENSION_STRIKETHROUGH},
+		// {blackfriday.EXTENSION_LAX_HTML_BLOCKS},
+		{blackfriday.EXTENSION_SPACE_HEADERS},
+		// {blackfriday.EXTENSION_HARD_LINE_BREAK},
+		// {blackfriday.EXTENSION_TAB_SIZE_EIGHT},
+		{blackfriday.EXTENSION_FOOTNOTES},
+		// {blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK},
+		{blackfriday.EXTENSION_HEADER_IDS},
+		// {blackfriday.EXTENSION_TITLEBLOCK},
+		{blackfriday.EXTENSION_AUTO_HEADER_IDS},
+		{blackfriday.EXTENSION_BACKSLASH_LINE_BREAK},
+		{blackfriday.EXTENSION_DEFINITION_LISTS},
+	}
+
+	actualFlags := getMarkdownExtensions(b)
+	for _, e := range allExtensions {
+		if actualFlags&e.testFlag != e.testFlag {
+			t.Errorf("Flag %v was not found in the list of extensions.", e)
+		}
+	}
+}
+
+func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
+	c := qt.New(t)
+	b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
+	c.Assert(err, qt.IsNil)
+
+	b.Extensions = []string{"definitionLists"}
+	b.ExtensionsMask = []string{""}
+
+	actualFlags := getMarkdownExtensions(b)
+	if actualFlags&blackfriday.EXTENSION_DEFINITION_LISTS != blackfriday.EXTENSION_DEFINITION_LISTS {
+		t.Errorf("Masked out flag {%v} found amongst returned extensions.", blackfriday.EXTENSION_DEFINITION_LISTS)
+	}
+}
+
+func TestGetFlags(t *testing.T) {
+	c := qt.New(t)
+	cfg := converter.ProviderConfig{Cfg: viper.New()}
+	b, err := internal.NewBlackfriday(cfg)
+	c.Assert(err, qt.IsNil)
+	flags := getFlags(false, b)
+	if flags&blackfriday.HTML_USE_XHTML != blackfriday.HTML_USE_XHTML {
+		t.Errorf("Test flag: %d was not found amongs set flags:%d; Result: %d", blackfriday.HTML_USE_XHTML, flags, flags&blackfriday.HTML_USE_XHTML)
+	}
+}
+
+func TestGetAllFlags(t *testing.T) {
+	c := qt.New(t)
+	cfg := converter.ProviderConfig{Cfg: viper.New()}
+	b, err := internal.NewBlackfriday(cfg)
+	c.Assert(err, qt.IsNil)
+
+	type data struct {
+		testFlag int
+	}
+
+	allFlags := []data{
+		{blackfriday.HTML_USE_XHTML},
+		{blackfriday.HTML_FOOTNOTE_RETURN_LINKS},
+		{blackfriday.HTML_USE_SMARTYPANTS},
+		{blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP},
+		{blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES},
+		{blackfriday.HTML_SMARTYPANTS_FRACTIONS},
+		{blackfriday.HTML_HREF_TARGET_BLANK},
+		{blackfriday.HTML_NOFOLLOW_LINKS},
+		{blackfriday.HTML_NOREFERRER_LINKS},
+		{blackfriday.HTML_SMARTYPANTS_DASHES},
+		{blackfriday.HTML_SMARTYPANTS_LATEX_DASHES},
+	}
+
+	b.AngledQuotes = true
+	b.Fractions = true
+	b.HrefTargetBlank = true
+	b.NofollowLinks = true
+	b.NoreferrerLinks = true
+	b.LatexDashes = true
+	b.PlainIDAnchors = true
+	b.SmartDashes = true
+	b.Smartypants = true
+	b.SmartypantsQuotesNBSP = true
+
+	actualFlags := getFlags(false, b)
+
+	var expectedFlags int
+	//OR-ing flags together...
+	for _, d := range allFlags {
+		expectedFlags |= d.testFlag
+	}
+	if expectedFlags != actualFlags {
+		t.Errorf("Expected flags (%d) did not equal actual (%d) flags.", expectedFlags, actualFlags)
+	}
+}
+
+func TestConvert(t *testing.T) {
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{
+		Cfg: viper.New(),
+	})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<p>testContent</p>\n")
+}
+
+func TestGetHTMLRendererAnchors(t *testing.T) {
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{
+		Cfg: viper.New(),
+	})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{
+		DocumentID: "testid",
+		ConfigOverrides: map[string]interface{}{
+			"plainIDAnchors": false,
+			"footnotes":      true,
+		},
+	})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte(`# Header
+
+This is a footnote.[^1] And then some.
+
+
+[^1]: Footnote text.
+
+`)})
+
+	c.Assert(err, qt.IsNil)
+	s := string(b.Bytes())
+	c.Assert(s, qt.Contains, "<h1 id=\"header:testid\">Header</h1>")
+	c.Assert(s, qt.Contains, "This is a footnote.<sup class=\"footnote-ref\" id=\"fnref:testid:1\"><a href=\"#fn:testid:1\">1</a></sup>")
+	c.Assert(s, qt.Contains, "<a class=\"footnote-return\" href=\"#fnref:testid:1\"><sup>[return]</sup></a>")
+}
--- /dev/null
+++ b/markup/blackfriday/renderer.go
@@ -1,0 +1,85 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package blackfriday
+
+import (
+	"bytes"
+	"strings"
+
+	"github.com/russross/blackfriday"
+)
+
+// hugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
+// adding some custom behaviour.
+type hugoHTMLRenderer struct {
+	c *blackfridayConverter
+	blackfriday.Renderer
+}
+
+// BlockCode renders a given text as a block of code.
+// Pygments is used if it is setup to handle code fences.
+func (r *hugoHTMLRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
+	if r.c.pygmentsCodeFences && (lang != "" || r.c.pygmentsCodeFencesGuessSyntax) {
+		opts := r.c.pygmentsOptions
+		str := strings.Trim(string(text), "\n\r")
+		highlighted, _ := r.c.cfg.Highlight(str, lang, opts)
+		out.WriteString(highlighted)
+	} else {
+		r.Renderer.BlockCode(out, text, lang)
+	}
+}
+
+// ListItem adds task list support to the Blackfriday renderer.
+func (r *hugoHTMLRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
+	if !r.c.bf.TaskLists {
+		r.Renderer.ListItem(out, text, flags)
+		return
+	}
+
+	switch {
+	case bytes.HasPrefix(text, []byte("[ ] ")):
+		text = append([]byte(`<label><input type="checkbox" disabled class="task-list-item">`), text[3:]...)
+		text = append(text, []byte(`</label>`)...)
+
+	case bytes.HasPrefix(text, []byte("[x] ")) || bytes.HasPrefix(text, []byte("[X] ")):
+		text = append([]byte(`<label><input type="checkbox" checked disabled class="task-list-item">`), text[3:]...)
+		text = append(text, []byte(`</label>`)...)
+	}
+
+	r.Renderer.ListItem(out, text, flags)
+}
+
+// List adds task list support to the Blackfriday renderer.
+func (r *hugoHTMLRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
+	if !r.c.bf.TaskLists {
+		r.Renderer.List(out, text, flags)
+		return
+	}
+	marker := out.Len()
+	r.Renderer.List(out, text, flags)
+	if out.Len() > marker {
+		list := out.Bytes()[marker:]
+		if bytes.Contains(list, []byte("task-list-item")) {
+			// Find the index of the first >, it might be 3 or 4 depending on whether
+			// there is a new line at the start, but this is safer than just hardcoding it.
+			closingBracketIndex := bytes.Index(list, []byte(">"))
+			// Rewrite the buffer from the marker
+			out.Truncate(marker)
+			// Safely assuming closingBracketIndex won't be -1 since there is a list
+			// May be either dl, ul or ol
+			list := append(list[:closingBracketIndex], append([]byte(` class="task-list"`), list[closingBracketIndex:]...)...)
+			out.Write(list)
+		}
+	}
+}
--- /dev/null
+++ b/markup/converter/converter.go
@@ -1,0 +1,83 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package converter
+
+import (
+	"github.com/gohugoio/hugo/common/loggers"
+	"github.com/gohugoio/hugo/config"
+	"github.com/spf13/afero"
+)
+
+// ProviderConfig configures a new Provider.
+type ProviderConfig struct {
+	Cfg       config.Provider // Site config
+	ContentFs afero.Fs
+	Logger    *loggers.Logger
+	Highlight func(code, lang, optsStr string) (string, error)
+}
+
+// NewProvider creates converter providers.
+type NewProvider interface {
+	New(cfg ProviderConfig) (Provider, error)
+}
+
+// Provider creates converters.
+type Provider interface {
+	New(ctx DocumentContext) (Converter, error)
+}
+
+// NewConverter is an adapter that can be used as a ConverterProvider.
+type NewConverter func(ctx DocumentContext) (Converter, error)
+
+// New creates a new Converter for the given ctx.
+func (n NewConverter) New(ctx DocumentContext) (Converter, error) {
+	return n(ctx)
+}
+
+// Converter wraps the Convert method that converts some markup into
+// another format, e.g. Markdown to HTML.
+type Converter interface {
+	Convert(ctx RenderContext) (Result, error)
+}
+
+// Result represents the minimum returned from Convert.
+type Result interface {
+	Bytes() []byte
+}
+
+// DocumentInfo holds additional information provided by some converters.
+type DocumentInfo interface {
+	AnchorSuffix() string
+}
+
+// Bytes holds a byte slice and implements the Result interface.
+type Bytes []byte
+
+// Bytes returns itself
+func (b Bytes) Bytes() []byte {
+	return b
+}
+
+// DocumentContext holds contextual information about the document to convert.
+type DocumentContext struct {
+	DocumentID      string
+	DocumentName    string
+	ConfigOverrides map[string]interface{}
+}
+
+// RenderContext holds contextual information about the content to render.
+type RenderContext struct {
+	Src       []byte
+	RenderTOC bool
+}
--- /dev/null
+++ b/markup/internal/blackfriday.go
@@ -1,0 +1,108 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package helpers implements general utility functions that work with
+// and on content.  The helper functions defined here lay down the
+// foundation of how Hugo works with files and filepaths, and perform
+// string operations on content.
+
+package internal
+
+import (
+	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/mitchellh/mapstructure"
+	"github.com/pkg/errors"
+)
+
+// BlackFriday holds configuration values for BlackFriday rendering.
+// It is kept here because it's used in several packages.
+type BlackFriday struct {
+	Smartypants           bool
+	SmartypantsQuotesNBSP bool
+	AngledQuotes          bool
+	Fractions             bool
+	HrefTargetBlank       bool
+	NofollowLinks         bool
+	NoreferrerLinks       bool
+	SmartDashes           bool
+	LatexDashes           bool
+	TaskLists             bool
+	PlainIDAnchors        bool
+	Extensions            []string
+	ExtensionsMask        []string
+	SkipHTML              bool
+
+	FootnoteAnchorPrefix       string
+	FootnoteReturnLinkContents string
+}
+
+func UpdateBlackFriday(old *BlackFriday, m map[string]interface{}) (*BlackFriday, error) {
+	// Create a copy so we can modify it.
+	bf := *old
+	if err := mapstructure.Decode(m, &bf); err != nil {
+		return nil, errors.WithMessage(err, "failed to decode rendering config")
+	}
+	return &bf, nil
+}
+
+// NewBlackfriday creates a new Blackfriday filled with site config or some sane defaults.
+func NewBlackfriday(cfg converter.ProviderConfig) (*BlackFriday, error) {
+	var siteConfig map[string]interface{}
+	if cfg.Cfg != nil {
+		siteConfig = cfg.Cfg.GetStringMap("blackfriday")
+	}
+
+	defaultParam := map[string]interface{}{
+		"smartypants":           true,
+		"angledQuotes":          false,
+		"smartypantsQuotesNBSP": false,
+		"fractions":             true,
+		"hrefTargetBlank":       false,
+		"nofollowLinks":         false,
+		"noreferrerLinks":       false,
+		"smartDashes":           true,
+		"latexDashes":           true,
+		"plainIDAnchors":        true,
+		"taskLists":             true,
+		"skipHTML":              false,
+	}
+
+	maps.ToLower(defaultParam)
+
+	config := make(map[string]interface{})
+
+	for k, v := range defaultParam {
+		config[k] = v
+	}
+
+	for k, v := range siteConfig {
+		config[k] = v
+	}
+
+	combinedConfig := &BlackFriday{}
+	if err := mapstructure.Decode(config, combinedConfig); err != nil {
+		return nil, errors.Errorf("failed to decode Blackfriday config: %s", err)
+	}
+
+	// TODO(bep) update/consolidate docs
+	if combinedConfig.FootnoteAnchorPrefix == "" {
+		combinedConfig.FootnoteAnchorPrefix = cfg.Cfg.GetString("footnoteAnchorPrefix")
+	}
+
+	if combinedConfig.FootnoteReturnLinkContents == "" {
+		combinedConfig.FootnoteReturnLinkContents = cfg.Cfg.GetString("footnoteReturnLinkContents")
+	}
+
+	return combinedConfig, nil
+}
--- /dev/null
+++ b/markup/internal/external.go
@@ -1,0 +1,52 @@
+package internal
+
+import (
+	"bytes"
+	"os/exec"
+	"strings"
+
+	"github.com/gohugoio/hugo/markup/converter"
+)
+
+func ExternallyRenderContent(
+	cfg converter.ProviderConfig,
+	ctx converter.DocumentContext,
+	content []byte, path string, args []string) []byte {
+
+	logger := cfg.Logger
+	cmd := exec.Command(path, args...)
+	cmd.Stdin = bytes.NewReader(content)
+	var out, cmderr bytes.Buffer
+	cmd.Stdout = &out
+	cmd.Stderr = &cmderr
+	err := cmd.Run()
+	// Most external helpers exit w/ non-zero exit code only if severe, i.e.
+	// halting errors occurred. -> log stderr output regardless of state of err
+	for _, item := range strings.Split(cmderr.String(), "\n") {
+		item := strings.TrimSpace(item)
+		if item != "" {
+			logger.ERROR.Printf("%s: %s", ctx.DocumentName, item)
+		}
+	}
+	if err != nil {
+		logger.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err)
+	}
+
+	return normalizeExternalHelperLineFeeds(out.Bytes())
+}
+
+// Strips carriage returns from third-party / external processes (useful for Windows)
+func normalizeExternalHelperLineFeeds(content []byte) []byte {
+	return bytes.Replace(content, []byte("\r"), []byte(""), -1)
+}
+
+func GetPythonExecPath() string {
+	path, err := exec.LookPath("python")
+	if err != nil {
+		path, err = exec.LookPath("python.exe")
+		if err != nil {
+			return ""
+		}
+	}
+	return path
+}
--- /dev/null
+++ b/markup/markup.go
@@ -1,0 +1,83 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package markup
+
+import (
+	"strings"
+
+	"github.com/gohugoio/hugo/markup/org"
+
+	"github.com/gohugoio/hugo/markup/asciidoc"
+	"github.com/gohugoio/hugo/markup/blackfriday"
+	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/gohugoio/hugo/markup/mmark"
+	"github.com/gohugoio/hugo/markup/pandoc"
+	"github.com/gohugoio/hugo/markup/rst"
+)
+
+func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, error) {
+	converters := make(map[string]converter.Provider)
+
+	add := func(p converter.NewProvider, aliases ...string) error {
+		c, err := p.New(cfg)
+		if err != nil {
+			return err
+		}
+		addConverter(converters, c, aliases...)
+		return nil
+	}
+
+	if err := add(blackfriday.Provider, "md", "markdown", "blackfriday"); err != nil {
+		return nil, err
+	}
+	if err := add(mmark.Provider, "mmark"); err != nil {
+		return nil, err
+	}
+	if err := add(asciidoc.Provider, "asciidoc"); err != nil {
+		return nil, err
+	}
+	if err := add(rst.Provider, "rst"); err != nil {
+		return nil, err
+	}
+	if err := add(pandoc.Provider, "pandoc"); err != nil {
+		return nil, err
+	}
+	if err := add(org.Provider, "org"); err != nil {
+		return nil, err
+	}
+
+	return &converterRegistry{converters: converters}, nil
+}
+
+type ConverterProvider interface {
+	Get(name string) converter.Provider
+}
+
+type converterRegistry struct {
+	// Maps name (md, markdown, blackfriday etc.) to a converter provider.
+	// Note that this is also used for aliasing, so the same converter
+	// may be registered multiple times.
+	// All names are lower case.
+	converters map[string]converter.Provider
+}
+
+func (r *converterRegistry) Get(name string) converter.Provider {
+	return r.converters[strings.ToLower(name)]
+}
+
+func addConverter(m map[string]converter.Provider, c converter.Provider, aliases ...string) {
+	for _, alias := range aliases {
+		m[alias] = c
+	}
+}
--- /dev/null
+++ b/markup/markup_test.go
@@ -1,0 +1,41 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package markup
+
+import (
+	"testing"
+
+	"github.com/spf13/viper"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConverterRegistry(t *testing.T) {
+	c := qt.New(t)
+
+	r, err := NewConverterProvider(converter.ProviderConfig{Cfg: viper.New()})
+
+	c.Assert(err, qt.IsNil)
+
+	c.Assert(r.Get("foo"), qt.IsNil)
+	c.Assert(r.Get("markdown"), qt.Not(qt.IsNil))
+	c.Assert(r.Get("mmark"), qt.Not(qt.IsNil))
+	c.Assert(r.Get("asciidoc"), qt.Not(qt.IsNil))
+	c.Assert(r.Get("rst"), qt.Not(qt.IsNil))
+	c.Assert(r.Get("pandoc"), qt.Not(qt.IsNil))
+	c.Assert(r.Get("org"), qt.Not(qt.IsNil))
+
+}
--- /dev/null
+++ b/markup/mmark/convert.go
@@ -1,0 +1,143 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package mmark converts Markdown to HTML using MMark v1.
+package mmark
+
+import (
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/miekg/mmark"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provider{}
+
+type provider struct {
+}
+
+func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	defaultBlackFriday, err := internal.NewBlackfriday(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	defaultExtensions := getMmarkExtensions(defaultBlackFriday)
+
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		b := defaultBlackFriday
+		extensions := defaultExtensions
+
+		if ctx.ConfigOverrides != nil {
+			var err error
+			b, err = internal.UpdateBlackFriday(b, ctx.ConfigOverrides)
+			if err != nil {
+				return nil, err
+			}
+			extensions = getMmarkExtensions(b)
+		}
+
+		return &mmarkConverter{
+			ctx:        ctx,
+			b:          b,
+			extensions: extensions,
+			cfg:        cfg,
+		}, nil
+	}
+
+	return n, nil
+
+}
+
+type mmarkConverter struct {
+	ctx        converter.DocumentContext
+	extensions int
+	b          *internal.BlackFriday
+	cfg        converter.ProviderConfig
+}
+
+func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	r := getHTMLRenderer(c.ctx, c.b, c.cfg)
+	return mmark.Parse(ctx.Src, r, c.extensions), nil
+}
+
+func getHTMLRenderer(
+	ctx converter.DocumentContext,
+	cfg *internal.BlackFriday,
+	pcfg converter.ProviderConfig) mmark.Renderer {
+
+	var (
+		flags      int
+		documentID string
+	)
+
+	documentID = ctx.DocumentID
+
+	renderParameters := mmark.HtmlRendererParameters{
+		FootnoteAnchorPrefix:       cfg.FootnoteAnchorPrefix,
+		FootnoteReturnLinkContents: cfg.FootnoteReturnLinkContents,
+	}
+
+	if documentID != "" && !cfg.PlainIDAnchors {
+		renderParameters.FootnoteAnchorPrefix = documentID + ":" + renderParameters.FootnoteAnchorPrefix
+	}
+
+	htmlFlags := flags
+	htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS
+
+	return &mmarkRenderer{
+		Config:    cfg,
+		Cfg:       pcfg.Cfg,
+		highlight: pcfg.Highlight,
+		Renderer:  mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
+	}
+
+}
+
+func getMmarkExtensions(cfg *internal.BlackFriday) int {
+	flags := 0
+	flags |= mmark.EXTENSION_TABLES
+	flags |= mmark.EXTENSION_FENCED_CODE
+	flags |= mmark.EXTENSION_AUTOLINK
+	flags |= mmark.EXTENSION_SPACE_HEADERS
+	flags |= mmark.EXTENSION_CITATION
+	flags |= mmark.EXTENSION_TITLEBLOCK_TOML
+	flags |= mmark.EXTENSION_HEADER_IDS
+	flags |= mmark.EXTENSION_AUTO_HEADER_IDS
+	flags |= mmark.EXTENSION_UNIQUE_HEADER_IDS
+	flags |= mmark.EXTENSION_FOOTNOTES
+	flags |= mmark.EXTENSION_SHORT_REF
+	flags |= mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK
+	flags |= mmark.EXTENSION_INCLUDE
+
+	for _, extension := range cfg.Extensions {
+		if flag, ok := mmarkExtensionMap[extension]; ok {
+			flags |= flag
+		}
+	}
+	return flags
+}
+
+var mmarkExtensionMap = map[string]int{
+	"tables":                 mmark.EXTENSION_TABLES,
+	"fencedCode":             mmark.EXTENSION_FENCED_CODE,
+	"autolink":               mmark.EXTENSION_AUTOLINK,
+	"laxHtmlBlocks":          mmark.EXTENSION_LAX_HTML_BLOCKS,
+	"spaceHeaders":           mmark.EXTENSION_SPACE_HEADERS,
+	"hardLineBreak":          mmark.EXTENSION_HARD_LINE_BREAK,
+	"footnotes":              mmark.EXTENSION_FOOTNOTES,
+	"noEmptyLineBeforeBlock": mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK,
+	"headerIds":              mmark.EXTENSION_HEADER_IDS,
+	"autoHeaderIds":          mmark.EXTENSION_AUTO_HEADER_IDS,
+}
--- /dev/null
+++ b/markup/mmark/convert_test.go
@@ -1,0 +1,77 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mmark
+
+import (
+	"testing"
+
+	"github.com/spf13/viper"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
+	"github.com/miekg/mmark"
+
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestGetMmarkExtensions(t *testing.T) {
+	c := qt.New(t)
+	b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
+	c.Assert(err, qt.IsNil)
+
+	//TODO: This is doing the same just with different marks...
+	type data struct {
+		testFlag int
+	}
+
+	b.Extensions = []string{"tables"}
+	b.ExtensionsMask = []string{""}
+	allExtensions := []data{
+		{mmark.EXTENSION_TABLES},
+		{mmark.EXTENSION_FENCED_CODE},
+		{mmark.EXTENSION_AUTOLINK},
+		{mmark.EXTENSION_SPACE_HEADERS},
+		{mmark.EXTENSION_CITATION},
+		{mmark.EXTENSION_TITLEBLOCK_TOML},
+		{mmark.EXTENSION_HEADER_IDS},
+		{mmark.EXTENSION_AUTO_HEADER_IDS},
+		{mmark.EXTENSION_UNIQUE_HEADER_IDS},
+		{mmark.EXTENSION_FOOTNOTES},
+		{mmark.EXTENSION_SHORT_REF},
+		{mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK},
+		{mmark.EXTENSION_INCLUDE},
+	}
+
+	actualFlags := getMmarkExtensions(b)
+	for _, e := range allExtensions {
+		if actualFlags&e.testFlag != e.testFlag {
+			t.Errorf("Flag %v was not found in the list of extensions.", e)
+		}
+	}
+}
+
+func TestConvert(t *testing.T) {
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{Cfg: viper.New(), Logger: loggers.NewErrorLogger()})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<p>testContent</p>\n")
+}
--- /dev/null
+++ b/markup/mmark/renderer.go
@@ -1,0 +1,44 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mmark
+
+import (
+	"bytes"
+	"strings"
+
+	"github.com/miekg/mmark"
+
+	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/markup/internal"
+)
+
+// hugoHTMLRenderer wraps a blackfriday.Renderer, typically a blackfriday.Html
+// adding some custom behaviour.
+type mmarkRenderer struct {
+	Cfg       config.Provider
+	Config    *internal.BlackFriday
+	highlight func(code, lang, optsStr string) (string, error)
+	mmark.Renderer
+}
+
+// BlockCode renders a given text as a block of code.
+func (r *mmarkRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string, caption []byte, subfigure bool, callouts bool) {
+	if r.Cfg.GetBool("pygmentsCodeFences") && (lang != "" || r.Cfg.GetBool("pygmentsCodeFencesGuessSyntax")) {
+		str := strings.Trim(string(text), "\n\r")
+		highlighted, _ := r.highlight(str, lang, "")
+		out.WriteString(highlighted)
+	} else {
+		r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts)
+	}
+}
--- /dev/null
+++ b/markup/org/convert.go
@@ -1,0 +1,69 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package org converts Emacs Org-Mode to HTML.
+package org
+
+import (
+	"bytes"
+
+	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/niklasfasching/go-org/org"
+	"github.com/spf13/afero"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provide{}
+
+type provide struct {
+}
+
+func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		return &orgConverter{
+			ctx: ctx,
+			cfg: cfg,
+		}, nil
+	}
+	return n, nil
+}
+
+type orgConverter struct {
+	ctx converter.DocumentContext
+	cfg converter.ProviderConfig
+}
+
+func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	logger := c.cfg.Logger
+	config := org.New()
+	config.Log = logger.WARN
+	config.ReadFile = func(filename string) ([]byte, error) {
+		return afero.ReadFile(c.cfg.ContentFs, filename)
+	}
+	writer := org.NewHTMLWriter()
+	writer.HighlightCodeBlock = func(source, lang string) string {
+		highlightedSource, err := c.cfg.Highlight(source, lang, "")
+		if err != nil {
+			logger.ERROR.Printf("Could not highlight source as lang %s. Using raw source.", lang)
+			return source
+		}
+		return highlightedSource
+	}
+
+	html, err := config.Parse(bytes.NewReader(ctx.Src), c.ctx.DocumentName).Write(writer)
+	if err != nil {
+		logger.ERROR.Printf("Could not render org: %s. Using unrendered content.", err)
+		return converter.Bytes(ctx.Src), nil
+	}
+	return converter.Bytes([]byte(html)), nil
+}
--- /dev/null
+++ b/markup/org/convert_test.go
@@ -1,0 +1,35 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConvert(t *testing.T) {
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<p>\ntestContent\n</p>\n")
+}
--- /dev/null
+++ b/markup/pandoc/convert.go
@@ -1,0 +1,76 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pandoc converts content to HTML using Pandoc as an external helper.
+package pandoc
+
+import (
+	"os/exec"
+
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provider{}
+
+type provider struct {
+}
+
+func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		return &pandocConverter{
+			ctx: ctx,
+			cfg: cfg,
+		}, nil
+	}
+	return n, nil
+
+}
+
+type pandocConverter struct {
+	ctx converter.DocumentContext
+	cfg converter.ProviderConfig
+}
+
+func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil
+}
+
+// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
+func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte {
+	logger := c.cfg.Logger
+	path := getPandocExecPath()
+	if path == "" {
+		logger.ERROR.Println("pandoc not found in $PATH: Please install.\n",
+			"                 Leaving pandoc content unrendered.")
+		return src
+	}
+	args := []string{"--mathjax"}
+	return internal.ExternallyRenderContent(c.cfg, ctx, src, path, args)
+}
+
+func getPandocExecPath() string {
+	path, err := exec.LookPath("pandoc")
+	if err != nil {
+		return ""
+	}
+
+	return path
+}
+
+// Supports returns whether Pandoc is installed on this computer.
+func Supports() bool {
+	return getPandocExecPath() != ""
+}
--- /dev/null
+++ b/markup/pandoc/convert_test.go
@@ -1,0 +1,38 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pandoc
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConvert(t *testing.T) {
+	if !Supports() {
+		t.Skip("pandoc not installed")
+	}
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<p>testContent</p>\n")
+}
--- /dev/null
+++ b/markup/rst/convert.go
@@ -1,0 +1,109 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package rst converts content to HTML using the RST external helper.
+package rst
+
+import (
+	"bytes"
+	"os/exec"
+	"runtime"
+
+	"github.com/gohugoio/hugo/markup/internal"
+
+	"github.com/gohugoio/hugo/markup/converter"
+)
+
+// Provider is the package entry point.
+var Provider converter.NewProvider = provider{}
+
+type provider struct {
+}
+
+func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+	var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+		return &rstConverter{
+			ctx: ctx,
+			cfg: cfg,
+		}, nil
+	}
+	return n, nil
+
+}
+
+type rstConverter struct {
+	ctx converter.DocumentContext
+	cfg converter.ProviderConfig
+}
+
+func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+	return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil
+}
+
+// getRstContent calls the Python script rst2html as an external helper
+// to convert reStructuredText content to HTML.
+func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte {
+	logger := c.cfg.Logger
+	path := getRstExecPath()
+
+	if path == "" {
+		logger.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n",
+			"                 Leaving reStructuredText content unrendered.")
+		return src
+	}
+	logger.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...")
+	var result []byte
+	// certain *nix based OSs wrap executables in scripted launchers
+	// invoking binaries on these OSs via python interpreter causes SyntaxError
+	// invoke directly so that shebangs work as expected
+	// handle Windows manually because it doesn't do shebangs
+	if runtime.GOOS == "windows" {
+		python := internal.GetPythonExecPath()
+		args := []string{path, "--leave-comments", "--initial-header-level=2"}
+		result = internal.ExternallyRenderContent(c.cfg, ctx, src, python, args)
+	} else {
+		args := []string{"--leave-comments", "--initial-header-level=2"}
+		result = internal.ExternallyRenderContent(c.cfg, ctx, src, path, args)
+	}
+	// TODO(bep) check if rst2html has a body only option.
+	bodyStart := bytes.Index(result, []byte("<body>\n"))
+	if bodyStart < 0 {
+		bodyStart = -7 //compensate for length
+	}
+
+	bodyEnd := bytes.Index(result, []byte("\n</body>"))
+	if bodyEnd < 0 || bodyEnd >= len(result) {
+		bodyEnd = len(result) - 1
+		if bodyEnd < 0 {
+			bodyEnd = 0
+		}
+	}
+
+	return result[bodyStart+7 : bodyEnd]
+}
+
+func getRstExecPath() string {
+	path, err := exec.LookPath("rst2html")
+	if err != nil {
+		path, err = exec.LookPath("rst2html.py")
+		if err != nil {
+			return ""
+		}
+	}
+	return path
+}
+
+// Supports returns whether rst is installed on this computer.
+func Supports() bool {
+	return getRstExecPath() != ""
+}
--- /dev/null
+++ b/markup/rst/convert_test.go
@@ -1,0 +1,38 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package rst
+
+import (
+	"testing"
+
+	"github.com/gohugoio/hugo/common/loggers"
+
+	"github.com/gohugoio/hugo/markup/converter"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConvert(t *testing.T) {
+	if !Supports() {
+		t.Skip("rst not installed")
+	}
+	c := qt.New(t)
+	p, err := Provider.New(converter.ProviderConfig{Logger: loggers.NewErrorLogger()})
+	c.Assert(err, qt.IsNil)
+	conv, err := p.New(converter.DocumentContext{})
+	c.Assert(err, qt.IsNil)
+	b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(b.Bytes()), qt.Equals, "<div class=\"document\">\n\n\n<p>testContent</p>\n</div>")
+}
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -29,6 +29,7 @@
 	"github.com/gohugoio/hugo/helpers"
 	"github.com/gohugoio/hugo/hugofs"
 	"github.com/gohugoio/hugo/langs"
+	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 )
 
@@ -894,7 +895,7 @@
 func newDeps(cfg config.Provider) *deps.Deps {
 	l := langs.NewLanguage("en", cfg)
 	l.Set("i18nDir", "i18n")
-	cs, err := helpers.NewContentSpec(l)
+	cs, err := helpers.NewContentSpec(l, loggers.NewErrorLogger(), afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
 	}
--- a/tpl/data/resources_test.go
+++ b/tpl/data/resources_test.go
@@ -195,7 +195,7 @@
 	}
 	cfg.Set("allModules", modules.Modules{mod})
 
-	cs, err := helpers.NewContentSpec(cfg)
+	cs, err := helpers.NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
 	}
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -97,19 +97,16 @@
 		return "", err
 	}
 
-	m := ns.deps.ContentSpec.RenderBytes(
-		&helpers.RenderingContext{
-			Cfg:     ns.deps.Cfg,
-			Content: []byte(ss),
-			PageFmt: "markdown",
-			Config:  ns.deps.ContentSpec.BlackFriday,
-		},
-	)
+	b, err := ns.deps.ContentSpec.RenderMarkdown([]byte(ss))
 
+	if err != nil {
+		return "", err
+	}
+
 	// Strip if this is a short inline type of text.
-	m = ns.deps.ContentSpec.TrimShortHTML(m)
+	b = ns.deps.ContentSpec.TrimShortHTML(b)
 
-	return helpers.BytesToHTML(m), nil
+	return helpers.BytesToHTML(b), nil
 }
 
 // Plainify returns a copy of s with all HTML tags removed.
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -17,6 +17,9 @@
 	"html/template"
 	"testing"
 
+	"github.com/gohugoio/hugo/common/loggers"
+	"github.com/spf13/afero"
+
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
@@ -239,7 +242,7 @@
 
 	l := langs.NewLanguage("en", cfg)
 
-	cs, err := helpers.NewContentSpec(l)
+	cs, err := helpers.NewContentSpec(l, loggers.NewErrorLogger(), afero.NewMemMapFs())
 	if err != nil {
 		panic(err)
 	}