2017-08-18 17:07:01 +02:00
|
|
|
package renderers
|
2017-07-14 18:12:25 +02:00
|
|
|
|
2017-08-11 23:16:49 +02:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"io"
|
|
|
|
"regexp"
|
|
|
|
|
2022-01-29 20:17:23 +01:00
|
|
|
"github.com/danog/blackfriday/v2"
|
|
|
|
"github.com/danog/gojekyll/utils"
|
2017-08-11 23:16:49 +02:00
|
|
|
"golang.org/x/net/html"
|
|
|
|
)
|
2017-07-14 18:12:25 +02:00
|
|
|
|
|
|
|
const blackfridayFlags = 0 |
|
2021-06-04 12:13:49 +02:00
|
|
|
blackfriday.UseXHTML |
|
|
|
|
blackfriday.Smartypants |
|
|
|
|
blackfriday.SmartypantsFractions |
|
|
|
|
blackfriday.SmartypantsDashes |
|
|
|
|
blackfriday.SmartypantsLatexDashes
|
2017-07-14 18:12:25 +02:00
|
|
|
|
|
|
|
const blackfridayExtensions = 0 |
|
2021-06-04 12:13:49 +02:00
|
|
|
blackfriday.NoIntraEmphasis |
|
|
|
|
blackfriday.Tables |
|
|
|
|
blackfriday.FencedCode |
|
|
|
|
blackfriday.Autolink |
|
|
|
|
blackfriday.Strikethrough |
|
|
|
|
blackfriday.SpaceHeadings |
|
|
|
|
blackfriday.HeadingIDs |
|
|
|
|
blackfriday.BackslashLineBreak |
|
|
|
|
blackfriday.DefinitionLists |
|
2022-01-29 20:17:23 +01:00
|
|
|
blackfriday.NoEmptyLineBeforeBlock |
|
2017-07-14 18:12:25 +02:00
|
|
|
// added relative to commonExtensions
|
2021-06-04 12:13:49 +02:00
|
|
|
blackfriday.AutoHeadingIDs
|
2017-07-14 18:12:25 +02:00
|
|
|
|
2017-08-22 16:47:34 +02:00
|
|
|
func renderMarkdown(md []byte) ([]byte, error) {
|
2021-06-04 12:13:49 +02:00
|
|
|
params := blackfriday.HTMLRendererParameters{
|
|
|
|
Flags: blackfridayFlags,
|
|
|
|
}
|
|
|
|
renderer := blackfriday.NewHTMLRenderer(params)
|
|
|
|
html := blackfriday.Run(
|
2017-08-22 16:47:34 +02:00
|
|
|
md,
|
2021-06-04 12:13:49 +02:00
|
|
|
blackfriday.WithRenderer(renderer),
|
|
|
|
blackfriday.WithExtensions(blackfridayExtensions),
|
2017-08-22 16:47:34 +02:00
|
|
|
)
|
2017-08-11 23:16:49 +02:00
|
|
|
html, err := renderInnerMarkdown(html)
|
|
|
|
if err != nil {
|
2017-08-22 17:00:46 +02:00
|
|
|
return nil, utils.WrapError(err, "markdown")
|
2017-08-11 23:16:49 +02:00
|
|
|
}
|
2017-08-22 16:47:34 +02:00
|
|
|
return html, nil
|
2017-08-11 23:16:49 +02:00
|
|
|
}
|
|
|
|
|
2017-08-22 16:47:34 +02:00
|
|
|
func _renderMarkdown(md []byte) ([]byte, error) {
|
2021-06-04 12:13:49 +02:00
|
|
|
params := blackfriday.HTMLRendererParameters{
|
|
|
|
Flags: blackfridayFlags,
|
|
|
|
}
|
|
|
|
renderer := blackfriday.NewHTMLRenderer(params)
|
|
|
|
html := blackfriday.Run(
|
2017-08-22 16:47:34 +02:00
|
|
|
md,
|
2021-06-04 12:13:49 +02:00
|
|
|
blackfriday.WithRenderer(renderer),
|
|
|
|
blackfriday.WithExtensions(blackfridayExtensions),
|
2017-08-22 16:47:34 +02:00
|
|
|
)
|
|
|
|
return html, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// search HTML for markdown=1, and process if found
|
2017-08-11 23:16:49 +02:00
|
|
|
func renderInnerMarkdown(b []byte) ([]byte, error) {
|
|
|
|
z := html.NewTokenizer(bytes.NewReader(b))
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
outer:
|
|
|
|
for {
|
|
|
|
tt := z.Next()
|
|
|
|
switch tt {
|
|
|
|
case html.ErrorToken:
|
|
|
|
if z.Err() == io.EOF {
|
|
|
|
break outer
|
|
|
|
}
|
|
|
|
return nil, z.Err()
|
|
|
|
case html.StartTagToken:
|
2017-08-14 21:23:00 +02:00
|
|
|
if hasMarkdownAttr(z) {
|
2017-08-22 16:47:34 +02:00
|
|
|
_, err := buf.Write(stripMarkdownAttr(z.Raw()))
|
2017-08-11 23:16:49 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := processInnerMarkdown(buf, z); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// the above leaves z set to the end token
|
2017-08-22 16:47:34 +02:00
|
|
|
// fall through to render it
|
2017-08-11 23:16:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
_, err := buf.Write(z.Raw())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
2017-08-14 21:23:00 +02:00
|
|
|
func hasMarkdownAttr(z *html.Tokenizer) bool {
|
2017-08-11 23:16:49 +02:00
|
|
|
for {
|
|
|
|
k, v, more := z.TagAttr()
|
|
|
|
switch {
|
|
|
|
case string(k) == "markdown" && string(v) == "1":
|
|
|
|
return true
|
|
|
|
case !more:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-22 16:47:34 +02:00
|
|
|
var markdownAttrRE = regexp.MustCompile(`\s*markdown\s*=[^\s>]*\s*`)
|
|
|
|
|
|
|
|
// return the text of a start tag, w/out the markdown attribute
|
|
|
|
func stripMarkdownAttr(tag []byte) []byte {
|
2017-08-14 21:23:00 +02:00
|
|
|
tag = markdownAttrRE.ReplaceAll(tag, []byte(" "))
|
|
|
|
tag = bytes.Replace(tag, []byte(" >"), []byte(">"), 1)
|
|
|
|
return tag
|
|
|
|
}
|
|
|
|
|
2017-08-22 17:10:39 +02:00
|
|
|
// Used inside markdown=1.
|
|
|
|
// TODO Instead of this approach, only count tags that match the start
|
|
|
|
// tag. For example, if <div markdown="1"> kicked off the inner markdown,
|
|
|
|
// count the div depth.
|
|
|
|
var notATagRE = regexp.MustCompile(`@|(https?|ftp):`)
|
|
|
|
|
2017-08-22 16:47:34 +02:00
|
|
|
// called once markdown="1" attribute is detected.
|
|
|
|
// Collects the HTML tokens into a string, applies markdown to them,
|
|
|
|
// and writes the result
|
2017-08-11 23:16:49 +02:00
|
|
|
func processInnerMarkdown(w io.Writer, z *html.Tokenizer) error {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
depth := 1
|
|
|
|
loop:
|
|
|
|
for {
|
|
|
|
tt := z.Next()
|
|
|
|
switch tt {
|
|
|
|
case html.ErrorToken:
|
|
|
|
return z.Err()
|
|
|
|
case html.StartTagToken:
|
2017-08-22 17:10:39 +02:00
|
|
|
if !notATagRE.Match(z.Raw()) {
|
|
|
|
depth++
|
|
|
|
}
|
2017-08-11 23:16:49 +02:00
|
|
|
case html.EndTagToken:
|
|
|
|
depth--
|
|
|
|
if depth == 0 {
|
|
|
|
break loop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_, err := buf.Write(z.Raw())
|
|
|
|
if err != nil {
|
2017-08-22 16:47:34 +02:00
|
|
|
return err
|
2017-08-11 23:16:49 +02:00
|
|
|
}
|
|
|
|
}
|
2017-08-22 16:47:34 +02:00
|
|
|
html, err := _renderMarkdown(buf.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = w.Write(html)
|
2017-08-11 23:16:49 +02:00
|
|
|
return err
|
2017-07-14 18:12:25 +02:00
|
|
|
}
|