mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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:
		| @@ -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>") | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user