summaryrefslogtreecommitdiffstats
path: root/markup
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-08-16 15:55:03 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-11-06 19:09:08 +0100
commit5f6b6ec68936ebbbf590894c02a1a3ecad30735f (patch)
treef6c91e225a3f24f51af1bde5cfb5b88515d0665d /markup
parent366ee4d8da1c2b0c1751e9bf6d54638439735296 (diff)
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
Diffstat (limited to 'markup')
-rw-r--r--markup/asciidoc/convert.go97
-rw-r--r--markup/asciidoc/convert_test.go38
-rw-r--r--markup/blackfriday/convert.go224
-rw-r--r--markup/blackfriday/convert_test.go194
-rw-r--r--markup/blackfriday/renderer.go85
-rw-r--r--markup/converter/converter.go83
-rw-r--r--markup/internal/blackfriday.go108
-rw-r--r--markup/internal/external.go52
-rw-r--r--markup/markup.go83
-rw-r--r--markup/markup_test.go41
-rw-r--r--markup/mmark/convert.go143
-rw-r--r--markup/mmark/convert_test.go77
-rw-r--r--markup/mmark/renderer.go44
-rw-r--r--markup/org/convert.go69
-rw-r--r--markup/org/convert_test.go35
-rw-r--r--markup/pandoc/convert.go76
-rw-r--r--markup/pandoc/convert_test.go38
-rw-r--r--markup/rst/convert.go109
-rw-r--r--markup/rst/convert_test.go38
19 files changed, 1634 insertions, 0 deletions
diff --git a/markup/asciidoc/convert.go b/markup/asciidoc/convert.go
new file mode 100644
index 000000000..9e63911d8
--- /dev/null
+++ b/markup/asciidoc/convert.go
@@ -0,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() != "")
+}
diff --git a/markup/asciidoc/convert_test.go b/markup/asciidoc/convert_test.go
new file mode 100644
index 000000000..1c53f4f25
--- /dev/null
+++ b/markup/asciidoc/convert_test.go
@@ -0,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")
+}
diff --git a/markup/blackfriday/convert.go b/markup/blackfriday/convert.go
new file mode 100644
index 000000000..f9d957a4e
--- /dev/null
+++ b/markup/blackfriday/convert.go
@@ -0,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)
+)
diff --git a/markup/blackfriday/convert_test.go b/markup/blackfriday/convert_test.go
new file mode 100644
index 000000000..094edf35f
--- /dev/null
+++ b/markup/blackfriday/convert_test.go
@@ -0,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>")
+}
diff --git a/markup/blackfriday/renderer.go b/markup/blackfriday/renderer.go
new file mode 100644
index 000000000..9f4d44e02
--- /dev/null
+++ b/markup/blackfriday/renderer.go
@@ -0,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)
+ }
+ }
+}
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
new file mode 100644
index 000000000..809efca8e
--- /dev/null
+++ b/markup/converter/converter.go
@@ -0,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
+}
diff --git a/markup/internal/blackfriday.go b/markup/internal/blackfriday.go
new file mode 100644
index 000000000..373df0c50
--- /dev/null
+++ b/markup/internal/blackfriday.go
@@ -0,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