Rewrite markdown rendering to blackfriday v2 and rewrite orgmode rendering to go-org (#8560)
* Rewrite markdown rendering to blackfriday v2.0 * Fix style * Fix go mod with golang 1.13 * Fix blackfriday v2 import * Inital orgmode renderer migration to go-org * Vendor go-org dependency * Ignore errors :/ * Update go-org to latest version * Update test * Fix go-org test * Remove unneeded code * Fix comments * Fix markdown test * Fix blackfriday regression rendering HTML block
This commit is contained in:
parent
690a8ec502
commit
086a46994a
55 changed files with 5769 additions and 3732 deletions
|
@ -323,6 +323,6 @@ func TestRender_ShortLinks(t *testing.T) {
|
|||
`<p><a href="`+notencodedImgurlWiki+`" rel="nofollow"><img src="`+notencodedImgurlWiki+`"/></a></p>`)
|
||||
test(
|
||||
"<p><a href=\"https://example.org\">[[foobar]]</a></p>",
|
||||
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`,
|
||||
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`)
|
||||
`<p></p><p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p><p></p>`,
|
||||
`<p></p><p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p><p></p>`)
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ package markdown
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// Renderer is a extended version of underlying render object.
|
||||
|
@ -25,134 +26,138 @@ type Renderer struct {
|
|||
|
||||
var byteMailto = []byte("mailto:")
|
||||
|
||||
// Link defines how formal links should be processed to produce corresponding HTML elements.
|
||||
func (r *Renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
// special case: this is not a link, a hash link or a mailto:, so it's a
|
||||
// relative URL
|
||||
if len(link) > 0 && !markup.IsLink(link) &&
|
||||
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
|
||||
lnk := string(link)
|
||||
var htmlEscaper = [256][]byte{
|
||||
'&': []byte("&"),
|
||||
'<': []byte("<"),
|
||||
'>': []byte(">"),
|
||||
'"': []byte("""),
|
||||
}
|
||||
|
||||
func escapeHTML(w io.Writer, s []byte) {
|
||||
var start, end int
|
||||
for end < len(s) {
|
||||
escSeq := htmlEscaper[s[end]]
|
||||
if escSeq != nil {
|
||||
_, _ = w.Write(s[start:end])
|
||||
_, _ = w.Write(escSeq)
|
||||
start = end + 1
|
||||
}
|
||||
end++
|
||||
}
|
||||
if start < len(s) && end <= len(s) {
|
||||
_, _ = w.Write(s[start:end])
|
||||
}
|
||||
}
|
||||
|
||||
// RenderNode is a default renderer of a single node of a syntax tree. For
|
||||
// block nodes it will be called twice: first time with entering=true, second
|
||||
// time with entering=false, so that it could know when it's working on an open
|
||||
// tag and when on close. It writes the result to w.
|
||||
//
|
||||
// The return value is a way to tell the calling walker to adjust its walk
|
||||
// pattern: e.g. it can terminate the traversal by returning Terminate. Or it
|
||||
// can ask the walker to skip a subtree of this node by returning SkipChildren.
|
||||
// The typical behavior is to return GoToNext, which asks for the usual
|
||||
// traversal to the next node.
|
||||
func (r *Renderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
switch node.Type {
|
||||
case blackfriday.Image:
|
||||
prefix := r.URLPrefix
|
||||
if r.IsWiki {
|
||||
lnk = util.URLJoin("wiki", lnk)
|
||||
prefix = util.URLJoin(prefix, "wiki", "raw")
|
||||
}
|
||||
mLink := util.URLJoin(r.URLPrefix, lnk)
|
||||
link = []byte(mLink)
|
||||
}
|
||||
|
||||
if len(content) > 10 && string(content[0:9]) == "<a href=\"" && bytes.Contains(content[9:], []byte("<img")) {
|
||||
// Image with link case: markdown `[![]()]()`
|
||||
// If the content is an image, then we change the original href around it
|
||||
// which points to itself to a new address "link"
|
||||
rightQuote := bytes.Index(content[9:], []byte("\""))
|
||||
content = bytes.Replace(content, content[9:9+rightQuote], link, 1)
|
||||
out.Write(content)
|
||||
} else {
|
||||
r.Renderer.Link(out, link, title, content)
|
||||
}
|
||||
}
|
||||
|
||||
// List renders markdown bullet or digit lists to HTML
|
||||
func (r *Renderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
marker := out.Len()
|
||||
if out.Len() > 0 {
|
||||
out.WriteByte('\n')
|
||||
}
|
||||
|
||||
if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("<dl>")
|
||||
} else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("<ol class='ui list'>")
|
||||
} else {
|
||||
out.WriteString("<ul class='ui list'>")
|
||||
}
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
return
|
||||
}
|
||||
if flags&blackfriday.LIST_TYPE_DEFINITION != 0 {
|
||||
out.WriteString("</dl>\n")
|
||||
} else if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
|
||||
out.WriteString("</ol>\n")
|
||||
} else {
|
||||
out.WriteString("</ul>\n")
|
||||
}
|
||||
}
|
||||
|
||||
// ListItem defines how list items should be processed to produce corresponding HTML elements.
|
||||
func (r *Renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
// Detect procedures to draw checkboxes.
|
||||
prefix := ""
|
||||
if bytes.HasPrefix(text, []byte("<p>")) {
|
||||
prefix = "<p>"
|
||||
}
|
||||
switch {
|
||||
case bytes.HasPrefix(text, []byte(prefix+"[ ] ")):
|
||||
text = append([]byte(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
|
||||
if prefix != "" {
|
||||
text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
|
||||
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
|
||||
link := node.LinkData.Destination
|
||||
if len(link) > 0 && !markup.IsLink(link) {
|
||||
lnk := string(link)
|
||||
lnk = util.URLJoin(prefix, lnk)
|
||||
lnk = strings.Replace(lnk, " ", "+", -1)
|
||||
link = []byte(lnk)
|
||||
}
|
||||
case bytes.HasPrefix(text, []byte(prefix+"[x] ")):
|
||||
text = append([]byte(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></span>`), text[3+len(prefix):]...)
|
||||
if prefix != "" {
|
||||
text = bytes.Replace(text, []byte(prefix), []byte{}, 1)
|
||||
node.LinkData.Destination = link
|
||||
// Render link around image only if parent is not link already
|
||||
if node.Parent != nil && node.Parent.Type != blackfriday.Link {
|
||||
if entering {
|
||||
_, _ = w.Write([]byte(`<a href="`))
|
||||
escapeHTML(w, link)
|
||||
_, _ = w.Write([]byte(`">`))
|
||||
return r.Renderer.RenderNode(w, node, entering)
|
||||
}
|
||||
s := r.Renderer.RenderNode(w, node, entering)
|
||||
_, _ = w.Write([]byte(`</a>`))
|
||||
return s
|
||||
}
|
||||
return r.Renderer.RenderNode(w, node, entering)
|
||||
case blackfriday.Link:
|
||||
// special case: this is not a link, a hash link or a mailto:, so it's a
|
||||
// relative URL
|
||||
link := node.LinkData.Destination
|
||||
if len(link) > 0 && !markup.IsLink(link) &&
|
||||
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) &&
|
||||
node.LinkData.Footnote == nil {
|
||||
lnk := string(link)
|
||||
if r.IsWiki {
|
||||
lnk = util.URLJoin("wiki", lnk)
|
||||
}
|
||||
link = []byte(util.URLJoin(r.URLPrefix, lnk))
|
||||
}
|
||||
node.LinkData.Destination = link
|
||||
return r.Renderer.RenderNode(w, node, entering)
|
||||
case blackfriday.Text:
|
||||
isListItem := false
|
||||
for n := node.Parent; n != nil; n = n.Parent {
|
||||
if n.Type == blackfriday.Item {
|
||||
isListItem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isListItem {
|
||||
text := node.Literal
|
||||
switch {
|
||||
case bytes.HasPrefix(text, []byte("[ ] ")):
|
||||
_, _ = w.Write([]byte(`<span class="ui fitted disabled checkbox"><input type="checkbox" disabled="disabled" /><label /></span>`))
|
||||
text = text[3:]
|
||||
case bytes.HasPrefix(text, []byte("[x] ")):
|
||||
_, _ = w.Write([]byte(`<span class="ui checked fitted disabled checkbox"><input type="checkbox" checked="" disabled="disabled" /><label /></span>`))
|
||||
text = text[3:]
|
||||
}
|
||||
node.Literal = text
|
||||
}
|
||||
}
|
||||
r.Renderer.ListItem(out, text, flags)
|
||||
}
|
||||
|
||||
// Image defines how images should be processed to produce corresponding HTML elements.
|
||||
func (r *Renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
prefix := r.URLPrefix
|
||||
if r.IsWiki {
|
||||
prefix = util.URLJoin(prefix, "wiki", "raw")
|
||||
}
|
||||
prefix = strings.Replace(prefix, "/src/", "/media/", 1)
|
||||
if len(link) > 0 && !markup.IsLink(link) {
|
||||
lnk := string(link)
|
||||
lnk = util.URLJoin(prefix, lnk)
|
||||
lnk = strings.Replace(lnk, " ", "+", -1)
|
||||
link = []byte(lnk)
|
||||
}
|
||||
|
||||
// Put a link around it pointing to itself by default
|
||||
out.WriteString(`<a href="`)
|
||||
out.Write(link)
|
||||
out.WriteString(`">`)
|
||||
r.Renderer.Image(out, link, title, alt)
|
||||
out.WriteString("</a>")
|
||||
return r.Renderer.RenderNode(w, node, entering)
|
||||
}
|
||||
|
||||
const (
|
||||
blackfridayExtensions = 0 |
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
blackfriday.EXTENSION_FENCED_CODE |
|
||||
blackfriday.EXTENSION_STRIKETHROUGH |
|
||||
blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK |
|
||||
blackfriday.EXTENSION_DEFINITION_LISTS |
|
||||
blackfriday.EXTENSION_FOOTNOTES |
|
||||
blackfriday.EXTENSION_HEADER_IDS |
|
||||
blackfriday.EXTENSION_AUTO_HEADER_IDS
|
||||
blackfriday.NoIntraEmphasis |
|
||||
blackfriday.Tables |
|
||||
blackfriday.FencedCode |
|
||||
blackfriday.Strikethrough |
|
||||
blackfriday.NoEmptyLineBeforeBlock |
|
||||
blackfriday.DefinitionLists |
|
||||
blackfriday.Footnotes |
|
||||
blackfriday.HeadingIDs |
|
||||
blackfriday.AutoHeadingIDs
|
||||
blackfridayHTMLFlags = 0 |
|
||||
blackfriday.HTML_SKIP_STYLE |
|
||||
blackfriday.HTML_OMIT_CONTENTS |
|
||||
blackfriday.HTML_USE_SMARTYPANTS
|
||||
blackfriday.Smartypants
|
||||
)
|
||||
|
||||
// RenderRaw renders Markdown to HTML without handling special links.
|
||||
func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
|
||||
renderer := &Renderer{
|
||||
Renderer: blackfriday.HtmlRenderer(blackfridayHTMLFlags, "", ""),
|
||||
Renderer: blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||
Flags: blackfridayHTMLFlags,
|
||||
}),
|
||||
URLPrefix: urlPrefix,
|
||||
IsWiki: wikiMarkdown,
|
||||
}
|
||||
|
||||
exts := blackfridayExtensions
|
||||
if setting.Markdown.EnableHardLineBreak {
|
||||
exts |= blackfriday.EXTENSION_HARD_LINE_BREAK
|
||||
exts |= blackfriday.HardLineBreak
|
||||
}
|
||||
|
||||
body = blackfriday.Markdown(body, renderer, exts)
|
||||
body = blackfriday.Run(body, blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(exts))
|
||||
return markup.SanitizeBytes(body)
|
||||
}
|
||||
|
||||
|
|
|
@ -166,13 +166,13 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
|
|||
<h3 id="footnotes">Footnotes</h3>
|
||||
|
||||
<p>Here is a simple footnote,<sup id="fnref:1"><a href="#fn:1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:bignote"><a href="#fn:bignote" rel="nofollow">2</a></sup></p>
|
||||
|
||||
<div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<ol>
|
||||
<li id="fn:1">This is the first footnote.
|
||||
</li>
|
||||
<li id="fn:1">This is the first footnote.</li>
|
||||
|
||||
<li id="fn:bignote"><p>Here is one with multiple paragraphs and code.</p>
|
||||
|
||||
|
@ -180,9 +180,9 @@ func testAnswers(baseURLContent, baseURLImages string) []string {
|
|||
|
||||
<p><code>{ my code }</code></p>
|
||||
|
||||
<p>Add as many paragraphs as you like.</p>
|
||||
</li>
|
||||
<p>Add as many paragraphs as you like.</p></li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
|
|
|
@ -6,43 +6,39 @@ package mdstripper
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
// MarkdownStripper extends blackfriday.Renderer
|
||||
type MarkdownStripper struct {
|
||||
blackfriday.Renderer
|
||||
links []string
|
||||
coallesce bool
|
||||
empty bool
|
||||
}
|
||||
|
||||
const (
|
||||
blackfridayExtensions = 0 |
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
blackfriday.EXTENSION_FENCED_CODE |
|
||||
blackfriday.EXTENSION_STRIKETHROUGH |
|
||||
blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK |
|
||||
blackfriday.EXTENSION_DEFINITION_LISTS |
|
||||
blackfriday.EXTENSION_FOOTNOTES |
|
||||
blackfriday.EXTENSION_HEADER_IDS |
|
||||
blackfriday.EXTENSION_AUTO_HEADER_IDS |
|
||||
blackfriday.NoIntraEmphasis |
|
||||
blackfriday.Tables |
|
||||
blackfriday.FencedCode |
|
||||
blackfriday.Strikethrough |
|
||||
blackfriday.NoEmptyLineBeforeBlock |
|
||||
blackfriday.DefinitionLists |
|
||||
blackfriday.Footnotes |
|
||||
blackfriday.HeadingIDs |
|
||||
blackfriday.AutoHeadingIDs |
|
||||
// Not included in modules/markup/markdown/markdown.go;
|
||||
// required here to process inline links
|
||||
blackfriday.EXTENSION_AUTOLINK
|
||||
blackfriday.Autolink
|
||||
)
|
||||
|
||||
//revive:disable:var-naming Implementing the Rendering interface requires breaking some linting rules
|
||||
|
||||
// StripMarkdown parses markdown content by removing all markup and code blocks
|
||||
// in order to extract links and other references
|
||||
func StripMarkdown(rawBytes []byte) (string, []string) {
|
||||
stripper := &MarkdownStripper{
|
||||
links: make([]string, 0, 10),
|
||||
}
|
||||
body := blackfriday.Markdown(rawBytes, stripper, blackfridayExtensions)
|
||||
return string(body), stripper.GetLinks()
|
||||
buf, links := StripMarkdownBytes(rawBytes)
|
||||
return string(buf), links
|
||||
}
|
||||
|
||||
// StripMarkdownBytes parses markdown content by removing all markup and code blocks
|
||||
|
@ -50,205 +46,67 @@ func StripMarkdown(rawBytes []byte) (string, []string) {
|
|||
func StripMarkdownBytes(rawBytes []byte) ([]byte, []string) {
|
||||
stripper := &MarkdownStripper{
|
||||
links: make([]string, 0, 10),
|
||||
empty: true,
|
||||
}
|
||||
body := blackfriday.Markdown(rawBytes, stripper, blackfridayExtensions)
|
||||
return body, stripper.GetLinks()
|
||||
|
||||
parser := blackfriday.New(blackfriday.WithRenderer(stripper), blackfriday.WithExtensions(blackfridayExtensions))
|
||||
ast := parser.Parse(rawBytes)
|
||||
var buf bytes.Buffer
|
||||
stripper.RenderHeader(&buf, ast)
|
||||
ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
return stripper.RenderNode(&buf, node, entering)
|
||||
})
|
||||
stripper.RenderFooter(&buf, ast)
|
||||
return buf.Bytes(), stripper.GetLinks()
|
||||
}
|
||||
|
||||
// block-level callbacks
|
||||
|
||||
// BlockCode dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) BlockCode(out *bytes.Buffer, text []byte, infoString string) {
|
||||
// Not rendered
|
||||
// RenderNode is the main rendering method. It will be called once for
|
||||
// every leaf node and twice for every non-leaf node (first with
|
||||
// entering=true, then with entering=false). The method should write its
|
||||
// rendition of the node to the supplied writer w.
|
||||
func (r *MarkdownStripper) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
if !entering {
|
||||
return blackfriday.GoToNext
|
||||
}
|
||||
switch node.Type {
|
||||
case blackfriday.Text:
|
||||
r.processString(w, node.Literal, node.Parent == nil)
|
||||
return blackfriday.GoToNext
|
||||
case blackfriday.Link:
|
||||
r.processLink(w, node.LinkData.Destination)
|
||||
r.coallesce = false
|
||||
return blackfriday.SkipChildren
|
||||
}
|
||||
r.coallesce = false
|
||||
return blackfriday.GoToNext
|
||||
}
|
||||
|
||||
// BlockQuote dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) BlockQuote(out *bytes.Buffer, text []byte) {
|
||||
// FIXME: perhaps it's better to leave out block quote for this?
|
||||
r.processString(out, text, false)
|
||||
// RenderHeader is a method that allows the renderer to produce some
|
||||
// content preceding the main body of the output document.
|
||||
func (r *MarkdownStripper) RenderHeader(w io.Writer, ast *blackfriday.Node) {
|
||||
}
|
||||
|
||||
// BlockHtml dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) BlockHtml(out *bytes.Buffer, text []byte) { //nolint
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
// RenderFooter is a symmetric counterpart of RenderHeader.
|
||||
func (r *MarkdownStripper) RenderFooter(w io.Writer, ast *blackfriday.Node) {
|
||||
}
|
||||
|
||||
// Header dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Header(out *bytes.Buffer, text func() bool, level int, id string) {
|
||||
text()
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// HRule dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) HRule(out *bytes.Buffer) {
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// List dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
text()
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// ListItem dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) ListItem(out *bytes.Buffer, text []byte, flags int) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// Paragraph dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
text()
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// Table dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
|
||||
r.processString(out, header, false)
|
||||
r.processString(out, body, false)
|
||||
}
|
||||
|
||||
// TableRow dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) TableRow(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// TableHeaderCell dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// TableCell dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) TableCell(out *bytes.Buffer, text []byte, flags int) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// Footnotes dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Footnotes(out *bytes.Buffer, text func() bool) {
|
||||
text()
|
||||
}
|
||||
|
||||
// FootnoteItem dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// TitleBlock dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) TitleBlock(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// Span-level callbacks
|
||||
|
||||
// AutoLink dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) AutoLink(out *bytes.Buffer, link []byte, kind int) {
|
||||
r.processLink(out, link, []byte{})
|
||||
}
|
||||
|
||||
// CodeSpan dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// DoubleEmphasis dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// Emphasis dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// Image dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// LineBreak dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) LineBreak(out *bytes.Buffer) {
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// Link dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
r.processLink(out, link, content)
|
||||
}
|
||||
|
||||
// RawHtmlTag dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) RawHtmlTag(out *bytes.Buffer, tag []byte) { //nolint
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// TripleEmphasis dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// StrikeThrough dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) StrikeThrough(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, false)
|
||||
}
|
||||
|
||||
// FootnoteRef dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
|
||||
// Not rendered
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// Low-level callbacks
|
||||
|
||||
// Entity dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) Entity(out *bytes.Buffer, entity []byte) {
|
||||
// FIXME: literal entities are not parsed; perhaps they should
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// NormalText dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) NormalText(out *bytes.Buffer, text []byte) {
|
||||
r.processString(out, text, true)
|
||||
}
|
||||
|
||||
// Header and footer
|
||||
|
||||
// DocumentHeader dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) DocumentHeader(out *bytes.Buffer) {
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// DocumentFooter dummy function to proceed with rendering
|
||||
func (r *MarkdownStripper) DocumentFooter(out *bytes.Buffer) {
|
||||
r.coallesce = false
|
||||
}
|
||||
|
||||
// GetFlags returns rendering flags
|
||||
func (r *MarkdownStripper) GetFlags() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
//revive:enable:var-naming
|
||||
|
||||
func doubleSpace(out *bytes.Buffer) {
|
||||
if out.Len() > 0 {
|
||||
out.WriteByte('\n')
|
||||
func (r *MarkdownStripper) doubleSpace(w io.Writer) {
|
||||
if !r.empty {
|
||||
_, _ = w.Write([]byte{'\n'})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *MarkdownStripper) processString(out *bytes.Buffer, text []byte, coallesce bool) {
|
||||
func (r *MarkdownStripper) processString(w io.Writer, text []byte, coallesce bool) {
|
||||
// Always break-up words
|
||||
if !coallesce || !r.coallesce {
|
||||
doubleSpace(out)
|
||||
r.doubleSpace(w)
|
||||
}
|
||||
out.Write(text)
|
||||
_, _ = w.Write(text)
|
||||
r.coallesce = coallesce
|
||||
r.empty = false
|
||||
}
|
||||
func (r *MarkdownStripper) processLink(out *bytes.Buffer, link []byte, content []byte) {
|
||||
|
||||
func (r *MarkdownStripper) processLink(w io.Writer, link []byte) {
|
||||
// Links are processed out of band
|
||||
r.links = append(r.links, string(link))
|
||||
r.coallesce = false
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/chaseadamsio/goorgeous"
|
||||
"github.com/russross/blackfriday"
|
||||
"github.com/niklasfasching/go-org/org"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -32,23 +36,23 @@ func (Parser) Extensions() []string {
|
|||
}
|
||||
|
||||
// Render renders orgmode rawbytes to HTML
|
||||
func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) (result []byte) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error("Panic in orgmode.Render: %v Just returning the rawBytes", err)
|
||||
result = rawBytes
|
||||
}
|
||||
}()
|
||||
htmlFlags := blackfriday.HTML_USE_XHTML
|
||||
htmlFlags |= blackfriday.HTML_SKIP_STYLE
|
||||
htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
|
||||
renderer := &markdown.Renderer{
|
||||
Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
|
||||
URLPrefix: urlPrefix,
|
||||
IsWiki: isWiki,
|
||||
func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
|
||||
htmlWriter := org.NewHTMLWriter()
|
||||
|
||||
renderer := &Renderer{
|
||||
HTMLWriter: htmlWriter,
|
||||
URLPrefix: urlPrefix,
|
||||
IsWiki: isWiki,
|
||||
}
|
||||
result = goorgeous.Org(rawBytes, renderer)
|
||||
return
|
||||
|
||||
htmlWriter.ExtendingWriter = renderer
|
||||
|
||||
res, err := org.New().Silent().Parse(bytes.NewReader(rawBytes), "").Write(renderer)
|
||||
if err != nil {
|
||||
log.Error("Panic in orgmode.Render: %v Just returning the rawBytes", err)
|
||||
return rawBytes
|
||||
}
|
||||
return []byte(res)
|
||||
}
|
||||
|
||||
// RenderString reners orgmode string to HTML string
|
||||
|
@ -56,7 +60,63 @@ func RenderString(rawContent string, urlPrefix string, metas map[string]string,
|
|||
return string(Render([]byte(rawContent), urlPrefix, metas, isWiki))
|
||||
}
|
||||
|
||||
// Render implements markup.Parser
|
||||
// Render reners orgmode string to HTML string
|
||||
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
|
||||
return Render(rawBytes, urlPrefix, metas, isWiki)
|
||||
}
|
||||
|
||||
// Renderer implements org.Writer
|
||||
type Renderer struct {
|
||||
*org.HTMLWriter
|
||||
URLPrefix string
|
||||
IsWiki bool
|
||||
}
|
||||
|
||||
var byteMailto = []byte("mailto:")
|
||||
|
||||
// WriteRegularLink renders images, links or videos
|
||||
func (r *Renderer) WriteRegularLink(l org.RegularLink) {
|
||||
link := []byte(html.EscapeString(l.URL))
|
||||
if l.Protocol == "file" {
|
||||
link = link[len("file:"):]
|
||||
}
|
||||
if len(link) > 0 && !markup.IsLink(link) &&
|
||||
link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
|
||||
lnk := string(link)
|
||||
if r.IsWiki {
|
||||
lnk = util.URLJoin("wiki", lnk)
|
||||
}
|
||||
link = []byte(util.URLJoin(r.URLPrefix, lnk))
|
||||
}
|
||||
|
||||
description := string(link)
|
||||
if l.Description != nil {
|
||||
description = r.nodesAsString(l.Description...)
|
||||
}
|
||||
switch l.Kind() {
|
||||
case "image":
|
||||
r.WriteString(fmt.Sprintf(`<img src="%s" alt="%s" title="%s" />`, link, description, description))
|
||||
case "video":
|
||||
r.WriteString(fmt.Sprintf(`<video src="%s" title="%s">%s</video>`, link, description, description))
|
||||
default:
|
||||
r.WriteString(fmt.Sprintf(`<a href="%s" title="%s">%s</a>`, link, description, description))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) emptyClone() *Renderer {
|
||||
wcopy := *(r.HTMLWriter)
|
||||
wcopy.Builder = strings.Builder{}
|
||||
|
||||
rcopy := *r
|
||||
rcopy.HTMLWriter = &wcopy
|
||||
|
||||
wcopy.ExtendingWriter = &rcopy
|
||||
|
||||
return &rcopy
|
||||
}
|
||||
|
||||
func (r *Renderer) nodesAsString(nodes ...org.Node) string {
|
||||
tmp := r.emptyClone()
|
||||
org.WriteNodes(tmp, nodes...)
|
||||
return tmp.String()
|
||||
}
|
||||
|
|
|
@ -27,12 +27,12 @@ func TestRender_StandardLinks(t *testing.T) {
|
|||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
googleRendered := `<p><a href="https://google.com/" title="https://google.com/">https://google.com/</a></p>`
|
||||
googleRendered := "<p>\n<a href=\"https://google.com/\" title=\"https://google.com/\">https://google.com/</a>\n</p>"
|
||||
test("[[https://google.com/]]", googleRendered)
|
||||
|
||||
lnk := util.URLJoin(AppSubURL, "WikiPage")
|
||||
test("[[WikiPage][WikiPage]]",
|
||||
`<p><a href="`+lnk+`" title="WikiPage">WikiPage</a></p>`)
|
||||
"<p>\n<a href=\""+lnk+"\" title=\"WikiPage\">WikiPage</a>\n</p>")
|
||||
}
|
||||
|
||||
func TestRender_Images(t *testing.T) {
|
||||
|
@ -45,10 +45,8 @@ func TestRender_Images(t *testing.T) {
|
|||
}
|
||||
|
||||
url := "../../.images/src/02/train.jpg"
|
||||
title := "Train"
|
||||
result := util.URLJoin(AppSubURL, url)
|
||||
|
||||
test(
|
||||
"[[file:"+url+"]["+title+"]]",
|
||||
`<p><a href="`+result+`"><img src="`+result+`" alt="`+title+`" title="`+title+`" /></a></p>`)
|
||||
test("[[file:"+url+"]]",
|
||||
"<p>\n<img src=\""+result+"\" alt=\""+result+"\" title=\""+result+"\" />\n</p>")
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue