diff --git a/common/hugio/writers.go b/common/hugio/writers.go index 82c4dca52e4..d0a20c7593c 100644 --- a/common/hugio/writers.go +++ b/common/hugio/writers.go @@ -18,6 +18,12 @@ import ( "io/ioutil" ) +// StringWriter is an io.Writer that also supports writing strings. +type StringWriter interface { + io.Writer + WriteString(s string) (int, error) +} + type multiWriteCloser struct { io.Writer closers []io.WriteCloser diff --git a/docs/content/en/content-management/formats.md b/docs/content/en/content-management/formats.md index a0ed992f24e..fff6c3050f3 100644 --- a/docs/content/en/content-management/formats.md +++ b/docs/content/en/content-management/formats.md @@ -1,6 +1,6 @@ --- -title: Supported Content Formats -linktitle: Supported Content Formats +title: Content Formats +linktitle: Content Formats description: Both HTML and Markdown are supported content formats. date: 2017-01-10 publishdate: 2017-01-10 @@ -13,191 +13,35 @@ menu: weight: 20 weight: 20 #rem draft: false -aliases: [/content/markdown-extras/,/content/supported-formats/,/doc/supported-formats/,/tutorials/mathjax/] +aliases: [/content/markdown-extras/,/content/supported-formats/,/doc/supported-formats/] toc: true --- -**Markdown is the main content format** and comes in two flavours: The excellent [Blackfriday project][blackfriday] (name your files `*.md` or set `markup = "markdown"` in front matter) or its fork [Mmark][mmark] (name your files `*.mmark` or set `markup = "mmark"` in front matter), both very fast markdown engines written in Go. +You can put any file type into your `/content` directories, but Hugo uses the `markup` front matter value if set or the file extension (see `Markup identifiers` in the table below) to determine if the markup needs to be processed, e.g.: -For Emacs users, [go-org](https://github.com/niklasfasching/go-org) provides built-in native support for Org-mode (name your files `*.org` or set `markup = "org"` in front matter) +* Markdown converted to HTML +* [Shortcodes](/content-management/shortcodes/) processed +* Layout applied -But in many situations, plain HTML is what you want. Just name your files with `.html` or `.htm` extension inside your content folder. Note that if you want your HTML files to have a layout, they need front matter. It can be empty, but it has to be there: +The current list of content formats in Hugo: -```html ---- -title: "This is a content file in HTML" ---- - -
false
disables smart fractions.<sup>5</sup>⁄<sub>12</sub>
).fractions = false
, Blackfriday still converts `1/2`, `1/4`, and `3/4` respectively to ½ (½
), ¼ (¼
) and ¾ (¾
), but only these three.
-
-`smartDashes`
-: default: **`true`** `, lang, lang)
- return preRe.ReplaceAllString(code, fmt.Sprintf("$1%s$2
$3", codeTag))
-}
-
-func chromaHighlight(w io.Writer, source, lexer, style string, f chroma.Formatter) error {
- l := lexers.Get(lexer)
- if l == nil {
- l = lexers.Analyse(source)
- }
- if l == nil {
- l = lexers.Fallback
- }
- l = chroma.Coalesce(l)
-
- if f == nil {
- f = formatters.Fallback
- }
-
- s := styles.Get(style)
- if s == nil {
- s = styles.Fallback
- }
-
- it, err := l.Tokenise(nil, source)
- if err != nil {
- return err
- }
-
- return f.Format(w, s, it)
-}
-
-var pygmentsKeywords = make(map[string]bool)
-
-func init() {
- pygmentsKeywords["encoding"] = true
- pygmentsKeywords["outencoding"] = true
- pygmentsKeywords["nowrap"] = true
- pygmentsKeywords["full"] = true
- pygmentsKeywords["title"] = true
- pygmentsKeywords["style"] = true
- pygmentsKeywords["noclasses"] = true
- pygmentsKeywords["classprefix"] = true
- pygmentsKeywords["cssclass"] = true
- pygmentsKeywords["cssstyles"] = true
- pygmentsKeywords["prestyles"] = true
- pygmentsKeywords["linenos"] = true
- pygmentsKeywords["hl_lines"] = true
- pygmentsKeywords["linenostart"] = true
- pygmentsKeywords["linenostep"] = true
- pygmentsKeywords["linenospecial"] = true
- pygmentsKeywords["nobackground"] = true
- pygmentsKeywords["lineseparator"] = true
- pygmentsKeywords["lineanchors"] = true
- pygmentsKeywords["linespans"] = true
- pygmentsKeywords["anchorlinenos"] = true
- pygmentsKeywords["startinline"] = true
-}
-
-func parseOptions(defaults map[string]string, in string) (map[string]string, error) {
- in = strings.Trim(in, " ")
- opts := make(map[string]string)
-
- for k, v := range defaults {
- opts[k] = v
- }
-
- if in == "" {
- return opts, nil
- }
-
- for _, v := range strings.Split(in, ",") {
- keyVal := strings.Split(v, "=")
- key := strings.ToLower(strings.Trim(keyVal[0], " "))
- if len(keyVal) != 2 || !pygmentsKeywords[key] {
- return opts, fmt.Errorf("invalid Pygments option: %s", key)
- }
- opts[key] = keyVal[1]
- }
-
- return opts, nil
-}
-
-func createOptionsString(options map[string]string) string {
- var keys []string
- for k := range options {
- keys = append(keys, k)
- }
- sort.Strings(keys)
-
- var optionsStr string
- for i, k := range keys {
- optionsStr += fmt.Sprintf("%s=%s", k, options[k])
- if i < len(options)-1 {
- optionsStr += ","
- }
- }
-
- return optionsStr
-}
-
-func parseDefaultPygmentsOpts(cfg config.Provider) (map[string]string, error) {
- options, err := parseOptions(nil, cfg.GetString("pygmentsOptions"))
- if err != nil {
- return nil, err
- }
-
- if cfg.IsSet("pygmentsStyle") {
- options["style"] = cfg.GetString("pygmentsStyle")
- }
-
- if cfg.IsSet("pygmentsUseClasses") {
- if cfg.GetBool("pygmentsUseClasses") {
- options["noclasses"] = "false"
- } else {
- options["noclasses"] = "true"
- }
-
- }
-
- if _, ok := options["encoding"]; !ok {
- options["encoding"] = "utf8"
- }
-
- return options, nil
-}
-
-func (cs *ContentSpec) chromaFormatterFromOptions(pygmentsOpts map[string]string) (chroma.Formatter, error) {
- var options = []html.Option{html.TabWidth(4)}
-
- if pygmentsOpts["noclasses"] == "false" {
- options = append(options, html.WithClasses())
- }
-
- lineNumbers := pygmentsOpts["linenos"]
- if lineNumbers != "" {
- options = append(options, html.WithLineNumbers())
- if lineNumbers != "inline" {
- options = append(options, html.LineNumbersInTable())
- }
- }
-
- startLineStr := pygmentsOpts["linenostart"]
- var startLine = 1
- if startLineStr != "" {
-
- line, err := strconv.Atoi(strings.TrimSpace(startLineStr))
- if err == nil {
- startLine = line
- options = append(options, html.BaseLineNumber(startLine))
- }
- }
-
- hlLines := pygmentsOpts["hl_lines"]
-
- if hlLines != "" {
- ranges, err := hlLinesToRanges(startLine, hlLines)
-
- if err == nil {
- options = append(options, html.HighlightLines(ranges))
- }
- }
-
- return html.New(options...), nil
-}
-
-func (cs *ContentSpec) parsePygmentsOpts(in string) (map[string]string, error) {
- opts, err := parseOptions(cs.defatultPygmentsOpts, in)
- if err != nil {
- return nil, err
- }
- return opts, nil
-
-}
-
-func (cs *ContentSpec) createPygmentsOptionsString(in string) (string, error) {
- opts, err := cs.parsePygmentsOpts(in)
- if err != nil {
- return "", err
- }
- return createOptionsString(opts), nil
-}
-
-// startLine compansates for https://github.com/alecthomas/chroma/issues/30
-func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
- var ranges [][2]int
- s = strings.TrimSpace(s)
-
- if s == "" {
- return ranges, nil
- }
-
- // Variants:
- // 1 2 3 4
- // 1-2 3-4
- // 1-2 3
- // 1 3-4
- // 1 3-4
- fields := strings.Split(s, " ")
- for _, field := range fields {
- field = strings.TrimSpace(field)
- if field == "" {
- continue
- }
- numbers := strings.Split(field, "-")
- var r [2]int
- first, err := strconv.Atoi(numbers[0])
- if err != nil {
- return ranges, err
- }
- first = first + startLine - 1
- r[0] = first
- if len(numbers) > 1 {
- second, err := strconv.Atoi(numbers[1])
- if err != nil {
- return ranges, err
- }
- second = second + startLine - 1
- r[1] = second
- } else {
- r[1] = first
- }
-
- ranges = append(ranges, r)
- }
- return ranges, nil
-
-}
diff --git a/helpers/pygments_test.go b/helpers/pygments_test.go
deleted file mode 100644
index 05d86e10405..00000000000
--- a/helpers/pygments_test.go
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright 2015 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 (
- "fmt"
- "reflect"
- "testing"
-
- "github.com/alecthomas/chroma/formatters/html"
-
- qt "github.com/frankban/quicktest"
- "github.com/spf13/viper"
-)
-
-func TestParsePygmentsArgs(t *testing.T) {
- c := qt.New(t)
-
- for i, this := range []struct {
- in string
- pygmentsStyle string
- pygmentsUseClasses bool
- expect1 interface{}
- }{
- {"", "foo", true, "encoding=utf8,noclasses=false,style=foo"},
- {"style=boo,noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"},
- {"Style=boo, noClasses=true", "foo", true, "encoding=utf8,noclasses=true,style=boo"},
- {"noclasses=true", "foo", true, "encoding=utf8,noclasses=true,style=foo"},
- {"style=boo", "foo", true, "encoding=utf8,noclasses=false,style=boo"},
- {"boo=invalid", "foo", false, false},
- {"style", "foo", false, false},
- } {
-
- v := viper.New()
- v.Set("pygmentsStyle", this.pygmentsStyle)
- v.Set("pygmentsUseClasses", this.pygmentsUseClasses)
- spec, err := NewContentSpec(v, nil, nil)
- c.Assert(err, qt.IsNil)
-
- result1, err := spec.createPygmentsOptionsString(this.in)
- if b, ok := this.expect1.(bool); ok && !b {
- if err == nil {
- t.Errorf("[%d] parsePygmentArgs didn't return an expected error", i)
- }
- } else {
- if err != nil {
- t.Errorf("[%d] parsePygmentArgs failed: %s", i, err)
- continue
- }
- if result1 != this.expect1 {
- t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result1, this.expect1)
- }
-
- }
- }
-}
-
-func TestParseDefaultPygmentsArgs(t *testing.T) {
- c := qt.New(t)
-
- expect := "encoding=utf8,noclasses=false,style=foo"
-
- for i, this := range []struct {
- in string
- pygmentsStyle interface{}
- pygmentsUseClasses interface{}
- pygmentsOptions string
- }{
- {"", "foo", true, "style=override,noclasses=override"},
- {"", nil, nil, "style=foo,noclasses=false"},
- {"style=foo,noclasses=false", nil, nil, "style=override,noclasses=override"},
- {"style=foo,noclasses=false", "override", false, "style=override,noclasses=override"},
- } {
- v := viper.New()
-
- v.Set("pygmentsOptions", this.pygmentsOptions)
-
- if s, ok := this.pygmentsStyle.(string); ok {
- v.Set("pygmentsStyle", s)
- }
-
- if b, ok := this.pygmentsUseClasses.(bool); ok {
- v.Set("pygmentsUseClasses", b)
- }
-
- spec, err := NewContentSpec(v, nil, nil)
- c.Assert(err, qt.IsNil)
-
- result, err := spec.createPygmentsOptionsString(this.in)
- if err != nil {
- t.Errorf("[%d] parsePygmentArgs failed: %s", i, err)
- continue
- }
- if result != expect {
- t.Errorf("[%d] parsePygmentArgs got %v but expected %v", i, result, expect)
- }
- }
-}
-
-type chromaInfo struct {
- classes bool
- lineNumbers bool
- lineNumbersInTable bool
- highlightRangesLen int
- highlightRangesStr string
- baseLineNumber int
-}
-
-func formatterChromaInfo(f *html.Formatter) chromaInfo {
- v := reflect.ValueOf(f).Elem()
- c := chromaInfo{}
- // Hack:
-
- c.classes = f.Classes
- c.lineNumbers = v.FieldByName("lineNumbers").Bool()
- c.lineNumbersInTable = v.FieldByName("lineNumbersInTable").Bool()
- c.baseLineNumber = int(v.FieldByName("baseLineNumber").Int())
- vv := v.FieldByName("highlightRanges")
- c.highlightRangesLen = vv.Len()
- c.highlightRangesStr = fmt.Sprint(vv)
-
- return c
-}
-
-func TestChromaHTMLHighlight(t *testing.T) {
- c := qt.New(t)
-
- v := viper.New()
- v.Set("pygmentsUseClasses", true)
- spec, err := NewContentSpec(v, nil, nil)
- c.Assert(err, qt.IsNil)
-
- result, err := spec.Highlight(`echo "Hello"`, "bash", "")
- c.Assert(err, qt.IsNil)
-
- c.Assert(result, qt.Contains, `echo "Hello"
}}
void do();
{{< /highlight >}}`,
- `(?s)`,
+ `(?s)`,
},
} {
diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go
index 1a690cdd9ca..03853bff6cc 100644
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -671,7 +671,7 @@ END
b.CreateSites().Build(BuildCfg{})
- contentMatchers := []string{"Another header
", "Another header
", "The End.
"}
+ contentMatchers := []string{"Another header
", "Another header
", "The End.
"}
for i := 1; i <= numPages; i++ {
if i%3 != 0 {
@@ -691,13 +691,13 @@ END
checkContent(b, fmt.Sprintf("public/%s/page%d/index.json", section, i), contentMatchers...)
}
- checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335\n\nRender 1: View: 8335\n\nRender 2: View: 8335\n\nRender 3: View: 8335\n\nRender 4: View: 8335\n\nEND\n")
- checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8335", "Render 4: View: 8335\n\nEND")
- checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8335", "4: View: 8335\n\nEND")
+ checkContent(b, "public/s1/index.html", "P: s1/_index.md\nList: 10|List Content: 8033\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8033\n\nRender 1: View: 8033\n\nRender 2: View: 8033\n\nRender 3: View: 8033\n\nRender 4: View: 8033\n\nEND\n")
+ checkContent(b, "public/s2/index.html", "P: s2/_index.md\nList: 10|List Content: 8033", "Render 4: View: 8033\n\nEND")
+ checkContent(b, "public/index.html", "P: _index.md\nList: 10|List Content: 8033", "4: View: 8033\n\nEND")
// Check paginated pages
for i := 2; i <= 9; i++ {
- checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8335\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8335", "Render 4: View: 8335\n\nEND")
+ checkContent(b, fmt.Sprintf("public/page/%d/index.html", i), fmt.Sprintf("Page: %d", i), "Content: 8033\n\n\nL1: 500 L2: 5\n\nRender 0: View: 8033", "Render 4: View: 8033\n\nEND")
}
}
@@ -977,7 +977,10 @@ enableRobotsTXT = true
[permalinks]
other = "/somewhere/else/:filename"
-[blackfriday]
+# TODO(bep)
+[markup]
+ defaultMarkdownHandler = "blackfriday"
+[markup.blackfriday]
angledQuotes = true
[Taxonomies]
@@ -1035,7 +1038,10 @@ enableRobotsTXT: true
permalinks:
other: "/somewhere/else/:filename"
-blackfriday:
+# TODO(bep)
+markup:
+ defaultMarkdownHandler: blackfriday
+ blackFriday:
angledQuotes: true
Taxonomies:
@@ -1093,9 +1099,12 @@ var multiSiteJSONConfigTemplate = `
"permalinks": {
"other": "/somewhere/else/:filename"
},
- "blackfriday": {
- "angledQuotes": true
- },
+ "markup": {
+ "defaultMarkdownHandler": "blackfriday",
+ "blackfriday": {
+ "angledQuotes": true
+ }
+ },
"Taxonomies": {
"tag": "tags"
},
diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go
index d137ac34005..d0d45bc72f4 100644
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -565,7 +565,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte
pm.sitemap = p.s.siteCfg.sitemap
}
- pm.markup = helpers.GuessType(pm.markup)
+ pm.markup = p.s.ContentSpec.ResolveMarkup(pm.markup)
if draft != nil && published != nil {
pm.draft = *draft
@@ -596,7 +596,7 @@ func (p *pageMeta) applyDefaultValues() error {
if p.markup == "" {
if !p.File().IsZero() {
// Fall back to file extension
- p.markup = helpers.GuessType(p.File().Ext())
+ p.markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
}
if p.markup == "" {
p.markup = "markdown"
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index ef2419eca34..dacf2d0090c 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -103,11 +103,14 @@ func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutpu
if err != nil {
return err
}
+ cp.convertedResult = r
cp.workContent = r.Bytes()
- tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
- cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
- cp.workContent = tmpContent
+ if _, ok := r.(converter.TableOfContentsProvider); !ok {
+ tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
+ cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
+ cp.workContent = tmpContent
+ }
}
if cp.placeholdersEnabled {
@@ -223,7 +226,8 @@ type pageContentOutput struct {
// Content state
- workContent []byte
+ workContent []byte
+ convertedResult converter.Result
// Temporary storage of placeholders mapped to their content.
// These are shortcodes etc. Some of these will need to be replaced
@@ -284,6 +288,10 @@ func (p *pageContentOutput) Summary() template.HTML {
func (p *pageContentOutput) TableOfContents() template.HTML {
p.p.s.initInit(p.initMain, p.p)
+ if tocProvider, ok := p.convertedResult.(converter.TableOfContentsProvider); ok {
+ cfg := p.p.s.ContentSpec.Converters.GetMarkupConfig()
+ return template.HTML(tocProvider.TableOfContents().ToHTML(cfg.TableOfContents.StartLevel, cfg.TableOfContents.EndLevel))
+ }
return p.tableOfContents
}
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index 6b9c4193d3b..f2af3f32a87 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -326,7 +326,7 @@ func normalizeContent(c string) string {
func checkPageTOC(t *testing.T, page page.Page, toc string) {
if page.TableOfContents() != template.HTML(toc) {
- t.Fatalf("Page TableOfContents is: %q.\nExpected %q", page.TableOfContents(), toc)
+ t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
}
}
@@ -442,6 +442,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
+
c := qt.New(t)
writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithSummaryDelimiterAndMarkdownThatCrossesBorder)
@@ -453,12 +454,12 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) {
p := s.RegularPages()[0]
if p.Summary() != template.HTML(
- "The best static site generator.1
") {
+ "The best static site generator.1
") {
t.Fatalf("Got summary:\n%q", p.Summary())
}
cnt := content(p)
- if cnt != "The best static site generator.1
\n\n\n\n
\n\n\n- Many people say so.\n [return]
\n
\n" {
+ if cnt != "The best static site generator.1
\n\n
\n\n- \n
Many people say so.
\n \n
\n " {
t.Fatalf("Got content:\n%q", cnt)
}
}
@@ -673,23 +674,13 @@ func TestPageWithShortCodeInSummary(t *testing.T) {
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary)
}
-func TestPageWithEmbeddedScriptTag(t *testing.T) {
- t.Parallel()
- assertFunc := func(t *testing.T, ext string, pages page.Pages) {
- p := pages[0]
- if ext == "ad" || ext == "rst" {
- // TOD(bep)
- return
- }
- checkPageContent(t, p, "\n", ext)
- }
-
- testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithEmbeddedScript)
-}
-
func TestPageWithAdditionalExtension(t *testing.T) {
t.Parallel()
cfg, fs := newTestCfg()
+ cfg.Set("markup", map[string]interface{}{
+ "defaultMarkdownHandler": "blackfriday", // TODO(bep)
+ })
+
c := qt.New(t)
writeSource(t, fs, filepath.Join("content", "simple.md"), simplePageWithAdditionalExtension)
@@ -716,8 +707,8 @@ func TestTableOfContents(t *testing.T) {
p := s.RegularPages()[0]
- checkPageContent(t, p, "\n\nFor some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.
\n\nAA
\n\nI have no idea, of course, how long it took me to reach the limit of the plain,\nbut at last I entered the foothills, following a pretty little canyon upward\ntoward the mountains. Beside me frolicked a laughing brooklet, hurrying upon\nits noisy way down to the silent sea. In its quieter pools I discovered many\nsmall fish, of four-or five-pound weight I should imagine. In appearance,\nexcept as to size and color, they were not unlike the whale of our own seas. As\nI watched them playing about I discovered, not only that they suckled their\nyoung, but that at intervals they rose to the surface to breathe as well as to\nfeed upon certain grasses and a strange, scarlet lichen which grew upon the\nrocks just above the water line.
\n\nAAA
\n\nI remember I felt an extraordinary persuasion that I was being played with,\nthat presently, when I was upon the very verge of safety, this mysterious\ndeath–as swift as the passage of light–would leap after me from the pit about\nthe cylinder and strike me down. ## BB
\n\nBBB
\n\n“You’re a great Granser,” he cried delightedly, “always making believe them little marks mean something.”
\n")
- checkPageTOC(t, p, "")
+ checkPageContent(t, p, "For some moments the old man did not reply. He stood with bowed head, buried in deep thought. But at last he spoke.
AA
I have no idea, of course, how long it took me to reach the limit of the plain, but at last I entered the foothills, following a pretty little canyon upward toward the mountains. Beside me frolicked a laughing brooklet, hurrying upon its noisy way down to the silent sea. In its quieter pools I discovered many small fish, of four-or five-pound weight I should imagine. In appearance, except as to size and color, they were not unlike the whale of our own seas. As I watched them playing about I discovered, not only that they suckled their young, but that at intervals they rose to the surface to breathe as well as to feed upon certain grasses and a strange, scarlet lichen which grew upon the rocks just above the water line.
AAA
I remember I felt an extraordinary persuasion that I was being played with, that presently, when I was upon the very verge of safety, this mysterious death–as swift as the passage of light–would leap after me from the pit about the cylinder and strike me down. ## BB
BBB
“You're a great Granser,” he cried delightedly, “always making believe them little marks mean something.”
")
+ checkPageTOC(t, p, "")
}
func TestPageWithMoreTag(t *testing.T) {
@@ -1518,12 +1509,12 @@ Summary: In Chinese, 好 means good.
b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557")
- b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1583")
+ b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1582")
- b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652")
- b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 652")
- b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 653")
- b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 653")
+ b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
+ b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
+ b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 652")
+ b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 652")
}
@@ -1608,3 +1599,49 @@ author = "Jo Nesbø"
b.AssertFileContent("public/index.html", "Author site config: Kurt Vonnegut")
}
+
+func TestGoldmark(t *testing.T) {
+ t.Parallel()
+
+ b := newTestSitesBuilder(t).WithConfigFile("toml", `
+baseURL = "https://example.org"
+
+[markup]
+defaultMarkdownHandler="goldmark"
+
+`)
+ b.WithTemplatesAdded("_default/single.html", `
+Title: {{ .Title }}
+ToC: {{ .TableOfContents }}
+Content: {{ .Content }}
+
+`, "shortcodes/s.html", "SHORT")
+
+ b.WithContent("page.md", `
++++
+title = "A Page!"
++++
+
+Foo.
+
+# Header {{% s %}} Number 1
+
+## First h2 [foo](/bar)
+
+Some text.
+
+### H3
+
+Some more text.
+
+## Second h2
+
+And then some.
+
+`)
+
+ b.Build(BuildCfg{})
+
+ b.AssertFileContent("public/page/index.html", `Header SHORT Number 1
`)
+
+}
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index eb763b2a02c..fdf37b6c26b 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -44,6 +44,11 @@ func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func
func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {
t.Helper()
cfg, fs := newTestCfg()
+
+ cfg.Set("markup", map[string]interface{}{
+ "defaultMarkdownHandler": "blackfriday", // TODO(bep)
+ })
+
c := qt.New(t)
// Need some front matter, see https://github.com/gohugoio/hugo/issues/2337
@@ -584,6 +589,9 @@ title: "Foo"
cfg.Set("pygmentsUseClasses", true)
cfg.Set("pygmentsCodefences", true)
+ cfg.Set("markup", map[string]interface{}{
+ "defaultMarkdownHandler": "blackfriday", // TODO(bep)
+ })
writeSourcesToSource(t, "content", fs, sources...)
@@ -597,6 +605,7 @@ title: "Foo"
th := newTestHelper(s.Cfg, s.Fs, t)
expected := cast.ToStringSlice(test.expected)
+
th.assertFileContent(filepath.FromSlash(test.outFile), expected...)
})
@@ -1245,6 +1254,9 @@ func TestShortcodeRef(t *testing.T) {
v.Set("blackfriday", map[string]interface{}{
"plainIDAnchors": plainIDAnchors,
})
+ v.Set("markup", map[string]interface{}{
+ "defaultMarkdownHandler": "blackfriday", // TODO(bep)
+ })
builder := newTestSitesBuilder(t).WithViper(v)
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index 995664da420..096c4d480a1 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -231,12 +231,12 @@ THE END.`, refShortcode),
// Issue #1753: Should not add a trailing newline after shortcode.
{
filepath.FromSlash("sect/doc3.md"),
- fmt.Sprintf(`**Ref 1:**{{< %s "sect/doc3.md" >}}.`, refShortcode),
+ fmt.Sprintf(`**Ref 1:** {{< %s "sect/doc3.md" >}}.`, refShortcode),
},
// Issue #3703
{
filepath.FromSlash("sect/doc4.md"),
- fmt.Sprintf(`**Ref 1:**{{< %s "%s" >}}.`, refShortcode, doc3Slashed),
+ fmt.Sprintf(`**Ref 1:** {{< %s "%s" >}}.`, refShortcode, doc3Slashed),
},
}
@@ -267,9 +267,9 @@ THE END.`, refShortcode),
expected string
}{
{filepath.FromSlash(fmt.Sprintf("public/sect/doc1%s", expectedPathSuffix)), fmt.Sprintf("Ref 2: %s/sect/doc2%s
\n", expectedBase, expectedURLSuffix)},
- {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("Ref 1:
\n\n%s/sect/doc1%s\n\nTHE END.
\n", expectedBase, expectedURLSuffix)},
- {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("Ref 1:%s/sect/doc3%s.
\n", expectedBase, expectedURLSuffix)},
- {filepath.FromSlash(fmt.Sprintf("public/sect/doc4%s", expectedPathSuffix)), fmt.Sprintf("Ref 1:%s/sect/doc3%s.
\n", expectedBase, expectedURLSuffix)},
+ {filepath.FromSlash(fmt.Sprintf("public/sect/doc2%s", expectedPathSuffix)), fmt.Sprintf("Ref 1:
\n%s/sect/doc1%s\nTHE END.
\n", expectedBase, expectedURLSuffix)},
+ {filepath.FromSlash(fmt.Sprintf("public/sect/doc3%s", expectedPathSuffix)), fmt.Sprintf("Ref 1: %s/sect/doc3%s.
\n", expectedBase, expectedURLSuffix)},
+ {filepath.FromSlash(fmt.Sprintf("public/sect/doc4%s", expectedPathSuffix)), fmt.Sprintf("Ref 1: %s/sect/doc3%s.
\n", expectedBase, expectedURLSuffix)},
}
for _, test := range tests {
@@ -330,12 +330,12 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) {
expected string
}{
{filepath.FromSlash("public/index.html"), "Home Sweet Home."},
- {filepath.FromSlash(expectedPagePath), "\n\ntitle
\n\nsome content
\n"},
+ {filepath.FromSlash(expectedPagePath), "title
\nsome content
\n"},
{filepath.FromSlash("public/404.html"), "Page Not Found."},
{filepath.FromSlash("public/index.xml"), "RSS "},
{filepath.FromSlash("public/sitemap.xml"), "SITEMAP "},
// Issue #1923
- {filepath.FromSlash("public/ugly.html"), "\n\ntitle
\n\ndoc2 content
\n"},
+ {filepath.FromSlash("public/ugly.html"), "title
\ndoc2 content
\n"},
}
for _, p := range s.RegularPages() {
@@ -540,14 +540,14 @@ func TestSkipRender(t *testing.T) {
doc string
expected string
}{
- {filepath.FromSlash("public/sect/doc1.html"), "\n\ntitle
\n\nsome content
\n"},
+ {filepath.FromSlash("public/sect/doc1.html"), "title
\nsome content
\n"},
{filepath.FromSlash("public/sect/doc2.html"), "more content"},
- {filepath.FromSlash("public/sect/doc3.html"), "\n\ndoc3
\n\nsome content
\n"},
- {filepath.FromSlash("public/sect/doc4.html"), "\n\ndoc4
\n\nsome content
\n"},
+ {filepath.FromSlash("public/sect/doc3.html"), "doc3
\nsome content
\n"},
+ {filepath.FromSlash("public/sect/doc4.html"), "doc4
\nsome content
\n"},
{filepath.FromSlash("public/sect/doc5.html"), "
body5"},
{filepath.FromSlash("public/sect/doc6.html"), "
body5"},
{filepath.FromSlash("public/doc7.html"), "doc7 content"},
- {filepath.FromSlash("public/sect/doc8.html"), "\n\ntitle
\n\nsome content
\n"},
+ {filepath.FromSlash("public/sect/doc8.html"), "title
\nsome content
\n"},
{filepath.FromSlash("public/doc9.html"), "doc9: SHORT"},
}
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
index 00f80235964..d861a5e0941 100644
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -743,7 +743,7 @@ func (th testHelper) assertFileContent(filename string, matches ...string) {
content := readDestination(th, th.Fs, filename)
for _, match := range matches {
match = th.replaceDefaultContentLanguageValue(match)
- th.Assert(strings.Contains(content, match), qt.Equals, true)
+ th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content))
}
}
@@ -755,7 +755,7 @@ func (th testHelper) assertFileContentRegexp(filename string, matches ...string)
r := regexp.MustCompile(match)
matches := r.MatchString(content)
if !matches {
- fmt.Println(content)
+ fmt.Println(match+":\n", content)
}
th.Assert(matches, qt.Equals, true)
}
diff --git a/markup/asciidoc/convert.go b/markup/asciidoc/convert.go
index 9e63911d837..65fdde0f564 100644
--- a/markup/asciidoc/convert.go
+++ b/markup/asciidoc/convert.go
@@ -24,19 +24,18 @@ import (
)
// Provider is the package entry point.
-var Provider converter.NewProvider = provider{}
+var Provider converter.ProviderProvider = 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 converter.NewProvider("asciidoc", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &asciidocConverter{
ctx: ctx,
cfg: cfg,
}, nil
- }
- return n, nil
+ }), nil
}
type asciidocConverter struct {
diff --git a/markup/blackfriday/blackfriday_config/config.go b/markup/blackfriday/blackfriday_config/config.go
new file mode 100644
index 00000000000..f26f7c570cb
--- /dev/null
+++ b/markup/blackfriday/blackfriday_config/config.go
@@ -0,0 +1,70 @@
+// 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 blackfriday_config
+
+import (
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+)
+
+// Default holds the default BlackFriday config.
+// Do not change!
+var Default = Config{
+ Smartypants: true,
+ AngledQuotes: false,
+ SmartypantsQuotesNBSP: false,
+ Fractions: true,
+ HrefTargetBlank: false,
+ NofollowLinks: false,
+ NoreferrerLinks: false,
+ SmartDashes: true,
+ LatexDashes: true,
+ PlainIDAnchors: true,
+ TaskLists: true,
+ SkipHTML: false,
+}
+
+// Config holds configuration values for BlackFriday rendering.
+// It is kept here because it's used in several packages.
+type Config 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 UpdateConfig(b Config, m map[string]interface{}) (Config, error) {
+ if err := mapstructure.Decode(m, &b); err != nil {
+ return b, errors.WithMessage(err, "failed to decode rendering config")
+ }
+ return b, nil
+}
diff --git a/markup/blackfriday/convert.go b/markup/blackfriday/convert.go
index f9d957a4e4d..350defcb63c 100644
--- a/markup/blackfriday/convert.go
+++ b/markup/blackfriday/convert.go
@@ -15,36 +15,27 @@
package blackfriday
import (
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"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{}
+var Provider converter.ProviderProvider = 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")
+ defaultExtensions := getMarkdownExtensions(cfg.MarkupConfig.BlackFriday)
- var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
- b := defaultBlackFriday
+ return converter.NewProvider("blackfriday", func(ctx converter.DocumentContext) (converter.Converter, error) {
+ b := cfg.MarkupConfig.BlackFriday
extensions := defaultExtensions
if ctx.ConfigOverrides != nil {
var err error
- b, err = internal.UpdateBlackFriday(b, ctx.ConfigOverrides)
+ b, err = blackfriday_config.UpdateConfig(b, ctx.ConfigOverrides)
if err != nil {
return nil, err
}
@@ -56,27 +47,16 @@ func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error)
bf: b,
extensions: extensions,
cfg: cfg,
-
- pygmentsCodeFences: pygmentsCodeFences,
- pygmentsCodeFencesGuessSyntax: pygmentsCodeFencesGuessSyntax,
- pygmentsOptions: pygmentsOptions,
}, nil
- }
-
- return n, nil
+ }), nil
}
type blackfridayConverter struct {
ctx converter.DocumentContext
- bf *internal.BlackFriday
+ bf blackfriday_config.Config
extensions int
-
- pygmentsCodeFences bool
- pygmentsCodeFencesGuessSyntax bool
- pygmentsOptions string
-
- cfg converter.ProviderConfig
+ cfg converter.ProviderConfig
}
func (c *blackfridayConverter) AnchorSuffix() string {
@@ -90,7 +70,6 @@ func (c *blackfridayConverter) Convert(ctx converter.RenderContext) (converter.R
r := c.getHTMLRenderer(ctx.RenderTOC)
return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil
-
}
func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
@@ -114,7 +93,7 @@ func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Rende
}
}
-func getFlags(renderTOC bool, cfg *internal.BlackFriday) int {
+func getFlags(renderTOC bool, cfg blackfriday_config.Config) int {
var flags int
@@ -168,7 +147,7 @@ func getFlags(renderTOC bool, cfg *internal.BlackFriday) int {
return flags
}
-func getMarkdownExtensions(cfg *internal.BlackFriday) int {
+func getMarkdownExtensions(cfg blackfriday_config.Config) int {
// Default Blackfriday common extensions
commonExtensions := 0 |
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
diff --git a/markup/blackfriday/convert_test.go b/markup/blackfriday/convert_test.go
index 094edf35f6c..b4d66dec66b 100644
--- a/markup/blackfriday/convert_test.go
+++ b/markup/blackfriday/convert_test.go
@@ -18,19 +18,15 @@ import (
"github.com/spf13/viper"
- "github.com/gohugoio/hugo/markup/internal"
-
"github.com/gohugoio/hugo/markup/converter"
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"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 := blackfriday_config.Default
b.Extensions = []string{"headerId"}
b.ExtensionsMask = []string{"noIntraEmphasis"}
@@ -45,9 +41,7 @@ func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
testFlag int
}
- c := qt.New(t)
- b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
- c.Assert(err, qt.IsNil)
+ b := blackfriday_config.Default
b.Extensions = []string{""}
b.ExtensionsMask = []string{""}
@@ -79,9 +73,7 @@ func TestGetMarkdownExtensionsByDefaultAllExtensionsAreEnabled(t *testing.T) {
}
func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
- c := qt.New(t)
- b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
- c.Assert(err, qt.IsNil)
+ b := blackfriday_config.Default
b.Extensions = []string{"definitionLists"}
b.ExtensionsMask = []string{""}
@@ -93,10 +85,7 @@ func TestGetMarkdownExtensionsAddingFlagsThroughRenderingContext(t *testing.T) {
}
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)
+ b := blackfriday_config.Default
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)
@@ -105,9 +94,8 @@ func TestGetFlags(t *testing.T) {
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)
+
+ b := blackfriday_config.Default
type data struct {
testFlag int
@@ -145,9 +133,8 @@ func TestGetAllFlags(t *testing.T) {
for _, d := range allFlags {
expectedFlags |= d.testFlag
}
- if expectedFlags != actualFlags {
- t.Errorf("Expected flags (%d) did not equal actual (%d) flags.", expectedFlags, actualFlags)
- }
+
+ c.Assert(actualFlags, qt.Equals, expectedFlags)
}
func TestConvert(t *testing.T) {
diff --git a/markup/blackfriday/renderer.go b/markup/blackfriday/renderer.go
index 9f4d44e02d8..a46e46b55f3 100644
--- a/markup/blackfriday/renderer.go
+++ b/markup/blackfriday/renderer.go
@@ -30,10 +30,9 @@ type hugoHTMLRenderer struct {
// 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
+ if r.c.cfg.MarkupConfig.Highlight.CodeFences {
str := strings.Trim(string(text), "\n\r")
- highlighted, _ := r.c.cfg.Highlight(str, lang, opts)
+ highlighted, _ := r.c.cfg.Highlight(str, lang, "")
out.WriteString(highlighted)
} else {
r.Renderer.BlockCode(out, text, lang)
diff --git a/markup/converter/converter.go b/markup/converter/converter.go
index 809efca8ec1..a1141f65ccc 100644
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -16,33 +16,51 @@ package converter
import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/markup/markup_config"
+ "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/spf13/afero"
)
// ProviderConfig configures a new Provider.
type ProviderConfig struct {
+ MarkupConfig markup_config.Config
+
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 {
+// ProviderProvider creates converter providers.
+type ProviderProvider interface {
New(cfg ProviderConfig) (Provider, error)
}
// Provider creates converters.
type Provider interface {
New(ctx DocumentContext) (Converter, error)
+ Name() string
+}
+
+// NewProvider creates a new Provider with the given name.
+func NewProvider(name string, create func(ctx DocumentContext) (Converter, error)) Provider {
+ return newConverter{
+ name: name,
+ create: create,
+ }
+}
+
+type newConverter struct {
+ name string
+ create func(ctx DocumentContext) (Converter, error)
}
-// NewConverter is an adapter that can be used as a ConverterProvider.
-type NewConverter func(ctx DocumentContext) (Converter, error)
+func (n newConverter) New(ctx DocumentContext) (Converter, error) {
+ return n.create(ctx)
+}
-// New creates a new Converter for the given ctx.
-func (n NewConverter) New(ctx DocumentContext) (Converter, error) {
- return n(ctx)
+func (n newConverter) Name() string {
+ return n.name
}
// Converter wraps the Convert method that converts some markup into
@@ -61,6 +79,11 @@ type DocumentInfo interface {
AnchorSuffix() string
}
+// TableOfContentsProvider provides the content as a ToC structure.
+type TableOfContentsProvider interface {
+ TableOfContents() tableofcontents.Root
+}
+
// Bytes holds a byte slice and implements the Result interface.
type Bytes []byte
diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go
new file mode 100644
index 00000000000..301b7a796ba
--- /dev/null
+++ b/markup/goldmark/convert.go
@@ -0,0 +1,184 @@
+// 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 goldmark converts Markdown to HTML using Goldmark.
+package goldmark
+
+import (
+ "bytes"
+
+ "github.com/alecthomas/chroma/styles"
+ "github.com/gohugoio/hugo/markup/converter"
+ "github.com/gohugoio/hugo/markup/highlight"
+ "github.com/gohugoio/hugo/markup/markup_config"
+ "github.com/gohugoio/hugo/markup/tableofcontents"
+ "github.com/yuin/goldmark"
+ hl "github.com/yuin/goldmark-highlighting"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+// Provider is the package entry point.
+var Provider converter.ProviderProvider = provide{}
+
+type provide struct {
+}
+
+func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
+ md := newMarkdown(cfg.MarkupConfig)
+ return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
+ return &goldmarkConverter{
+ ctx: ctx,
+ cfg: cfg,
+ md: md,
+ }, nil
+ }), nil
+}
+
+type goldmarkConverter struct {
+ md goldmark.Markdown
+ ctx converter.DocumentContext
+ cfg converter.ProviderConfig
+}
+
+func newMarkdown(mcfg markup_config.Config) goldmark.Markdown {
+ cfg := mcfg.Goldmark
+
+ var (
+ extensions = []goldmark.Extender{
+ newTocExtension(),
+ }
+ rendererOptions []renderer.Option
+ )
+
+ if cfg.Renderer.HardWraps {
+ rendererOptions = append(rendererOptions, html.WithHardWraps())
+ }
+
+ if cfg.Renderer.XHTML {
+ rendererOptions = append(rendererOptions, html.WithXHTML())
+ }
+
+ if cfg.Renderer.Unsafe {
+ rendererOptions = append(rendererOptions, html.WithUnsafe())
+ }
+
+ if mcfg.Highlight.CodeFences {
+ extensions = append(extensions, newHighlighting(mcfg.Highlight))
+ }
+
+ if cfg.Extensions.Table {
+ extensions = append(extensions, extension.Table)
+ }
+
+ if cfg.Extensions.Strikethrough {
+ extensions = append(extensions, extension.Strikethrough)
+ }
+
+ if cfg.Extensions.Linkify {
+ extensions = append(extensions, extension.Linkify)
+ }
+
+ if cfg.Extensions.TaskList {
+ extensions = append(extensions, extension.TaskList)
+ }
+
+ if cfg.Extensions.Typographer {
+ extensions = append(extensions, extension.Typographer)
+ }
+
+ if cfg.Extensions.DefinitionList {
+ extensions = append(extensions, extension.DefinitionList)
+ }
+
+ if cfg.Extensions.Footnote {
+ extensions = append(extensions, extension.Footnote)
+ }
+
+ md := goldmark.New(
+ goldmark.WithExtensions(
+ extensions...,
+ ),
+ goldmark.WithParserOptions(
+ parser.WithAttribute(),
+ parser.WithAutoHeadingID(),
+ ),
+ goldmark.WithRendererOptions(
+ rendererOptions...,
+ ),
+ )
+
+ return md
+
+}
+
+type converterResult struct {
+ converter.Result
+ toc tableofcontents.Root
+}
+
+func (c converterResult) TableOfContents() tableofcontents.Root {
+ return c.toc
+}
+
+func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
+ buf := &bytes.Buffer{}
+ pctx := parser.NewContext()
+ pctx.Set(tocEnableKey, ctx.RenderTOC)
+
+ reader := text.NewReader(ctx.Src)
+
+ doc := c.md.Parser().Parse(
+ reader,
+ parser.WithContext(pctx),
+ )
+
+ if err := c.md.Renderer().Render(buf, ctx.Src, doc); err != nil {
+ return nil, err
+ }
+
+ if toc, ok := pctx.Get(tocResultKey).(tableofcontents.Root); ok {
+ return converterResult{
+ Result: buf,
+ toc: toc,
+ }, nil
+ }
+
+ return buf, nil
+}
+
+func newHighlighting(cfg highlight.Config) goldmark.Extender {
+ style := styles.Get(cfg.Style)
+ if style == nil {
+ style = styles.Fallback
+ }
+
+ wrapper := highlight.GetWrapper(cfg)
+
+ e := hl.NewHighlighting(
+ hl.WithStyle(cfg.Style),
+ hl.WithFormatOptions(
+ cfg.ToHTMLOptions(true)...,
+ ),
+
+ hl.WithWrapperRenderer(func(w util.BufWriter, ctx hl.CodeBlockContext, entering bool) {
+ wrapper(w, ctx, entering)
+ }),
+ )
+
+ return e
+}
diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go
new file mode 100644
index 00000000000..8cd2f2d033b
--- /dev/null
+++ b/markup/goldmark/convert_test.go
@@ -0,0 +1,129 @@
+// 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 goldmark
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/gohugoio/hugo/markup/markup_config"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "github.com/gohugoio/hugo/markup/converter"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestTestConvert(t *testing.T) {
+ c := qt.New(t)
+
+ // Smoke test of the default configuration.
+ content := `
+## Code Fences
+
+§§§bash
+LINE1
+§§§
+
+## Code Fences No Lexer
+
+§§§moo
+LINE1
+§§§
+
+## Custom ID {#custom}
+
+## Auto ID
+
+* Autolink: https://gohugo.io/
+* Strikethrough:~~Hi~~ Hello, world!
+
+## Table
+
+| foo | bar |
+| --- | --- |
+| baz | bim |
+
+## Task Lists (default on)
+
+- [x] Finish my changes[^1]
+- [ ] Push my commits to GitHub
+- [ ] Open a pull request
+
+
+## Smartypants (default on)
+
+* Straight double "quotes" and single 'quotes' into “curly” quote HTML entities
+* Dashes (“--” and “---”) into en- and em-dash entities
+* Three consecutive dots (“...”) into an ellipsis entity
+
+## Footnotes
+
+That's some text with a footnote.[^1]
+
+## Definition Lists
+
+date
+: the datetime assigned to this page.
+
+description
+: the description for the content.
+
+
+[^1]: And that's the footnote.
+
+`
+
+ // Code fences
+ content = strings.Replace(content, "§§§", "```", -1)
+
+ mconf := markup_config.Default
+
+ p, err := Provider.New(
+ converter.ProviderConfig{
+ MarkupConfig: mconf,
+ 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(content)})
+ c.Assert(err, qt.IsNil)
+
+ got := string(b.Bytes())
+
+ // Header IDs
+ c.Assert(got, qt.Contains, `Custom ID
`, qt.Commentf(got))
+ c.Assert(got, qt.Contains, `Auto ID
`, qt.Commentf(got))
+
+ // Code fences
+ c.Assert(got, qt.Contains, "LINE1\n
")
+ c.Assert(got, qt.Contains, "Code Fences No Lexer\nLINE1\n
")
+
+ // Extensions
+ c.Assert(got, qt.Contains, `Autolink: https://gohugo.io/`)
+ c.Assert(got, qt.Contains, `Strikethrough:Hi Hello, world`)
+ c.Assert(got, qt.Contains, `foo `)
+ c.Assert(got, qt.Contains, `Push my commits to GitHub `)
+
+ c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`)
+ c.Assert(got, qt.Contains, `Dashes (“–” and “—”) `)
+ c.Assert(got, qt.Contains, `Three consecutive dots (“…”)`)
+ c.Assert(got, qt.Contains, `footnote.1`)
+ c.Assert(got, qt.Contains, ``)
+ c.Assert(got, qt.Contains, `date `)
+
+}
diff --git a/markup/goldmark/goldmark_config/config.go b/markup/goldmark/goldmark_config/config.go
new file mode 100644
index 00000000000..208ff060ceb
--- /dev/null
+++ b/markup/goldmark/goldmark_config/config.go
@@ -0,0 +1,60 @@
+// 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 goldmark_config holds Goldmark related configuration.
+package goldmark_config
+
+// DefaultConfig holds the default Goldmark configuration.
+var Default = Config{
+ Extensions: Extensions{
+ Typographer: true,
+ Footnote: true,
+ DefinitionList: true,
+ Table: true,
+ Strikethrough: true,
+ Linkify: true,
+ TaskList: true,
+ },
+ Renderer: Renderer{
+ Unsafe: false,
+ },
+}
+
+// Config configures Goldmark.
+type Config struct {
+ Renderer Renderer
+ Extensions Extensions
+}
+
+type Extensions struct {
+ Typographer bool
+ Footnote bool
+ DefinitionList bool
+
+ // GitHub flavored markdown
+ Table bool
+ Strikethrough bool
+ Linkify bool
+ TaskList bool
+}
+
+type Renderer struct {
+ // Whether softline breaks should be rendered as '
'
+ HardWraps bool
+
+ // XHTML instead of HTML5.
+ XHTML bool
+
+ // Allow raw HTML etc.
+ Unsafe bool
+}
diff --git a/markup/goldmark/toc.go b/markup/goldmark/toc.go
new file mode 100644
index 00000000000..6b13c300dbe
--- /dev/null
+++ b/markup/goldmark/toc.go
@@ -0,0 +1,102 @@
+// 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 goldmark
+
+import (
+ "bytes"
+
+ "github.com/gohugoio/hugo/markup/tableofcontents"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/ast"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/text"
+ "github.com/yuin/goldmark/util"
+)
+
+var (
+ tocResultKey = parser.NewContextKey()
+ tocEnableKey = parser.NewContextKey()
+)
+
+type tocTransformer struct {
+}
+
+func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parser.Context) {
+ if !pc.Get(tocEnableKey).(bool) {
+ return
+ }
+
+ var (
+ toc tableofcontents.Root
+ header tableofcontents.Header
+ level int
+ row = -1
+ inHeading bool
+ headingText bytes.Buffer
+ )
+
+ ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
+ s := ast.WalkStatus(ast.WalkContinue)
+ if n.Kind() == ast.KindHeading {
+ if inHeading && !entering {
+ header.Text = headingText.String()
+ headingText.Reset()
+ toc.AddAt(header, row, level-1)
+ header = tableofcontents.Header{}
+ inHeading = false
+ return s, nil
+ }
+
+ inHeading = true
+ }
+
+ if !(inHeading && entering) {
+ return s, nil
+ }
+
+ switch n.Kind() {
+ case ast.KindHeading:
+ heading := n.(*ast.Heading)
+ level = heading.Level
+
+ if level == 1 || row == -1 {
+ row++
+ }
+
+ id, found := heading.AttributeString("id")
+ if found {
+ header.ID = string(id.([]byte))
+ }
+ case ast.KindText:
+ textNode := n.(*ast.Text)
+ headingText.Write(textNode.Text(reader.Source()))
+ }
+
+ return s, nil
+ })
+
+ pc.Set(tocResultKey, toc)
+}
+
+type tocExtension struct {
+}
+
+func newTocExtension() goldmark.Extender {
+ return &tocExtension{}
+}
+
+func (e *tocExtension) Extend(m goldmark.Markdown) {
+ m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&tocTransformer{}, 10)))
+}
diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/toc_test.go
new file mode 100644
index 00000000000..1a3b2fd8aa9
--- /dev/null
+++ b/markup/goldmark/toc_test.go
@@ -0,0 +1,72 @@
+// 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 goldmark converts Markdown to HTML using Goldmark.
+package goldmark
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/common/loggers"
+
+ "github.com/gohugoio/hugo/markup/converter"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestToc(t *testing.T) {
+ c := qt.New(t)
+
+ content := `
+# Header 1
+
+## First h2
+
+Some text.
+
+### H3
+
+Some more text.
+
+## Second h2
+
+And then some.
+
+### Second H3
+
+#### First H4
+
+`
+
+ 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(content), RenderTOC: true})
+ c.Assert(err, qt.IsNil)
+ got := b.(converter.TableOfContentsProvider).TableOfContents().ToHTML(2, 3)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+}
diff --git a/markup/highlight/config.go b/markup/highlight/config.go
new file mode 100644
index 00000000000..9110f432e0e
--- /dev/null
+++ b/markup/highlight/config.go
@@ -0,0 +1,189 @@
+// 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 highlight provides code highlighting.
+package highlight
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/alecthomas/chroma/formatters/html"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+var DefaultConfig = Config{
+ // The highlighter style to use.
+ // See https://xyproto.github.io/splash/docs/all.html
+ Style: "monokai",
+ LineNoStart: 1,
+ CodeFences: true,
+ NoClasses: false,
+}
+
+//
+type Config struct {
+ Style string
+
+ CodeFences bool
+
+ // Use inline CSS styles.
+ NoClasses bool
+
+ // When set, line numbers will be printed.
+ // Values:'inline' or 'table'.
+ LineNos string
+
+ // Start the line numbers from this value (default is 1).
+ LineNoStart int
+
+ // A space separated list of line numbers, e.g. “3-8 10-20”.
+ Hl_Lines string
+}
+
+func (cfg Config) ToHTMLOptions(preventSurroundingPre bool) []html.Option {
+ var options []html.Option
+
+ if preventSurroundingPre {
+ options = append(options, html.PreventSurroundingPre())
+ }
+
+ if cfg.LineNos != "" {
+ options = append(options,
+ html.WithLineNumbers(),
+ html.BaseLineNumber(cfg.LineNoStart),
+ )
+
+ if cfg.LineNos != "inline" {
+ options = append(options, html.LineNumbersInTable())
+ }
+ }
+
+ if cfg.Hl_Lines != "" {
+ ranges, err := hlLinesToRanges(cfg.LineNoStart, cfg.Hl_Lines)
+ if err == nil {
+ options = append(options, html.HighlightLines(ranges))
+ }
+ }
+
+ if !cfg.NoClasses {
+ options = append(options, html.WithClasses())
+ }
+
+ return options
+}
+
+func applyOptionsFromString(opts string, cfg *Config) error {
+ optsm, err := parseOptions(opts)
+ if err != nil {
+ return err
+ }
+ return mapstructure.WeakDecode(optsm, cfg)
+}
+
+// ApplyLegacyConfig applies legacy config from back when we had
+// Pygments.
+func ApplyLegacyConfig(cfg config.Provider, conf *Config) error {
+ if conf.Style == DefaultConfig.Style {
+ if s := cfg.GetString("pygmentsStyle"); s != "" {
+ conf.Style = s
+ }
+ }
+
+ if conf.NoClasses == DefaultConfig.NoClasses && cfg.IsSet("pygmentsUseClasses") {
+ conf.NoClasses = !cfg.GetBool("pygmentsUseClasses")
+ }
+
+ if conf.CodeFences == DefaultConfig.CodeFences && cfg.IsSet("pygmentsCodeFences") {
+ conf.CodeFences = cfg.GetBool("pygmentsCodeFences")
+ }
+
+ if cfg.IsSet("pygmentsOptions") {
+ if err := applyOptionsFromString(cfg.GetString("pygmentsOptions"), conf); err != nil {
+ return err
+ }
+ }
+
+ return nil
+
+}
+
+func parseOptions(in string) (map[string]interface{}, error) {
+ in = strings.Trim(in, " ")
+ opts := make(map[string]interface{})
+
+ if in == "" {
+ return opts, nil
+ }
+
+ for _, v := range strings.Split(in, ",") {
+ keyVal := strings.Split(v, "=")
+ key := strings.ToLower(strings.Trim(keyVal[0], " "))
+ if len(keyVal) != 2 {
+ return opts, fmt.Errorf("invalid Highlight option: %s", key)
+ }
+ opts[key] = keyVal[1]
+ }
+
+ return opts, nil
+}
+
+// startLine compansates for https://github.com/alecthomas/chroma/issues/30
+func hlLinesToRanges(startLine int, s string) ([][2]int, error) {
+ var ranges [][2]int
+ s = strings.TrimSpace(s)
+
+ if s == "" {
+ return ranges, nil
+ }
+
+ // Variants:
+ // 1 2 3 4
+ // 1-2 3-4
+ // 1-2 3
+ // 1 3-4
+ // 1 3-4
+ fields := strings.Split(s, " ")
+ for _, field := range fields {
+ field = strings.TrimSpace(field)
+ if field == "" {
+ continue
+ }
+ numbers := strings.Split(field, "-")
+ var r [2]int
+ first, err := strconv.Atoi(numbers[0])
+ if err != nil {
+ return ranges, err
+ }
+ first = first + startLine - 1
+ r[0] = first
+ if len(numbers) > 1 {
+ second, err := strconv.Atoi(numbers[1])
+ if err != nil {
+ return ranges, err
+ }
+ second = second + startLine - 1
+ r[1] = second
+ } else {
+ r[1] = first
+ }
+
+ ranges = append(ranges, r)
+ }
+ return ranges, nil
+
+}
diff --git a/markup/highlight/config_test.go b/markup/highlight/config_test.go
new file mode 100644
index 00000000000..8d1daab182c
--- /dev/null
+++ b/markup/highlight/config_test.go
@@ -0,0 +1,57 @@
+// 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 highlight provides code highlighting.
+package highlight
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestConfig(t *testing.T) {
+ c := qt.New(t)
+
+ c.Run("applyLegacyConfig", func(c *qt.C) {
+ v := viper.New()
+ v.Set("pygmentsStyle", "hugo")
+ v.Set("pygmentsUseClasses", false)
+ v.Set("pygmentsCodeFences", false)
+ v.Set("pygmentsOptions", "linenos=table")
+
+ cfg := DefaultConfig
+ err := ApplyLegacyConfig(v, &cfg)
+ c.Assert(err, qt.IsNil)
+ c.Assert(cfg.Style, qt.Equals, "hugo")
+ c.Assert(cfg.NoClasses, qt.Equals, true)
+ c.Assert(cfg.CodeFences, qt.Equals, false)
+ c.Assert(cfg.LineNos, qt.Equals, "table")
+
+ })
+
+ c.Run("parseOptions", func(c *qt.C) {
+ cfg := DefaultConfig
+ opts := "noclasses=true,linenos=table,linenostart=32,hl_lines=3-8 10-20"
+ err := applyOptionsFromString(opts, &cfg)
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(cfg.NoClasses, qt.Equals, true)
+ c.Assert(cfg.LineNos, qt.Equals, "table")
+ c.Assert(cfg.LineNoStart, qt.Equals, 32)
+ c.Assert(cfg.Hl_Lines, qt.Equals, "3-8 10-20")
+
+ })
+}
diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go
new file mode 100644
index 00000000000..44576179a88
--- /dev/null
+++ b/markup/highlight/highlight.go
@@ -0,0 +1,172 @@
+// 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 highlight
+
+import (
+ "strings"
+ "sync"
+
+ "github.com/alecthomas/chroma"
+ "github.com/alecthomas/chroma/formatters/html"
+ "github.com/alecthomas/chroma/lexers"
+ "github.com/alecthomas/chroma/styles"
+ "github.com/gohugoio/hugo/common/hugio"
+
+ hl "github.com/yuin/goldmark-highlighting"
+)
+
+func New(cfg Config) Highlighter {
+ return Highlighter{
+ cfg: cfg,
+ }
+}
+
+type Highlighter struct {
+ cfg Config
+}
+
+func (h Highlighter) Highlight(code, lang, optsStr string) (string, error) {
+ cfg := h.cfg
+ if optsStr != "" {
+ applyOptionsFromString(optsStr, &cfg)
+ }
+ return highlight(code, lang, cfg, GetWrapper(cfg))
+}
+
+func highlight(code, lang string, cfg Config, wrap func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool)) (string, error) {
+ w := &strings.Builder{}
+ var lexer chroma.Lexer
+ if lang != "" {
+ lexer = lexers.Get(lang)
+ }
+
+ ctx := codeBlockContext{
+ lang: lang,
+ highlighted: lexer != nil,
+ }
+
+ if !ctx.Highlighted() {
+ if wrap != nil {
+ wrap(w, ctx, true)
+ }
+ w.WriteString(code)
+ if wrap != nil {
+ wrap(w, ctx, false)
+ }
+ return w.String(), nil
+ }
+
+ style := styles.Get(cfg.Style)
+ if style == nil {
+ style = styles.Fallback
+ }
+
+ iterator, err := lexer.Tokenise(nil, code)
+ if err != nil {
+ return "", err
+ }
+
+ formatter := html.New(cfg.ToHTMLOptions(wrap != nil)...)
+
+ if wrap != nil {
+ wrap(w, ctx, true)
+ }
+ if err := formatter.Format(w, style, iterator); err != nil {
+ return "", err
+ }
+ if wrap != nil {
+ wrap(w, ctx, false)
+ }
+ return w.String(), nil
+}
+
+type codeBlockContext struct {
+ lang string
+ highlighted bool
+}
+
+func (c codeBlockContext) Language() ([]byte, bool) {
+ return []byte(c.lang), c.lang != ""
+}
+
+func (c codeBlockContext) Highlighted() bool {
+ return c.highlighted
+}
+
+func (c codeBlockContext) Attributes() hl.ImmutableAttributes {
+ return nil
+}
+
+// TODO(bep) improve this somewhere upstream.
+func GetWrapper(cfg Config) func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool) {
+ return wrapperCache.getWrapper(cfg)
+}
+
+var wrapperCache = wrappers{
+ m: make(map[Config]func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool)),
+}
+
+type wrappers struct {
+ m map[Config]func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool)
+ sync.RWMutex
+}
+
+func (w wrappers) getWrapper(cfg Config) func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool) {
+ w.RLock()
+ wrapper, found := w.m[cfg]
+ w.RUnlock()
+
+ if found {
+ return wrapper
+ }
+
+ w.Lock()
+ defer w.Unlock()
+
+ s, _ := highlight("", "bash", cfg, nil)
+ pre := s[:strings.Index(s, ">")+1]
+ wrapper = func(w hugio.StringWriter, ctx hl.CodeBlockContext, entering bool) {
+ language, hasLang := ctx.Language()
+ if entering {
+ if !hasLang {
+ w.WriteString("")
+ return
+ }
+ if ctx.Highlighted() {
+ w.WriteString(``)
+ w.WriteString(pre)
+ } else {
+ w.WriteString("")
+ }
+ w.WriteString(``)
+ } else {
+ if !hasLang {
+ w.WriteString("
")
+ return
+ }
+ w.WriteString(``)
+ if ctx.Highlighted() {
+ w.WriteString(``)
+ }
+ }
+ }
+
+ w.m[cfg] = wrapper
+
+ return wrapper
+}
diff --git a/markup/highlight/highlight_test.go b/markup/highlight/highlight_test.go
new file mode 100644
index 00000000000..b880b38a864
--- /dev/null
+++ b/markup/highlight/highlight_test.go
@@ -0,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 highlight provides code highlighting.
+package highlight
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestHighlight(t *testing.T) {
+ c := qt.New(t)
+ h := New(DefaultConfig)
+
+ result, _ := h.Highlight(`echo "Hugo Rocks!"`, "bash", "")
+ c.Assert(result, qt.Equals, `echo "Hugo Rocks!"
`)
+ result, _ = h.Highlight(`echo "Hugo Rocks!"`, "unknown", "")
+ c.Assert(result, qt.Equals, `echo "Hugo Rocks!"
`)
+
+ result, _ = h.Highlight(`LINE1
+LINE2
+LINE3
+LINE4
+LINE5
+`, "bash", "linenos=table,hl_lines=2 4-5,linenostart=3")
+ c.Assert(result, qt.Contains, "\n3\n")
+ c.Assert(result, qt.Contains, "4")
+
+}
diff --git a/markup/internal/blackfriday.go b/markup/internal/blackfriday.go
deleted file mode 100644
index 373df0c5013..00000000000
--- a/markup/internal/blackfriday.go
+++ /dev/null
@@ -1,108 +0,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 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
-}
diff --git a/markup/markup.go b/markup/markup.go
index 54193aba36d..58438883e64 100644
--- a/markup/markup.go
+++ b/markup/markup.go
@@ -16,6 +16,12 @@ package markup
import (
"strings"
+ "github.com/gohugoio/hugo/markup/highlight"
+
+ "github.com/gohugoio/hugo/markup/markup_config"
+
+ "github.com/gohugoio/hugo/markup/goldmark"
+
"github.com/gohugoio/hugo/markup/org"
"github.com/gohugoio/hugo/markup/asciidoc"
@@ -29,39 +35,70 @@ import (
func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, error) {
converters := make(map[string]converter.Provider)
- add := func(p converter.NewProvider, aliases ...string) error {
+ markupConfig, err := markup_config.Decode(cfg.Cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg.Highlight == nil {
+ h := highlight.New(markupConfig.Highlight)
+ cfg.Highlight = func(code, lang, optsStr string) (string, error) {
+ return h.Highlight(code, lang, optsStr)
+ }
+ }
+
+ cfg.MarkupConfig = markupConfig
+
+ add := func(p converter.ProviderProvider, aliases ...string) error {
c, err := p.New(cfg)
if err != nil {
return err
}
+
+ name := c.Name()
+
+ aliases = append(aliases, name)
+
+ if strings.EqualFold(name, cfg.MarkupConfig.DefaultMarkdownHandler) {
+ aliases = append(aliases, "markdown")
+ }
+
addConverter(converters, c, aliases...)
return nil
}
- if err := add(blackfriday.Provider, "md", "markdown", "blackfriday"); err != nil {
+ if err := add(goldmark.Provider); err != nil {
+ return nil, err
+ }
+ if err := add(blackfriday.Provider); err != nil {
return nil, err
}
- if err := add(mmark.Provider, "mmark"); err != nil {
+ if err := add(mmark.Provider); err != nil {
return nil, err
}
- if err := add(asciidoc.Provider, "asciidoc"); err != nil {
+ if err := add(asciidoc.Provider, "ad", "adoc"); err != nil {
return nil, err
}
- if err := add(rst.Provider, "rst"); err != nil {
+ if err := add(rst.Provider); err != nil {
return nil, err
}
- if err := add(pandoc.Provider, "pandoc"); err != nil {
+ if err := add(pandoc.Provider, "pdc"); err != nil {
return nil, err
}
- if err := add(org.Provider, "org"); err != nil {
+ if err := add(org.Provider); err != nil {
return nil, err
}
- return &converterRegistry{converters: converters}, nil
+ return &converterRegistry{
+ config: cfg,
+ converters: converters,
+ }, nil
}
type ConverterProvider interface {
Get(name string) converter.Provider
+ GetMarkupConfig() markup_config.Config
+ Highlight(code, lang, optsStr string) (string, error)
}
type converterRegistry struct {
@@ -70,12 +107,22 @@ type converterRegistry struct {
// may be registered multiple times.
// All names are lower case.
converters map[string]converter.Provider
+
+ config converter.ProviderConfig
}
func (r *converterRegistry) Get(name string) converter.Provider {
return r.converters[strings.ToLower(name)]
}
+func (r *converterRegistry) Highlight(code, lang, optsStr string) (string, error) {
+ return r.config.Highlight(code, lang, optsStr)
+}
+
+func (r *converterRegistry) GetMarkupConfig() markup_config.Config {
+ return r.config.MarkupConfig
+}
+
func addConverter(m map[string]converter.Provider, c converter.Provider, aliases ...string) {
for _, alias := range aliases {
m[alias] = c
diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go
new file mode 100644
index 00000000000..529553cb5a2
--- /dev/null
+++ b/markup/markup_config/config.go
@@ -0,0 +1,105 @@
+// 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_config
+
+import (
+ "github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/docshelper"
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
+ "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
+ "github.com/gohugoio/hugo/markup/highlight"
+ "github.com/gohugoio/hugo/markup/tableofcontents"
+ "github.com/gohugoio/hugo/parser"
+ "github.com/mitchellh/mapstructure"
+)
+
+type Config struct {
+ // Default markdown handler for md/markdown extensions.
+ // Default is "goldmark".
+ // Before Hugo 0.60 this was "blackfriday".
+ DefaultMarkdownHandler string
+
+ Highlight highlight.Config
+ TableOfContents tableofcontents.Config
+
+ // Content renderers
+ Goldmark goldmark_config.Config
+ BlackFriday blackfriday_config.Config
+}
+
+func Decode(cfg config.Provider) (conf Config, err error) {
+ conf = Default
+
+ m := cfg.GetStringMap("markup")
+ if m == nil {
+ return
+ }
+
+ err = mapstructure.WeakDecode(m, &conf)
+ if err != nil {
+ return
+ }
+
+ if err = applyLegacyConfig(cfg, &conf); err != nil {
+ return
+ }
+
+ if err = highlight.ApplyLegacyConfig(cfg, &conf.Highlight); err != nil {
+ return
+ }
+
+ return
+}
+
+func applyLegacyConfig(cfg config.Provider, conf *Config) error {
+ if bm := cfg.GetStringMap("blackfriday"); bm != nil {
+ // Legacy top level blackfriday config.
+ err := mapstructure.WeakDecode(bm, &conf.BlackFriday)
+ if err != nil {
+ return err
+ }
+ }
+
+ if conf.BlackFriday.FootnoteAnchorPrefix == "" {
+ conf.BlackFriday.FootnoteAnchorPrefix = cfg.GetString("footnoteAnchorPrefix")
+ }
+
+ if conf.BlackFriday.FootnoteReturnLinkContents == "" {
+ conf.BlackFriday.FootnoteReturnLinkContents = cfg.GetString("footnoteReturnLinkContents")
+ }
+
+ return nil
+
+}
+
+var Default = Config{
+ DefaultMarkdownHandler: "goldmark",
+
+ TableOfContents: tableofcontents.DefaultConfig,
+ Highlight: highlight.DefaultConfig,
+
+ Goldmark: goldmark_config.Default,
+ BlackFriday: blackfriday_config.Default,
+}
+
+func init() {
+ docsProvider := func() map[string]interface{} {
+ docs := make(map[string]interface{})
+ docs["markup"] = parser.LowerCaseCamelJSONMarshaller{Value: Default}
+ return docs
+
+ }
+ // TODO(bep) merge maps
+ docshelper.AddDocProvider("config", docsProvider)
+}
diff --git a/markup/markup_config/config_test.go b/markup/markup_config/config_test.go
new file mode 100644
index 00000000000..0e144bc19c0
--- /dev/null
+++ b/markup/markup_config/config_test.go
@@ -0,0 +1,67 @@
+// 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_config
+
+import (
+ "testing"
+
+ "github.com/spf13/viper"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestConfig(t *testing.T) {
+ c := qt.New(t)
+
+ c.Run("Decode", func(c *qt.C) {
+ c.Parallel()
+ v := viper.New()
+
+ v.Set("markup", map[string]interface{}{
+ "goldmark": map[string]interface{}{
+ "unsafe": true,
+ },
+ })
+
+ conf, err := Decode(v)
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(conf.Goldmark.Unsafe, qt.Equals, true)
+ c.Assert(conf.BlackFriday.Fractions, qt.Equals, true)
+
+ })
+
+ c.Run("legacy", func(c *qt.C) {
+ c.Parallel()
+ v := viper.New()
+
+ v.Set("blackfriday", map[string]interface{}{
+ "angledQuotes": true,
+ })
+
+ v.Set("footnoteAnchorPrefix", "myprefix")
+ v.Set("footnoteReturnLinkContents", "myreturn")
+ v.Set("pygmentsStyle", "hugo")
+
+ conf, err := Decode(v)
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(conf.BlackFriday.AngledQuotes, qt.Equals, true)
+ c.Assert(conf.BlackFriday.FootnoteAnchorPrefix, qt.Equals, "myprefix")
+ c.Assert(conf.BlackFriday.FootnoteReturnLinkContents, qt.Equals, "myreturn")
+ c.Assert(conf.Highlight.Style, qt.Equals, "hugo")
+ c.Assert(conf.Highlight.CodeFences, qt.Equals, true)
+ })
+
+}
diff --git a/markup/markup_test.go b/markup/markup_test.go
index c4c1ee03204..669c0a446b6 100644
--- a/markup/markup_test.go
+++ b/markup/markup_test.go
@@ -29,13 +29,23 @@ func TestConverterRegistry(t *testing.T) {
r, err := NewConverterProvider(converter.ProviderConfig{Cfg: viper.New()})
c.Assert(err, qt.IsNil)
+ c.Assert("goldmark", qt.Equals, r.GetMarkupConfig().DefaultMarkdownHandler)
+
+ checkName := func(name string) {
+ p := r.Get(name)
+ c.Assert(p, qt.Not(qt.IsNil))
+ c.Assert(p.Name(), qt.Equals, name)
+ }
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))
+ c.Assert(r.Get("markdown").Name(), qt.Equals, "goldmark")
+
+ checkName("goldmark")
+ checkName("mmark")
+ checkName("asciidoc")
+ checkName("rst")
+ checkName("pandoc")
+ checkName("org")
+ checkName("blackfriday")
}
diff --git a/markup/mmark/convert.go b/markup/mmark/convert.go
index a0da346c141..2249c572eff 100644
--- a/markup/mmark/convert.go
+++ b/markup/mmark/convert.go
@@ -15,33 +15,28 @@
package mmark
import (
- "github.com/gohugoio/hugo/markup/internal"
-
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"github.com/gohugoio/hugo/markup/converter"
"github.com/miekg/mmark"
)
// Provider is the package entry point.
-var Provider converter.NewProvider = provider{}
+var Provider converter.ProviderProvider = 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
- }
-
+ defaultBlackFriday := cfg.MarkupConfig.BlackFriday
defaultExtensions := getMmarkExtensions(defaultBlackFriday)
- var n converter.NewConverter = func(ctx converter.DocumentContext) (converter.Converter, error) {
+ return converter.NewProvider("mmark", 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)
+ b, err = blackfriday_config.UpdateConfig(b, ctx.ConfigOverrides)
if err != nil {
return nil, err
}
@@ -54,16 +49,14 @@ func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error)
extensions: extensions,
cfg: cfg,
}, nil
- }
-
- return n, nil
+ }), nil
}
type mmarkConverter struct {
ctx converter.DocumentContext
extensions int
- b *internal.BlackFriday
+ b blackfriday_config.Config
cfg converter.ProviderConfig
}
@@ -74,7 +67,7 @@ func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result,
func getHTMLRenderer(
ctx converter.DocumentContext,
- cfg *internal.BlackFriday,
+ cfg blackfriday_config.Config,
pcfg converter.ProviderConfig) mmark.Renderer {
var (
@@ -97,15 +90,14 @@ func getHTMLRenderer(
htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS
return &mmarkRenderer{
- Config: cfg,
- Cfg: pcfg.Cfg,
- highlight: pcfg.Highlight,
- Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
+ BlackfridayConfig: cfg,
+ Config: pcfg,
+ Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters),
}
}
-func getMmarkExtensions(cfg *internal.BlackFriday) int {
+func getMmarkExtensions(cfg blackfriday_config.Config) int {
flags := 0
flags |= mmark.EXTENSION_TABLES
flags |= mmark.EXTENSION_FENCED_CODE
diff --git a/markup/mmark/convert_test.go b/markup/mmark/convert_test.go
index d015ee94ccb..3945f80dacc 100644
--- a/markup/mmark/convert_test.go
+++ b/markup/mmark/convert_test.go
@@ -20,19 +20,14 @@ import (
"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"
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
+ "github.com/gohugoio/hugo/markup/converter"
+ "github.com/miekg/mmark"
)
func TestGetMmarkExtensions(t *testing.T) {
- c := qt.New(t)
- b, err := internal.NewBlackfriday(converter.ProviderConfig{Cfg: viper.New()})
- c.Assert(err, qt.IsNil)
+ b := blackfriday_config.Default
//TODO: This is doing the same just with different marks...
type data struct {
diff --git a/markup/mmark/renderer.go b/markup/mmark/renderer.go
index 07fe71c956b..6cb7f105e4d 100644
--- a/markup/mmark/renderer.go
+++ b/markup/mmark/renderer.go
@@ -17,26 +17,24 @@ import (
"bytes"
"strings"
+ "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
+ "github.com/gohugoio/hugo/markup/converter"
"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)
+ Config converter.ProviderConfig
+ BlackfridayConfig blackfriday_config.Config
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")) {
+ if r.Config.MarkupConfig.Highlight.CodeFences {
str := strings.Trim(string(text), "\n\r")
- highlighted, _ := r.highlight(str, lang, "")
+ highlighted, _ := r.Config.Highlight(str, lang, "")
out.WriteString(highlighted)
} else {
r.Renderer.BlockCode(out, text, lang, caption, subfigure, callouts)
diff --git a/markup/org/convert.go b/markup/org/convert.go
index a951e6fe15e..4d6e5e2fa0f 100644
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -23,19 +23,18 @@ import (
)
// Provider is the package entry point.
-var Provider converter.NewProvider = provide{}
+var Provider converter.ProviderProvider = 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 converter.NewProvider("org", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &orgConverter{
ctx: ctx,
cfg: cfg,
}, nil
- }
- return n, nil
+ }), nil
}
type orgConverter struct {
diff --git a/markup/pandoc/convert.go b/markup/pandoc/convert.go
index 4deab0b4665..d538d4a5265 100644
--- a/markup/pandoc/convert.go
+++ b/markup/pandoc/convert.go
@@ -23,19 +23,18 @@ import (
)
// Provider is the package entry point.
-var Provider converter.NewProvider = provider{}
+var Provider converter.ProviderProvider = 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 converter.NewProvider("pandoc", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &pandocConverter{
ctx: ctx,
cfg: cfg,
}, nil
- }
- return n, nil
+ }), nil
}
diff --git a/markup/rst/convert.go b/markup/rst/convert.go
index e12e34f6db0..040b40d792d 100644
--- a/markup/rst/convert.go
+++ b/markup/rst/convert.go
@@ -25,20 +25,18 @@ import (
)
// Provider is the package entry point.
-var Provider converter.NewProvider = provider{}
+var Provider converter.ProviderProvider = 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 converter.NewProvider("rst", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &rstConverter{
ctx: ctx,
cfg: cfg,
}, nil
- }
- return n, nil
-
+ }), nil
}
type rstConverter struct {
diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go
new file mode 100644
index 00000000000..6cd84e5ae91
--- /dev/null
+++ b/markup/tableofcontents/tableofcontents.go
@@ -0,0 +1,148 @@
+// 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 tableofcontents
+
+import (
+ "strings"
+)
+
+type Headers []Header
+
+type Header struct {
+ ID string
+ Text string
+
+ Headers Headers
+}
+
+func (h Header) IsZero() bool {
+ return h.ID == "" && h.Text == ""
+}
+
+type Root struct {
+ Headers Headers
+}
+
+func (toc *Root) AddAt(h Header, y, x int) {
+ for i := len(toc.Headers); i <= y; i++ {
+ toc.Headers = append(toc.Headers, Header{})
+ }
+
+ if x == 0 {
+ toc.Headers[y] = h
+ return
+ }
+
+ header := &toc.Headers[y]
+
+ for i := 1; i < x; i++ {
+ if len(header.Headers) == 0 {
+ header.Headers = append(header.Headers, Header{})
+ }
+ header = &header.Headers[len(header.Headers)-1]
+ }
+ header.Headers = append(header.Headers, h)
+}
+
+func (toc Root) ToHTML(startLevel, stopLevel int) string {
+ b := &tocBuilder{
+ s: strings.Builder{},
+ h: toc.Headers,
+ startLevel: startLevel,
+ stopLevel: stopLevel,
+ }
+ b.Build()
+ return b.s.String()
+}
+
+type tocBuilder struct {
+ s strings.Builder
+ h Headers
+
+ startLevel int
+ stopLevel int
+}
+
+func (b *tocBuilder) Build() {
+ b.buildHeaders2(b.h)
+}
+
+func (b *tocBuilder) buildHeaders2(h Headers) {
+ b.s.WriteString("")
+}
+
+func (b *tocBuilder) buildHeaders(level, indent int, h Headers) {
+ if level < b.startLevel {
+ for _, h := range h {
+ b.buildHeaders(level+1, indent, h.Headers)
+ }
+ return
+ }
+
+ if b.stopLevel != -1 && level > b.stopLevel {
+ return
+ }
+
+ hasChildren := len(h) > 0
+
+ if hasChildren {
+ b.s.WriteString("\n")
+ b.indent(indent + 1)
+ b.s.WriteString("\n")
+ }
+
+ for _, h := range h {
+ b.buildHeader(level+1, indent+2, h)
+ }
+
+ if hasChildren {
+ b.indent(indent + 1)
+ b.s.WriteString("
")
+ b.s.WriteString("\n")
+ b.indent(indent)
+ }
+
+}
+func (b *tocBuilder) buildHeader(level, indent int, h Header) {
+ b.indent(indent)
+ b.s.WriteString("")
+ if !h.IsZero() {
+ b.s.WriteString("" + h.Text + "")
+ }
+ b.buildHeaders(level, indent, h.Headers)
+ b.s.WriteString(" \n")
+}
+
+func (b *tocBuilder) indent(n int) {
+ for i := 0; i < n; i++ {
+ b.s.WriteString(" ")
+ }
+}
+
+var DefaultConfig = Config{
+ StartLevel: 2,
+ EndLevel: 3,
+}
+
+type Config struct {
+ // Heading start level to include in the table of contents, starting
+ // at h1 (inclusive).
+ StartLevel int
+
+ // Heading end level, inclusive, to include in the table of contents.
+ // Default is 3, a value of -1 will include everything.
+ EndLevel int
+}
diff --git a/markup/tableofcontents/tableofcontents_test.go b/markup/tableofcontents/tableofcontents_test.go
new file mode 100644
index 00000000000..1ea96c82f91
--- /dev/null
+++ b/markup/tableofcontents/tableofcontents_test.go
@@ -0,0 +1,119 @@
+// 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 tableofcontents
+
+import (
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestToc(t *testing.T) {
+ c := qt.New(t)
+
+ toc := &Root{}
+
+ toc.AddAt(Header{Text: "Header 1", ID: "h1-1"}, 0, 0)
+ toc.AddAt(Header{Text: "1-H2-1", ID: "1-h2-1"}, 0, 1)
+ toc.AddAt(Header{Text: "1-H2-2", ID: "1-h2-2"}, 0, 1)
+ toc.AddAt(Header{Text: "1-H3-1", ID: "1-h2-2"}, 0, 2)
+ toc.AddAt(Header{Text: "Header 2", ID: "h1-2"}, 1, 0)
+
+ got := toc.ToHTML(1, -1)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+ got = toc.ToHTML(1, 1)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+ got = toc.ToHTML(1, 2)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+ got = toc.ToHTML(2, 2)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+}
+
+func TestTocMissingParent(t *testing.T) {
+ c := qt.New(t)
+
+ toc := &Root{}
+
+ toc.AddAt(Header{Text: "H2", ID: "h2"}, 0, 1)
+ toc.AddAt(Header{Text: "H3", ID: "h3"}, 1, 2)
+ toc.AddAt(Header{Text: "H3", ID: "h3"}, 1, 2)
+
+ got := toc.ToHTML(1, -1)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+ got = toc.ToHTML(3, 3)
+ c.Assert(got, qt.Equals, ``, qt.Commentf(got))
+
+}
diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go
new file mode 100644
index 00000000000..e7aeb2abfa7
--- /dev/null
+++ b/parser/lowercase_camel_json.go
@@ -0,0 +1,51 @@
+// 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 parser
+
+import (
+ "encoding/json"
+ "regexp"
+ "unicode"
+ "unicode/utf8"
+)
+
+// Regexp definitions
+var keyMatchRegex = regexp.MustCompile(`\"(\w+)\":`)
+var wordBarrierRegex = regexp.MustCompile(`(\w)([A-Z])`)
+
+// Code adapted from https://gist.github.com/piersy/b9934790a8892db1a603820c0c23e4a7
+type LowerCaseCamelJSONMarshaller struct {
+ Value interface{}
+}
+
+func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) {
+ marshalled, err := json.Marshal(c.Value)
+
+ converted := keyMatchRegex.ReplaceAllFunc(
+ marshalled,
+ func(match []byte) []byte {
+ // Empty keys are valid JSON, only lowercase if we do not have an
+ // empty key.
+ if len(match) > 2 {
+ // Decode first rune after the double quotes
+ r, width := utf8.DecodeRune(match[1:])
+ r = unicode.ToLower(r)
+ utf8.EncodeRune(match[1:width+1], r)
+ }
+ return match
+ },
+ )
+
+ return converted, err
+}
diff --git a/tpl/transform/remarshal.go b/tpl/transform/remarshal.go
index 182bd21d619..d9b6829a0ca 100644
--- a/tpl/transform/remarshal.go
+++ b/tpl/transform/remarshal.go
@@ -18,33 +18,42 @@ import (
// change without notice if it serves a purpose in the docs.
// Format is one of json, yaml or toml.
func (ns *Namespace) Remarshal(format string, data interface{}) (string, error) {
- from, err := cast.ToStringE(data)
- if err != nil {
- return "", err
- }
+ var meta map[string]interface{}
- from = strings.TrimSpace(from)
format = strings.TrimSpace(strings.ToLower(format))
- if from == "" {
- return "", nil
- }
-
mark, err := toFormatMark(format)
if err != nil {
return "", err
}
- fromFormat := metadecoders.Default.FormatFromContentString(from)
- if fromFormat == "" {
- return "", errors.New("failed to detect format from content")
- }
+ if m, ok := data.(map[string]interface{}); ok {
+ meta = m
+ } else {
+ from, err := cast.ToStringE(data)
+ if err != nil {
+ return "", err
+ }
- meta, err := metadecoders.Default.UnmarshalToMap([]byte(from), fromFormat)
- if err != nil {
- return "", err
+ from = strings.TrimSpace(from)
+ if from == "" {
+ return "", nil
+ }
+
+ fromFormat := metadecoders.Default.FormatFromContentString(from)
+ if fromFormat == "" {
+ return "", errors.New("failed to detect format from content")
+ }
+
+ meta, err = metadecoders.Default.UnmarshalToMap([]byte(from), fromFormat)
+ if err != nil {
+ return "", err
+ }
}
+ // Make it so 1.0 float64 prints as 1 etc.
+ applyMarshalTypes(meta)
+
var result bytes.Buffer
if err := parser.InterfaceToConfig(meta, mark, &result); err != nil {
return "", err
@@ -53,6 +62,23 @@ func (ns *Namespace) Remarshal(format string, data interface{}) (string, error)
return result.String(), nil
}
+// The unmarshal/marshal dance is extremely type lossy, and we need
+// to make sure that integer types prints as "43" and not "43.0" in
+// all formats, hence this hack.
+func applyMarshalTypes(m map[string]interface{}) {
+ for k, v := range m {
+ switch t := v.(type) {
+ case map[string]interface{}:
+ applyMarshalTypes(t)
+ case float64:
+ i := int64(t)
+ if t == float64(i) {
+ m[k] = i
+ }
+ }
+ }
+}
+
func toFormatMark(format string) (metadecoders.Format, error) {
if f := metadecoders.FormatFromString(format); f != "" {
return f, nil
diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go
index 06bae42d40e..12d8aafb553 100644
--- a/tpl/transform/remarshal_test.go
+++ b/tpl/transform/remarshal_test.go
@@ -156,11 +156,11 @@ Hugo = "Rules"
func TestTestRemarshalError(t *testing.T) {
t.Parallel()
+ c := qt.New(t)
v := viper.New()
v.Set("contentDir", "content")
ns := New(newDeps(v))
- c := qt.New(t)
_, err := ns.Remarshal("asdf", "asdf")
c.Assert(err, qt.Not(qt.IsNil))
@@ -169,3 +169,19 @@ func TestTestRemarshalError(t *testing.T) {
c.Assert(err, qt.Not(qt.IsNil))
}
+
+func TestTestRemarshalMapInput(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+ v := viper.New()
+ v.Set("contentDir", "content")
+ ns := New(newDeps(v))
+
+ input := map[string]interface{}{
+ "hello": "world",
+ }
+
+ output, err := ns.Remarshal("toml", input)
+ c.Assert(err, qt.IsNil)
+ c.Assert(output, qt.Equals, "hello = \"world\"\n")
+}
diff --git a/tpl/transform/transform.go b/tpl/transform/transform.go
index 24eedc24f45..b168d2a50d4 100644
--- a/tpl/transform/transform.go
+++ b/tpl/transform/transform.go
@@ -65,7 +65,7 @@ func (ns *Namespace) Highlight(s interface{}, lang, opts string) (template.HTML,
return "", err
}
- highlighted, _ := ns.deps.ContentSpec.Highlight(ss, lang, opts)
+ highlighted, _ := ns.deps.ContentSpec.Converters.Highlight(ss, lang, opts)
return template.HTML(highlighted), nil
}
diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go
index a6a3b793ecd..3794c6f2ea9 100644
--- a/tpl/transform/transform_test.go
+++ b/tpl/transform/transform_test.go
@@ -204,7 +204,7 @@ And then some.
result, err := ns.Markdownify(text)
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, template.HTML(
- "#First
\n\nThis is some bold text.
\n\nSecond
\n\nThis is some more text.
\n\nAnd then some.
\n"))
+ "#First
\nThis is some bold text.
\nSecond
\nThis is some more text.
\nAnd then some.
\n"))
}