Refactor renders (#15175)
* Refactor renders * Some performance optimization * Fix comment * Transform reader * Fix csv test * Fix test * Fix tests * Improve optimaziation * Fix test * Fix test * Detect file encoding with reader * Improve optimaziation * reduce memory usage * improve code * fix build * Fix test * Fix for go1.15 * Fix render * Fix comment * Fix lint * Fix test * Don't use NormalEOF when unnecessary * revert change on util.go * Apply suggestions from code review Co-authored-by: zeripath <art27@cantab.net> * rename function * Take NormalEOF back Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
		
							parent
							
								
									c9cc6698d2
								
							
						
					
					
						commit
						9d99f6ab19
					
				
					 41 changed files with 1027 additions and 627 deletions
				
			
		|  | @ -114,7 +114,7 @@ func runPR() { | ||||||
| 
 | 
 | ||||||
| 	log.Printf("[PR] Setting up router\n") | 	log.Printf("[PR] Setting up router\n") | ||||||
| 	//routers.GlobalInit()
 | 	//routers.GlobalInit()
 | ||||||
| 	external.RegisterParsers() | 	external.RegisterRenderers() | ||||||
| 	markup.Init() | 	markup.Init() | ||||||
| 	c := routes.NormalRoutes() | 	c := routes.NormalRoutes() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/references" | 	"code.gitea.io/gitea/modules/references" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  | @ -1178,8 +1179,13 @@ func findCodeComments(e Engine, opts FindCommentsOptions, issue *Issue, currentU | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(), | 		var err error | ||||||
| 			issue.Repo.ComposeMetas())) | 		if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: issue.Repo.Link(), | ||||||
|  | 			Metas:     issue.Repo.ComposeMetas(), | ||||||
|  | 		}, comment.Content); err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	return comments[:n], nil | 	return comments[:n], nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -863,7 +863,10 @@ func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []* | ||||||
| 
 | 
 | ||||||
| // DescriptionHTML does special handles to description and return HTML string.
 | // DescriptionHTML does special handles to description and return HTML string.
 | ||||||
| func (repo *Repository) DescriptionHTML() template.HTML { | func (repo *Repository) DescriptionHTML() template.HTML { | ||||||
| 	desc, err := markup.RenderDescriptionHTML([]byte(repo.Description), repo.HTMLURL(), repo.ComposeMetas()) | 	desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: repo.HTMLURL(), | ||||||
|  | 		Metas:     repo.ComposeMetas(), | ||||||
|  | 	}, repo.Description) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) | 		log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) | ||||||
| 		return template.HTML(markup.Sanitize(repo.Description)) | 		return template.HTML(markup.Sanitize(repo.Description)) | ||||||
|  |  | ||||||
|  | @ -5,13 +5,14 @@ | ||||||
| package models | package models | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"code.gitea.io/gitea/modules/storage" | ||||||
| 	"code.gitea.io/gitea/modules/util" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/gobwas/glob" | 	"github.com/gobwas/glob" | ||||||
| ) | ) | ||||||
|  | @ -49,9 +50,9 @@ func (gt GiteaTemplate) Globs() []glob.Glob { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	gt.globs = make([]glob.Glob, 0) | 	gt.globs = make([]glob.Glob, 0) | ||||||
| 	lines := strings.Split(string(util.NormalizeEOL(gt.Content)), "\n") | 	scanner := bufio.NewScanner(bytes.NewReader(gt.Content)) | ||||||
| 	for _, line := range lines { | 	for scanner.Scan() { | ||||||
| 		line = strings.TrimSpace(line) | 		line := strings.TrimSpace(scanner.Text()) | ||||||
| 		if line == "" || strings.HasPrefix(line, "#") { | 		if line == "" || strings.HasPrefix(line, "#") { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ package charset | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"unicode/utf8" | 	"unicode/utf8" | ||||||
| 
 | 
 | ||||||
|  | @ -21,6 +23,33 @@ import ( | ||||||
| // UTF8BOM is the utf-8 byte-order marker
 | // UTF8BOM is the utf-8 byte-order marker
 | ||||||
| var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'} | var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'} | ||||||
| 
 | 
 | ||||||
|  | // ToUTF8WithFallbackReader detects the encoding of content and coverts to UTF-8 reader if possible
 | ||||||
|  | func ToUTF8WithFallbackReader(rd io.Reader) io.Reader { | ||||||
|  | 	var buf = make([]byte, 2048) | ||||||
|  | 	n, err := rd.Read(buf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return rd | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	charsetLabel, err := DetectEncoding(buf[:n]) | ||||||
|  | 	if err != nil || charsetLabel == "UTF-8" { | ||||||
|  | 		return io.MultiReader(bytes.NewReader(RemoveBOMIfPresent(buf[:n])), rd) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	encoding, _ := charset.Lookup(charsetLabel) | ||||||
|  | 	if encoding == nil { | ||||||
|  | 		return io.MultiReader(bytes.NewReader(buf[:n]), rd) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return transform.NewReader( | ||||||
|  | 		io.MultiReader( | ||||||
|  | 			bytes.NewReader(RemoveBOMIfPresent(buf[:n])), | ||||||
|  | 			rd, | ||||||
|  | 		), | ||||||
|  | 		encoding.NewDecoder(), | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ToUTF8WithErr converts content to UTF8 encoding
 | // ToUTF8WithErr converts content to UTF8 encoding
 | ||||||
| func ToUTF8WithErr(content []byte) (string, error) { | func ToUTF8WithErr(content []byte) (string, error) { | ||||||
| 	charsetLabel, err := DetectEncoding(content) | 	charsetLabel, err := DetectEncoding(content) | ||||||
|  | @ -49,24 +78,8 @@ func ToUTF8WithErr(content []byte) (string, error) { | ||||||
| 
 | 
 | ||||||
| // ToUTF8WithFallback detects the encoding of content and coverts to UTF-8 if possible
 | // ToUTF8WithFallback detects the encoding of content and coverts to UTF-8 if possible
 | ||||||
| func ToUTF8WithFallback(content []byte) []byte { | func ToUTF8WithFallback(content []byte) []byte { | ||||||
| 	charsetLabel, err := DetectEncoding(content) | 	bs, _ := ioutil.ReadAll(ToUTF8WithFallbackReader(bytes.NewReader(content))) | ||||||
| 	if err != nil || charsetLabel == "UTF-8" { | 	return bs | ||||||
| 		return RemoveBOMIfPresent(content) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	encoding, _ := charset.Lookup(charsetLabel) |  | ||||||
| 	if encoding == nil { |  | ||||||
| 		return content |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// If there is an error, we concatenate the nicely decoded part and the
 |  | ||||||
| 	// original left over. This way we won't lose data.
 |  | ||||||
| 	result, n, err := transform.Bytes(encoding.NewDecoder(), content) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return append(result, content[n:]...) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return RemoveBOMIfPresent(result) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // ToUTF8 converts content to UTF8 encoding and ignore error
 | // ToUTF8 converts content to UTF8 encoding and ignore error
 | ||||||
|  |  | ||||||
|  | @ -7,7 +7,9 @@ package csv | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/csv" | 	"encoding/csv" | ||||||
|  | 	stdcsv "encoding/csv" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"io" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
|  | @ -18,17 +20,31 @@ import ( | ||||||
| var quoteRegexp = regexp.MustCompile(`["'][\s\S]+?["']`) | var quoteRegexp = regexp.MustCompile(`["'][\s\S]+?["']`) | ||||||
| 
 | 
 | ||||||
| // CreateReader creates a csv.Reader with the given delimiter.
 | // CreateReader creates a csv.Reader with the given delimiter.
 | ||||||
| func CreateReader(rawBytes []byte, delimiter rune) *csv.Reader { | func CreateReader(input io.Reader, delimiter rune) *stdcsv.Reader { | ||||||
| 	rd := csv.NewReader(bytes.NewReader(rawBytes)) | 	rd := stdcsv.NewReader(input) | ||||||
| 	rd.Comma = delimiter | 	rd.Comma = delimiter | ||||||
| 	rd.TrimLeadingSpace = true | 	rd.TrimLeadingSpace = true | ||||||
| 	return rd | 	return rd | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CreateReaderAndGuessDelimiter tries to guess the field delimiter from the content and creates a csv.Reader.
 | // CreateReaderAndGuessDelimiter tries to guess the field delimiter from the content and creates a csv.Reader.
 | ||||||
| func CreateReaderAndGuessDelimiter(rawBytes []byte) *csv.Reader { | func CreateReaderAndGuessDelimiter(rd io.Reader) (*stdcsv.Reader, error) { | ||||||
| 	delimiter := guessDelimiter(rawBytes) | 	var data = make([]byte, 1e4) | ||||||
| 	return CreateReader(rawBytes, delimiter) | 	size, err := rd.Read(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	delimiter := guessDelimiter(data[:size]) | ||||||
|  | 
 | ||||||
|  | 	var newInput io.Reader | ||||||
|  | 	if size < 1e4 { | ||||||
|  | 		newInput = bytes.NewReader(data[:size]) | ||||||
|  | 	} else { | ||||||
|  | 		newInput = io.MultiReader(bytes.NewReader(data), rd) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return CreateReader(newInput, delimiter), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // guessDelimiter scores the input CSV data against delimiters, and returns the best match.
 | // guessDelimiter scores the input CSV data against delimiters, and returns the best match.
 | ||||||
|  |  | ||||||
|  | @ -5,20 +5,23 @@ | ||||||
| package csv | package csv | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestCreateReader(t *testing.T) { | func TestCreateReader(t *testing.T) { | ||||||
| 	rd := CreateReader([]byte{}, ',') | 	rd := CreateReader(bytes.NewReader([]byte{}), ',') | ||||||
| 	assert.Equal(t, ',', rd.Comma) | 	assert.Equal(t, ',', rd.Comma) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestCreateReaderAndGuessDelimiter(t *testing.T) { | func TestCreateReaderAndGuessDelimiter(t *testing.T) { | ||||||
| 	input := "a;b;c\n1;2;3\n4;5;6" | 	input := "a;b;c\n1;2;3\n4;5;6" | ||||||
| 
 | 
 | ||||||
| 	rd := CreateReaderAndGuessDelimiter([]byte(input)) | 	rd, err := CreateReaderAndGuessDelimiter(strings.NewReader(input)) | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, ';', rd.Comma) | 	assert.Equal(t, ';', rd.Comma) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,9 +5,11 @@ | ||||||
| package markup | package markup | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"html" | 	"html" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/csv" | 	"code.gitea.io/gitea/modules/csv" | ||||||
|  | @ -16,55 +18,89 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	markup.RegisterParser(Parser{}) | 	markup.RegisterRenderer(Renderer{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Parser implements markup.Parser for csv files
 | // Renderer implements markup.Renderer for csv files
 | ||||||
| type Parser struct { | type Renderer struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Name implements markup.Parser
 | // Name implements markup.Renderer
 | ||||||
| func (Parser) Name() string { | func (Renderer) Name() string { | ||||||
| 	return "csv" | 	return "csv" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NeedPostProcess implements markup.Parser
 | // NeedPostProcess implements markup.Renderer
 | ||||||
| func (Parser) NeedPostProcess() bool { return false } | func (Renderer) NeedPostProcess() bool { return false } | ||||||
| 
 | 
 | ||||||
| // Extensions implements markup.Parser
 | // Extensions implements markup.Renderer
 | ||||||
| func (Parser) Extensions() []string { | func (Renderer) Extensions() []string { | ||||||
| 	return []string{".csv", ".tsv"} | 	return []string{".csv", ".tsv"} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render implements markup.Parser
 | func writeField(w io.Writer, element, class, field string) error { | ||||||
| func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | 	if _, err := io.WriteString(w, "<"); err != nil { | ||||||
| 	var tmpBlock bytes.Buffer | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.WriteString(w, element); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if len(class) > 0 { | ||||||
|  | 		if _, err := io.WriteString(w, " class=\""); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if _, err := io.WriteString(w, class); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if _, err := io.WriteString(w, "\""); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.WriteString(w, ">"); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.WriteString(w, html.EscapeString(field)); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.WriteString(w, "</"); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.WriteString(w, element); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	_, err := io.WriteString(w, ">") | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Render implements markup.Renderer
 | ||||||
|  | func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
|  | 	var tmpBlock = bufio.NewWriter(output) | ||||||
|  | 
 | ||||||
|  | 	// FIXME: don't read all to memory
 | ||||||
|  | 	rawBytes, err := ioutil.ReadAll(input) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) { | 	if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) { | ||||||
| 		tmpBlock.WriteString("<pre>") | 		if _, err := tmpBlock.WriteString("<pre>"); err != nil { | ||||||
| 		tmpBlock.WriteString(html.EscapeString(string(rawBytes))) | 			return err | ||||||
| 		tmpBlock.WriteString("</pre>") |  | ||||||
| 		return tmpBlock.Bytes() |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	rd := csv.CreateReaderAndGuessDelimiter(rawBytes) |  | ||||||
| 
 |  | ||||||
| 	writeField := func(element, class, field string) { |  | ||||||
| 		tmpBlock.WriteString("<") |  | ||||||
| 		tmpBlock.WriteString(element) |  | ||||||
| 		if len(class) > 0 { |  | ||||||
| 			tmpBlock.WriteString(" class=\"") |  | ||||||
| 			tmpBlock.WriteString(class) |  | ||||||
| 			tmpBlock.WriteString("\"") |  | ||||||
| 		} | 		} | ||||||
| 		tmpBlock.WriteString(">") | 		if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil { | ||||||
| 		tmpBlock.WriteString(html.EscapeString(field)) | 			return err | ||||||
| 		tmpBlock.WriteString("</") | 		} | ||||||
| 		tmpBlock.WriteString(element) | 		_, err = tmpBlock.WriteString("</pre>") | ||||||
| 		tmpBlock.WriteString(">") | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	tmpBlock.WriteString(`<table class="data-table">`) | 	rd, err := csv.CreateReaderAndGuessDelimiter(bytes.NewReader(rawBytes)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := tmpBlock.WriteString(`<table class="data-table">`); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
| 	row := 1 | 	row := 1 | ||||||
| 	for { | 	for { | ||||||
| 		fields, err := rd.Read() | 		fields, err := rd.Read() | ||||||
|  | @ -74,20 +110,29 @@ func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 		tmpBlock.WriteString("<tr>") | 		if _, err := tmpBlock.WriteString("<tr>"); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
| 		element := "td" | 		element := "td" | ||||||
| 		if row == 1 { | 		if row == 1 { | ||||||
| 			element = "th" | 			element = "th" | ||||||
| 		} | 		} | ||||||
| 		writeField(element, "line-num", strconv.Itoa(row)) | 		if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil { | ||||||
| 		for _, field := range fields { | 			return err | ||||||
| 			writeField(element, "", field) | 		} | ||||||
|  | 		for _, field := range fields { | ||||||
|  | 			if err := writeField(tmpBlock, element, "", field); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if _, err := tmpBlock.WriteString("</tr>"); err != nil { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 		tmpBlock.WriteString("</tr>") |  | ||||||
| 
 | 
 | ||||||
| 		row++ | 		row++ | ||||||
| 	} | 	} | ||||||
| 	tmpBlock.WriteString("</table>") | 	if _, err = tmpBlock.WriteString("</table>"); err != nil { | ||||||
| 
 | 		return err | ||||||
| 	return tmpBlock.Bytes() | 	} | ||||||
|  | 	return tmpBlock.Flush() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,13 +5,16 @@ | ||||||
| package markup | package markup | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
|  | 
 | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestRenderCSV(t *testing.T) { | func TestRenderCSV(t *testing.T) { | ||||||
| 	var parser Parser | 	var render Renderer | ||||||
| 	var kases = map[string]string{ | 	var kases = map[string]string{ | ||||||
| 		"a":        "<table class=\"data-table\"><tr><th class=\"line-num\">1</th><th>a</th></tr></table>", | 		"a":        "<table class=\"data-table\"><tr><th class=\"line-num\">1</th><th>a</th></tr></table>", | ||||||
| 		"1,2":      "<table class=\"data-table\"><tr><th class=\"line-num\">1</th><th>1</th><th>2</th></tr></table>", | 		"1,2":      "<table class=\"data-table\"><tr><th class=\"line-num\">1</th><th>1</th><th>2</th></tr></table>", | ||||||
|  | @ -20,7 +23,9 @@ func TestRenderCSV(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for k, v := range kases { | 	for k, v := range kases { | ||||||
| 		res := parser.Render([]byte(k), "", nil, false) | 		var buf strings.Builder | ||||||
| 		assert.EqualValues(t, v, string(res)) | 		err := render.Render(&markup.RenderContext{}, strings.NewReader(k), &buf) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.EqualValues(t, v, buf.String()) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										60
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										60
									
								
								modules/markup/external/external.go
									
									
									
									
										vendored
									
									
								
							|  | @ -5,7 +5,7 @@ | ||||||
| package external | package external | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"os" | 	"os" | ||||||
|  | @ -19,32 +19,32 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // RegisterParsers registers all supported third part parsers according settings
 | // RegisterRenderers registers all supported third part renderers according settings
 | ||||||
| func RegisterParsers() { | func RegisterRenderers() { | ||||||
| 	for _, parser := range setting.ExternalMarkupParsers { | 	for _, renderer := range setting.ExternalMarkupRenderers { | ||||||
| 		if parser.Enabled && parser.Command != "" && len(parser.FileExtensions) > 0 { | 		if renderer.Enabled && renderer.Command != "" && len(renderer.FileExtensions) > 0 { | ||||||
| 			markup.RegisterParser(&Parser{parser}) | 			markup.RegisterRenderer(&Renderer{renderer}) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Parser implements markup.Parser for external tools
 | // Renderer implements markup.Renderer for external tools
 | ||||||
| type Parser struct { | type Renderer struct { | ||||||
| 	setting.MarkupParser | 	setting.MarkupRenderer | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Name returns the external tool name
 | // Name returns the external tool name
 | ||||||
| func (p *Parser) Name() string { | func (p *Renderer) Name() string { | ||||||
| 	return p.MarkupName | 	return p.MarkupName | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NeedPostProcess implements markup.Parser
 | // NeedPostProcess implements markup.Renderer
 | ||||||
| func (p *Parser) NeedPostProcess() bool { | func (p *Renderer) NeedPostProcess() bool { | ||||||
| 	return p.MarkupParser.NeedPostProcess | 	return p.MarkupRenderer.NeedPostProcess | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Extensions returns the supported extensions of the tool
 | // Extensions returns the supported extensions of the tool
 | ||||||
| func (p *Parser) Extensions() []string { | func (p *Renderer) Extensions() []string { | ||||||
| 	return p.FileExtensions | 	return p.FileExtensions | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -56,14 +56,10 @@ func envMark(envName string) string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render renders the data of the document to HTML via the external tool.
 | // Render renders the data of the document to HTML via the external tool.
 | ||||||
| func (p *Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	var ( | 	var ( | ||||||
| 		bs           []byte | 		urlRawPrefix = strings.Replace(ctx.URLPrefix, "/src/", "/raw/", 1) | ||||||
| 		buf          = bytes.NewBuffer(bs) | 		command      = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), ctx.URLPrefix, | ||||||
| 		rd           = bytes.NewReader(rawBytes) |  | ||||||
| 		urlRawPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) |  | ||||||
| 
 |  | ||||||
| 		command = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), urlPrefix, |  | ||||||
| 			envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command) | 			envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command) | ||||||
| 		commands = strings.Fields(command) | 		commands = strings.Fields(command) | ||||||
| 		args     = commands[1:] | 		args     = commands[1:] | ||||||
|  | @ -73,8 +69,7 @@ func (p *Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]stri | ||||||
| 		// write to temp file
 | 		// write to temp file
 | ||||||
| 		f, err := ioutil.TempFile("", "gitea_input") | 		f, err := ioutil.TempFile("", "gitea_input") | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("%s create temp file when rendering %s failed: %v", p.Name(), p.Command, err) | 			return fmt.Errorf("%s create temp file when rendering %s failed: %v", p.Name(), p.Command, err) | ||||||
| 			return []byte("") |  | ||||||
| 		} | 		} | ||||||
| 		tmpPath := f.Name() | 		tmpPath := f.Name() | ||||||
| 		defer func() { | 		defer func() { | ||||||
|  | @ -83,17 +78,15 @@ func (p *Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]stri | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
| 
 | 
 | ||||||
| 		_, err = io.Copy(f, rd) | 		_, err = io.Copy(f, input) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			f.Close() | 			f.Close() | ||||||
| 			log.Error("%s write data to temp file when rendering %s failed: %v", p.Name(), p.Command, err) | 			return fmt.Errorf("%s write data to temp file when rendering %s failed: %v", p.Name(), p.Command, err) | ||||||
| 			return []byte("") |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		err = f.Close() | 		err = f.Close() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error("%s close temp file when rendering %s failed: %v", p.Name(), p.Command, err) | 			return fmt.Errorf("%s close temp file when rendering %s failed: %v", p.Name(), p.Command, err) | ||||||
| 			return []byte("") |  | ||||||
| 		} | 		} | ||||||
| 		args = append(args, f.Name()) | 		args = append(args, f.Name()) | ||||||
| 	} | 	} | ||||||
|  | @ -101,16 +94,15 @@ func (p *Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]stri | ||||||
| 	cmd := exec.Command(commands[0], args...) | 	cmd := exec.Command(commands[0], args...) | ||||||
| 	cmd.Env = append( | 	cmd.Env = append( | ||||||
| 		os.Environ(), | 		os.Environ(), | ||||||
| 		"GITEA_PREFIX_SRC="+urlPrefix, | 		"GITEA_PREFIX_SRC="+ctx.URLPrefix, | ||||||
| 		"GITEA_PREFIX_RAW="+urlRawPrefix, | 		"GITEA_PREFIX_RAW="+urlRawPrefix, | ||||||
| 	) | 	) | ||||||
| 	if !p.IsInputFile { | 	if !p.IsInputFile { | ||||||
| 		cmd.Stdin = rd | 		cmd.Stdin = input | ||||||
| 	} | 	} | ||||||
| 	cmd.Stdout = buf | 	cmd.Stdout = output | ||||||
| 	if err := cmd.Run(); err != nil { | 	if err := cmd.Run(); err != nil { | ||||||
| 		log.Error("%s render run command %s %v failed: %v", p.Name(), commands[0], args, err) | 		return fmt.Errorf("%s render run command %s %v failed: %v", p.Name(), commands[0], args, err) | ||||||
| 		return []byte("") |  | ||||||
| 	} | 	} | ||||||
| 	return buf.Bytes() | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ package markup | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | @ -144,7 +146,7 @@ func (p *postProcessError) Error() string { | ||||||
| 	return "PostProcess: " + p.context + ", " + p.err.Error() | 	return "PostProcess: " + p.context + ", " + p.err.Error() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type processor func(ctx *postProcessCtx, node *html.Node) | type processor func(ctx *RenderContext, node *html.Node) | ||||||
| 
 | 
 | ||||||
| var defaultProcessors = []processor{ | var defaultProcessors = []processor{ | ||||||
| 	fullIssuePatternProcessor, | 	fullIssuePatternProcessor, | ||||||
|  | @ -159,34 +161,17 @@ var defaultProcessors = []processor{ | ||||||
| 	emojiShortCodeProcessor, | 	emojiShortCodeProcessor, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type postProcessCtx struct { |  | ||||||
| 	metas          map[string]string |  | ||||||
| 	urlPrefix      string |  | ||||||
| 	isWikiMarkdown bool |  | ||||||
| 
 |  | ||||||
| 	// processors used by this context.
 |  | ||||||
| 	procs []processor |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // PostProcess does the final required transformations to the passed raw HTML
 | // PostProcess does the final required transformations to the passed raw HTML
 | ||||||
| // data, and ensures its validity. Transformations include: replacing links and
 | // data, and ensures its validity. Transformations include: replacing links and
 | ||||||
| // emails with HTML links, parsing shortlinks in the format of [[Link]], like
 | // emails with HTML links, parsing shortlinks in the format of [[Link]], like
 | ||||||
| // MediaWiki, linking issues in the format #ID, and mentions in the format
 | // MediaWiki, linking issues in the format #ID, and mentions in the format
 | ||||||
| // @user, and others.
 | // @user, and others.
 | ||||||
| func PostProcess( | func PostProcess( | ||||||
| 	rawHTML []byte, | 	ctx *RenderContext, | ||||||
| 	urlPrefix string, | 	input io.Reader, | ||||||
| 	metas map[string]string, | 	output io.Writer, | ||||||
| 	isWikiMarkdown bool, | ) error { | ||||||
| ) ([]byte, error) { | 	return postProcess(ctx, defaultProcessors, input, output) | ||||||
| 	// create the context from the parameters
 |  | ||||||
| 	ctx := &postProcessCtx{ |  | ||||||
| 		metas:          metas, |  | ||||||
| 		urlPrefix:      urlPrefix, |  | ||||||
| 		isWikiMarkdown: isWikiMarkdown, |  | ||||||
| 		procs:          defaultProcessors, |  | ||||||
| 	} |  | ||||||
| 	return ctx.postProcess(rawHTML) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var commitMessageProcessors = []processor{ | var commitMessageProcessors = []processor{ | ||||||
|  | @ -205,23 +190,18 @@ var commitMessageProcessors = []processor{ | ||||||
| // the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
 | // the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
 | ||||||
| // set, which changes every text node into a link to the passed default link.
 | // set, which changes every text node into a link to the passed default link.
 | ||||||
| func RenderCommitMessage( | func RenderCommitMessage( | ||||||
| 	rawHTML []byte, | 	ctx *RenderContext, | ||||||
| 	urlPrefix, defaultLink string, | 	content string, | ||||||
| 	metas map[string]string, | ) (string, error) { | ||||||
| ) ([]byte, error) { | 	var procs = commitMessageProcessors | ||||||
| 	ctx := &postProcessCtx{ | 	if ctx.DefaultLink != "" { | ||||||
| 		metas:     metas, |  | ||||||
| 		urlPrefix: urlPrefix, |  | ||||||
| 		procs:     commitMessageProcessors, |  | ||||||
| 	} |  | ||||||
| 	if defaultLink != "" { |  | ||||||
| 		// we don't have to fear data races, because being
 | 		// we don't have to fear data races, because being
 | ||||||
| 		// commitMessageProcessors of fixed len and cap, every time we append
 | 		// commitMessageProcessors of fixed len and cap, every time we append
 | ||||||
| 		// something to it the slice is realloc+copied, so append always
 | 		// something to it the slice is realloc+copied, so append always
 | ||||||
| 		// generates the slice ex-novo.
 | 		// generates the slice ex-novo.
 | ||||||
| 		ctx.procs = append(ctx.procs, genDefaultLinkProcessor(defaultLink)) | 		procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) | ||||||
| 	} | 	} | ||||||
| 	return ctx.postProcess(rawHTML) | 	return renderProcessString(ctx, procs, content) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var commitMessageSubjectProcessors = []processor{ | var commitMessageSubjectProcessors = []processor{ | ||||||
|  | @ -245,83 +225,72 @@ var emojiProcessors = []processor{ | ||||||
| // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
 | // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
 | ||||||
| // which changes every text node into a link to the passed default link.
 | // which changes every text node into a link to the passed default link.
 | ||||||
| func RenderCommitMessageSubject( | func RenderCommitMessageSubject( | ||||||
| 	rawHTML []byte, | 	ctx *RenderContext, | ||||||
| 	urlPrefix, defaultLink string, | 	content string, | ||||||
| 	metas map[string]string, | ) (string, error) { | ||||||
| ) ([]byte, error) { | 	var procs = commitMessageSubjectProcessors | ||||||
| 	ctx := &postProcessCtx{ | 	if ctx.DefaultLink != "" { | ||||||
| 		metas:     metas, |  | ||||||
| 		urlPrefix: urlPrefix, |  | ||||||
| 		procs:     commitMessageSubjectProcessors, |  | ||||||
| 	} |  | ||||||
| 	if defaultLink != "" { |  | ||||||
| 		// we don't have to fear data races, because being
 | 		// we don't have to fear data races, because being
 | ||||||
| 		// commitMessageSubjectProcessors of fixed len and cap, every time we
 | 		// commitMessageSubjectProcessors of fixed len and cap, every time we
 | ||||||
| 		// append something to it the slice is realloc+copied, so append always
 | 		// append something to it the slice is realloc+copied, so append always
 | ||||||
| 		// generates the slice ex-novo.
 | 		// generates the slice ex-novo.
 | ||||||
| 		ctx.procs = append(ctx.procs, genDefaultLinkProcessor(defaultLink)) | 		procs = append(procs, genDefaultLinkProcessor(ctx.DefaultLink)) | ||||||
| 	} | 	} | ||||||
| 	return ctx.postProcess(rawHTML) | 	return renderProcessString(ctx, procs, content) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderIssueTitle to process title on individual issue/pull page
 | // RenderIssueTitle to process title on individual issue/pull page
 | ||||||
| func RenderIssueTitle( | func RenderIssueTitle( | ||||||
| 	rawHTML []byte, | 	ctx *RenderContext, | ||||||
| 	urlPrefix string, | 	title string, | ||||||
| 	metas map[string]string, | ) (string, error) { | ||||||
| ) ([]byte, error) { | 	return renderProcessString(ctx, []processor{ | ||||||
| 	ctx := &postProcessCtx{ | 		issueIndexPatternProcessor, | ||||||
| 		metas:     metas, | 		sha1CurrentPatternProcessor, | ||||||
| 		urlPrefix: urlPrefix, | 		emojiShortCodeProcessor, | ||||||
| 		procs: []processor{ | 		emojiProcessor, | ||||||
| 			issueIndexPatternProcessor, | 	}, title) | ||||||
| 			sha1CurrentPatternProcessor, | } | ||||||
| 			emojiShortCodeProcessor, | 
 | ||||||
| 			emojiProcessor, | func renderProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { | ||||||
| 		}, | 	var buf strings.Builder | ||||||
|  | 	if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { | ||||||
|  | 		return "", err | ||||||
| 	} | 	} | ||||||
| 	return ctx.postProcess(rawHTML) | 	return buf.String(), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderDescriptionHTML will use similar logic as PostProcess, but will
 | // RenderDescriptionHTML will use similar logic as PostProcess, but will
 | ||||||
| // use a single special linkProcessor.
 | // use a single special linkProcessor.
 | ||||||
| func RenderDescriptionHTML( | func RenderDescriptionHTML( | ||||||
| 	rawHTML []byte, | 	ctx *RenderContext, | ||||||
| 	urlPrefix string, | 	content string, | ||||||
| 	metas map[string]string, | ) (string, error) { | ||||||
| ) ([]byte, error) { | 	return renderProcessString(ctx, []processor{ | ||||||
| 	ctx := &postProcessCtx{ | 		descriptionLinkProcessor, | ||||||
| 		metas:     metas, | 		emojiShortCodeProcessor, | ||||||
| 		urlPrefix: urlPrefix, | 		emojiProcessor, | ||||||
| 		procs: []processor{ | 	}, content) | ||||||
| 			descriptionLinkProcessor, |  | ||||||
| 			emojiShortCodeProcessor, |  | ||||||
| 			emojiProcessor, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	return ctx.postProcess(rawHTML) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderEmoji for when we want to just process emoji and shortcodes
 | // RenderEmoji for when we want to just process emoji and shortcodes
 | ||||||
| // in various places it isn't already run through the normal markdown procesor
 | // in various places it isn't already run through the normal markdown procesor
 | ||||||
| func RenderEmoji( | func RenderEmoji( | ||||||
| 	rawHTML []byte, | 	content string, | ||||||
| ) ([]byte, error) { | ) (string, error) { | ||||||
| 	ctx := &postProcessCtx{ | 	return renderProcessString(&RenderContext{}, emojiProcessors, content) | ||||||
| 		procs: emojiProcessors, |  | ||||||
| 	} |  | ||||||
| 	return ctx.postProcess(rawHTML) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) | var tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`) | ||||||
| var nulCleaner = strings.NewReplacer("\000", "") | var nulCleaner = strings.NewReplacer("\000", "") | ||||||
| 
 | 
 | ||||||
| func (ctx *postProcessCtx) postProcess(rawHTML []byte) ([]byte, error) { | func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { | ||||||
| 	if ctx.procs == nil { | 	// FIXME: don't read all content to memory
 | ||||||
| 		ctx.procs = defaultProcessors | 	rawHTML, err := ioutil.ReadAll(input) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// give a generous extra 50 bytes
 |  | ||||||
| 	res := bytes.NewBuffer(make([]byte, 0, len(rawHTML)+50)) | 	res := bytes.NewBuffer(make([]byte, 0, len(rawHTML)+50)) | ||||||
| 	// prepend "<html><body>"
 | 	// prepend "<html><body>"
 | ||||||
| 	_, _ = res.WriteString("<html><body>") | 	_, _ = res.WriteString("<html><body>") | ||||||
|  | @ -335,11 +304,11 @@ func (ctx *postProcessCtx) postProcess(rawHTML []byte) ([]byte, error) { | ||||||
| 	// parse the HTML
 | 	// parse the HTML
 | ||||||
| 	nodes, err := html.ParseFragment(res, nil) | 	nodes, err := html.ParseFragment(res, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, &postProcessError{"invalid HTML", err} | 		return &postProcessError{"invalid HTML", err} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, node := range nodes { | 	for _, node := range nodes { | ||||||
| 		ctx.visitNode(node, true) | 		visitNode(ctx, procs, node, true) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	newNodes := make([]*html.Node, 0, len(nodes)) | 	newNodes := make([]*html.Node, 0, len(nodes)) | ||||||
|  | @ -365,25 +334,17 @@ func (ctx *postProcessCtx) postProcess(rawHTML []byte) ([]byte, error) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	nodes = newNodes |  | ||||||
| 
 |  | ||||||
| 	// Create buffer in which the data will be placed again. We know that the
 |  | ||||||
| 	// length will be at least that of res; to spare a few alloc+copy, we
 |  | ||||||
| 	// reuse res, resetting its length to 0.
 |  | ||||||
| 	res.Reset() |  | ||||||
| 	// Render everything to buf.
 | 	// Render everything to buf.
 | ||||||
| 	for _, node := range nodes { | 	for _, node := range newNodes { | ||||||
| 		err = html.Render(res, node) | 		err = html.Render(output, node) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, &postProcessError{"error rendering processed HTML", err} | 			return &postProcessError{"error rendering processed HTML", err} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 	return nil | ||||||
| 	// Everything done successfully, return parsed data.
 |  | ||||||
| 	return res.Bytes(), nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | func visitNode(ctx *RenderContext, procs []processor, node *html.Node, visitText bool) { | ||||||
| 	// Add user-content- to IDs if they don't already have them
 | 	// Add user-content- to IDs if they don't already have them
 | ||||||
| 	for idx, attr := range node.Attr { | 	for idx, attr := range node.Attr { | ||||||
| 		if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) { | 		if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) { | ||||||
|  | @ -399,7 +360,7 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | ||||||
| 	switch node.Type { | 	switch node.Type { | ||||||
| 	case html.TextNode: | 	case html.TextNode: | ||||||
| 		if visitText { | 		if visitText { | ||||||
| 			ctx.textNode(node) | 			textNode(ctx, procs, node) | ||||||
| 		} | 		} | ||||||
| 	case html.ElementNode: | 	case html.ElementNode: | ||||||
| 		if node.Data == "img" { | 		if node.Data == "img" { | ||||||
|  | @ -410,8 +371,8 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | ||||||
| 				} | 				} | ||||||
| 				link := []byte(attr.Val) | 				link := []byte(attr.Val) | ||||||
| 				if len(link) > 0 && !IsLink(link) { | 				if len(link) > 0 && !IsLink(link) { | ||||||
| 					prefix := ctx.urlPrefix | 					prefix := ctx.URLPrefix | ||||||
| 					if ctx.isWikiMarkdown { | 					if ctx.IsWiki { | ||||||
| 						prefix = util.URLJoin(prefix, "wiki", "raw") | 						prefix = util.URLJoin(prefix, "wiki", "raw") | ||||||
| 					} | 					} | ||||||
| 					prefix = strings.Replace(prefix, "/src/", "/media/", 1) | 					prefix = strings.Replace(prefix, "/src/", "/media/", 1) | ||||||
|  | @ -449,7 +410,7 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		for n := node.FirstChild; n != nil; n = n.NextSibling { | 		for n := node.FirstChild; n != nil; n = n.NextSibling { | ||||||
| 			ctx.visitNode(n, visitText) | 			visitNode(ctx, procs, n, visitText) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	// ignore everything else
 | 	// ignore everything else
 | ||||||
|  | @ -457,8 +418,8 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) { | ||||||
| 
 | 
 | ||||||
| // textNode runs the passed node through various processors, in order to handle
 | // textNode runs the passed node through various processors, in order to handle
 | ||||||
| // all kinds of special links handled by the post-processing.
 | // all kinds of special links handled by the post-processing.
 | ||||||
| func (ctx *postProcessCtx) textNode(node *html.Node) { | func textNode(ctx *RenderContext, procs []processor, node *html.Node) { | ||||||
| 	for _, processor := range ctx.procs { | 	for _, processor := range procs { | ||||||
| 		processor(ctx, node) | 		processor(ctx, node) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -609,7 +570,7 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func mentionProcessor(ctx *postProcessCtx, node *html.Node) { | func mentionProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	// We replace only the first mention; other mentions will be addressed later
 | 	// We replace only the first mention; other mentions will be addressed later
 | ||||||
| 	found, loc := references.FindFirstMentionBytes([]byte(node.Data)) | 	found, loc := references.FindFirstMentionBytes([]byte(node.Data)) | ||||||
| 	if !found { | 	if !found { | ||||||
|  | @ -617,26 +578,26 @@ func mentionProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	} | 	} | ||||||
| 	mention := node.Data[loc.Start:loc.End] | 	mention := node.Data[loc.Start:loc.End] | ||||||
| 	var teams string | 	var teams string | ||||||
| 	teams, ok := ctx.metas["teams"] | 	teams, ok := ctx.Metas["teams"] | ||||||
| 	// FIXME: util.URLJoin may not be necessary here:
 | 	// FIXME: util.URLJoin may not be necessary here:
 | ||||||
| 	// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | 	// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | ||||||
| 	// is an AppSubURL link we can probably fallback to concatenation.
 | 	// is an AppSubURL link we can probably fallback to concatenation.
 | ||||||
| 	// team mention should follow @orgName/teamName style
 | 	// team mention should follow @orgName/teamName style
 | ||||||
| 	if ok && strings.Contains(mention, "/") { | 	if ok && strings.Contains(mention, "/") { | ||||||
| 		mentionOrgAndTeam := strings.Split(mention, "/") | 		mentionOrgAndTeam := strings.Split(mention, "/") | ||||||
| 		if mentionOrgAndTeam[0][1:] == ctx.metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { | 		if mentionOrgAndTeam[0][1:] == ctx.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { | ||||||
| 			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) | 			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.Metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention")) | ||||||
| 		} | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) | 	replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention")) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) { | func shortLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	shortLinkProcessorFull(ctx, node, false) | 	shortLinkProcessorFull(ctx, node, false) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) { | func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) { | ||||||
| 	m := shortLinkPattern.FindStringSubmatchIndex(node.Data) | 	m := shortLinkPattern.FindStringSubmatchIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  | @ -741,13 +702,13 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) { | ||||||
| 			link = url.PathEscape(link) | 			link = url.PathEscape(link) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	urlPrefix := ctx.urlPrefix | 	urlPrefix := ctx.URLPrefix | ||||||
| 	if image { | 	if image { | ||||||
| 		if !absoluteLink { | 		if !absoluteLink { | ||||||
| 			if IsSameDomain(urlPrefix) { | 			if IsSameDomain(urlPrefix) { | ||||||
| 				urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) | 				urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1) | ||||||
| 			} | 			} | ||||||
| 			if ctx.isWikiMarkdown { | 			if ctx.IsWiki { | ||||||
| 				link = util.URLJoin("wiki", "raw", link) | 				link = util.URLJoin("wiki", "raw", link) | ||||||
| 			} | 			} | ||||||
| 			link = util.URLJoin(urlPrefix, link) | 			link = util.URLJoin(urlPrefix, link) | ||||||
|  | @ -778,7 +739,7 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) { | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		if !absoluteLink { | 		if !absoluteLink { | ||||||
| 			if ctx.isWikiMarkdown { | 			if ctx.IsWiki { | ||||||
| 				link = util.URLJoin("wiki", link) | 				link = util.URLJoin("wiki", link) | ||||||
| 			} | 			} | ||||||
| 			link = util.URLJoin(urlPrefix, link) | 			link = util.URLJoin(urlPrefix, link) | ||||||
|  | @ -794,8 +755,8 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) { | ||||||
| 	replaceContent(node, m[0], m[1], linkNode) | 	replaceContent(node, m[0], m[1], linkNode) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) { | func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) | 	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data) | ||||||
|  | @ -811,7 +772,7 @@ func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	matchOrg := linkParts[len(linkParts)-4] | 	matchOrg := linkParts[len(linkParts)-4] | ||||||
| 	matchRepo := linkParts[len(linkParts)-3] | 	matchRepo := linkParts[len(linkParts)-3] | ||||||
| 
 | 
 | ||||||
| 	if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] { | 	if matchOrg == ctx.Metas["user"] && matchRepo == ctx.Metas["repo"] { | ||||||
| 		// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | 		// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | ||||||
| 		// and we should indicate that in the text somehow
 | 		// and we should indicate that in the text somehow
 | ||||||
| 		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue")) | 		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue")) | ||||||
|  | @ -822,8 +783,8 @@ func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -832,8 +793,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 		ref   *references.RenderizableReference | 		ref   *references.RenderizableReference | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| 	_, exttrack := ctx.metas["format"] | 	_, exttrack := ctx.Metas["format"] | ||||||
| 	alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric | 	alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric | ||||||
| 
 | 
 | ||||||
| 	// Repos with external issue trackers might still need to reference local PRs
 | 	// Repos with external issue trackers might still need to reference local PRs
 | ||||||
| 	// We need to concern with the first one that shows up in the text, whichever it is
 | 	// We need to concern with the first one that shows up in the text, whichever it is
 | ||||||
|  | @ -853,8 +814,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	var link *html.Node | 	var link *html.Node | ||||||
| 	reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] | 	reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End] | ||||||
| 	if exttrack && !ref.IsPull { | 	if exttrack && !ref.IsPull { | ||||||
| 		ctx.metas["index"] = ref.Issue | 		ctx.Metas["index"] = ref.Issue | ||||||
| 		link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue") | 		link = createLink(com.Expand(ctx.Metas["format"], ctx.Metas), reftext, "ref-issue") | ||||||
| 	} else { | 	} else { | ||||||
| 		// Path determines the type of link that will be rendered. It's unknown at this point whether
 | 		// Path determines the type of link that will be rendered. It's unknown at this point whether
 | ||||||
| 		// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 | 		// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 | ||||||
|  | @ -864,7 +825,7 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 			path = "pulls" | 			path = "pulls" | ||||||
| 		} | 		} | ||||||
| 		if ref.Owner == "" { | 		if ref.Owner == "" { | ||||||
| 			link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "ref-issue") | 			link = createLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], path, ref.Issue), reftext, "ref-issue") | ||||||
| 		} else { | 		} else { | ||||||
| 			link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") | 			link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue") | ||||||
| 		} | 		} | ||||||
|  | @ -893,8 +854,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // fullSha1PatternProcessor renders SHA containing URLs
 | // fullSha1PatternProcessor renders SHA containing URLs
 | ||||||
| func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) { | func fullSha1PatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.metas == nil { | 	if ctx.Metas == nil { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := anySHA1Pattern.FindStringSubmatchIndex(node.Data) | 	m := anySHA1Pattern.FindStringSubmatchIndex(node.Data) | ||||||
|  | @ -944,8 +905,7 @@ func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emojiShortCodeProcessor for rendering text like :smile: into emoji
 | // emojiShortCodeProcessor for rendering text like :smile: into emoji
 | ||||||
| func emojiShortCodeProcessor(ctx *postProcessCtx, node *html.Node) { | func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 
 |  | ||||||
| 	m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data) | 	m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  | @ -968,7 +928,7 @@ func emojiShortCodeProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emoji processor to match emoji and add emoji class
 | // emoji processor to match emoji and add emoji class
 | ||||||
| func emojiProcessor(ctx *postProcessCtx, node *html.Node) { | func emojiProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := emoji.FindEmojiSubmatchIndex(node.Data) | 	m := emoji.FindEmojiSubmatchIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  | @ -983,8 +943,8 @@ func emojiProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 
 | 
 | ||||||
| // sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that
 | // sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that
 | ||||||
| // are assumed to be in the same repository.
 | // are assumed to be in the same repository.
 | ||||||
| func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) { | func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	if ctx.metas == nil || ctx.metas["user"] == "" || ctx.metas["repo"] == "" || ctx.metas["repoPath"] == "" { | 	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data) | 	m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data) | ||||||
|  | @ -1000,7 +960,7 @@ func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	// as used by git and github for linking and thus we have to do similar.
 | 	// as used by git and github for linking and thus we have to do similar.
 | ||||||
| 	// Because of this, we check to make sure that a matched hash is actually
 | 	// Because of this, we check to make sure that a matched hash is actually
 | ||||||
| 	// a commit in the repository before making it a link.
 | 	// a commit in the repository before making it a link.
 | ||||||
| 	if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.metas["repoPath"]); err != nil { | 	if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.Metas["repoPath"]); err != nil { | ||||||
| 		if !strings.Contains(err.Error(), "fatal: Needed a single revision") { | 		if !strings.Contains(err.Error(), "fatal: Needed a single revision") { | ||||||
| 			log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err) | 			log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err) | ||||||
| 		} | 		} | ||||||
|  | @ -1008,11 +968,11 @@ func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	replaceContent(node, m[2], m[3], | 	replaceContent(node, m[2], m[3], | ||||||
| 		createCodeLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "commit", hash), base.ShortSha(hash), "commit")) | 		createCodeLink(util.URLJoin(setting.AppURL, ctx.Metas["user"], ctx.Metas["repo"], "commit", hash), base.ShortSha(hash), "commit")) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emailAddressProcessor replaces raw email addresses with a mailto: link.
 | // emailAddressProcessor replaces raw email addresses with a mailto: link.
 | ||||||
| func emailAddressProcessor(ctx *postProcessCtx, node *html.Node) { | func emailAddressProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := emailRegex.FindStringSubmatchIndex(node.Data) | 	m := emailRegex.FindStringSubmatchIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  | @ -1023,7 +983,7 @@ func emailAddressProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| 
 | 
 | ||||||
| // linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | // linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | ||||||
| // markdown.
 | // markdown.
 | ||||||
| func linkProcessor(ctx *postProcessCtx, node *html.Node) { | func linkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := common.LinkRegex.FindStringIndex(node.Data) | 	m := common.LinkRegex.FindStringIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  | @ -1033,7 +993,7 @@ func linkProcessor(ctx *postProcessCtx, node *html.Node) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func genDefaultLinkProcessor(defaultLink string) processor { | func genDefaultLinkProcessor(defaultLink string) processor { | ||||||
| 	return func(ctx *postProcessCtx, node *html.Node) { | 	return func(ctx *RenderContext, node *html.Node) { | ||||||
| 		ch := &html.Node{ | 		ch := &html.Node{ | ||||||
| 			Parent: node, | 			Parent: node, | ||||||
| 			Type:   html.TextNode, | 			Type:   html.TextNode, | ||||||
|  | @ -1052,7 +1012,7 @@ func genDefaultLinkProcessor(defaultLink string) processor { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // descriptionLinkProcessor creates links for DescriptionHTML
 | // descriptionLinkProcessor creates links for DescriptionHTML
 | ||||||
| func descriptionLinkProcessor(ctx *postProcessCtx, node *html.Node) { | func descriptionLinkProcessor(ctx *RenderContext, node *html.Node) { | ||||||
| 	m := common.LinkRegex.FindStringIndex(node.Data) | 	m := common.LinkRegex.FindStringIndex(node.Data) | ||||||
| 	if m == nil { | 	if m == nil { | ||||||
| 		return | 		return | ||||||
|  |  | ||||||
|  | @ -61,8 +61,8 @@ var localMetas = map[string]string{ | ||||||
| func TestRender_IssueIndexPattern(t *testing.T) { | func TestRender_IssueIndexPattern(t *testing.T) { | ||||||
| 	// numeric: render inputs without valid mentions
 | 	// numeric: render inputs without valid mentions
 | ||||||
| 	test := func(s string) { | 	test := func(s string) { | ||||||
| 		testRenderIssueIndexPattern(t, s, s, nil) | 		testRenderIssueIndexPattern(t, s, s, &RenderContext{}) | ||||||
| 		testRenderIssueIndexPattern(t, s, s, &postProcessCtx{metas: numericMetas}) | 		testRenderIssueIndexPattern(t, s, s, &RenderContext{Metas: numericMetas}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// should not render anything when there are no mentions
 | 	// should not render anything when there are no mentions
 | ||||||
|  | @ -109,13 +109,13 @@ func TestRender_IssueIndexPattern2(t *testing.T) { | ||||||
| 			links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, path), "ref-issue", index, marker) | 			links[i] = numericIssueLink(util.URLJoin(setting.AppSubURL, path), "ref-issue", index, marker) | ||||||
| 		} | 		} | ||||||
| 		expectedNil := fmt.Sprintf(expectedFmt, links...) | 		expectedNil := fmt.Sprintf(expectedFmt, links...) | ||||||
| 		testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas}) | 		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{Metas: localMetas}) | ||||||
| 
 | 
 | ||||||
| 		for i, index := range indices { | 		for i, index := range indices { | ||||||
| 			links[i] = numericIssueLink(prefix, "ref-issue", index, marker) | 			links[i] = numericIssueLink(prefix, "ref-issue", index, marker) | ||||||
| 		} | 		} | ||||||
| 		expectedNum := fmt.Sprintf(expectedFmt, links...) | 		expectedNum := fmt.Sprintf(expectedFmt, links...) | ||||||
| 		testRenderIssueIndexPattern(t, s, expectedNum, &postProcessCtx{metas: numericMetas}) | 		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{Metas: numericMetas}) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// should render freestanding mentions
 | 	// should render freestanding mentions
 | ||||||
|  | @ -150,7 +150,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	// alphanumeric: render inputs without valid mentions
 | 	// alphanumeric: render inputs without valid mentions
 | ||||||
| 	test := func(s string) { | 	test := func(s string) { | ||||||
| 		testRenderIssueIndexPattern(t, s, s, &postProcessCtx{metas: alphanumericMetas}) | 		testRenderIssueIndexPattern(t, s, s, &RenderContext{Metas: alphanumericMetas}) | ||||||
| 	} | 	} | ||||||
| 	test("") | 	test("") | ||||||
| 	test("this is a test") | 	test("this is a test") | ||||||
|  | @ -181,25 +181,22 @@ func TestRender_IssueIndexPattern4(t *testing.T) { | ||||||
| 			links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue", name) | 			links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", "ref-issue", name) | ||||||
| 		} | 		} | ||||||
| 		expected := fmt.Sprintf(expectedFmt, links...) | 		expected := fmt.Sprintf(expectedFmt, links...) | ||||||
| 		testRenderIssueIndexPattern(t, s, expected, &postProcessCtx{metas: alphanumericMetas}) | 		testRenderIssueIndexPattern(t, s, expected, &RenderContext{Metas: alphanumericMetas}) | ||||||
| 	} | 	} | ||||||
| 	test("OTT-1234 test", "%s test", "OTT-1234") | 	test("OTT-1234 test", "%s test", "OTT-1234") | ||||||
| 	test("test T-12 issue", "test %s issue", "T-12") | 	test("test T-12 issue", "test %s issue", "T-12") | ||||||
| 	test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890") | 	test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *postProcessCtx) { | func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) { | ||||||
| 	if ctx == nil { | 	if ctx.URLPrefix == "" { | ||||||
| 		ctx = new(postProcessCtx) | 		ctx.URLPrefix = AppSubURL | ||||||
| 	} |  | ||||||
| 	ctx.procs = []processor{issueIndexPatternProcessor} |  | ||||||
| 	if ctx.urlPrefix == "" { |  | ||||||
| 		ctx.urlPrefix = AppSubURL |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	res, err := ctx.postProcess([]byte(input)) | 	var buf strings.Builder | ||||||
|  | 	err := postProcess(ctx, []processor{issueIndexPatternProcessor}, strings.NewReader(input), &buf) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, expected, string(res)) | 	assert.Equal(t, expected, buf.String()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestRender_AutoLink(t *testing.T) { | func TestRender_AutoLink(t *testing.T) { | ||||||
|  | @ -207,12 +204,22 @@ func TestRender_AutoLink(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer, err := PostProcess([]byte(input), setting.AppSubURL, localMetas, false) | 		var buffer strings.Builder | ||||||
|  | 		err := PostProcess(&RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 		}, strings.NewReader(input), &buffer) | ||||||
| 		assert.Equal(t, err, nil) | 		assert.Equal(t, err, nil) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) | ||||||
| 		buffer, err = PostProcess([]byte(input), setting.AppSubURL, localMetas, true) | 
 | ||||||
|  | 		buffer.Reset() | ||||||
|  | 		err = PostProcess(&RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 			IsWiki:    true, | ||||||
|  | 		}, strings.NewReader(input), &buffer) | ||||||
| 		assert.Equal(t, err, nil) | 		assert.Equal(t, err, nil) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// render valid issue URLs
 | 	// render valid issue URLs
 | ||||||
|  | @ -235,15 +242,13 @@ func TestRender_FullIssueURLs(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		ctx := new(postProcessCtx) | 		var result strings.Builder | ||||||
| 		ctx.procs = []processor{fullIssuePatternProcessor} | 		err := postProcess(&RenderContext{ | ||||||
| 		if ctx.urlPrefix == "" { | 			URLPrefix: AppSubURL, | ||||||
| 			ctx.urlPrefix = AppSubURL | 			Metas:     localMetas, | ||||||
| 		} | 		}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result) | ||||||
| 		ctx.metas = localMetas |  | ||||||
| 		result, err := ctx.postProcess([]byte(input)) |  | ||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, expected, string(result)) | 		assert.Equal(t, expected, result.String()) | ||||||
| 	} | 	} | ||||||
| 	test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | 	test("Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6", | ||||||
| 		"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | 		"Here is a link https://git.osgeo.org/gogs/postgis/postgis/pulls/6") | ||||||
|  |  | ||||||
|  | @ -28,7 +28,12 @@ func TestRender_Commits(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString(".md", input, setting.AppSubURL, localMetas) | 		buffer, err := RenderString(&RenderContext{ | ||||||
|  | 			Filename:  ".md", | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +64,12 @@ func TestRender_CrossReferences(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString("a.md", input, setting.AppSubURL, localMetas) | 		buffer, err := RenderString(&RenderContext{ | ||||||
|  | 			Filename:  "a.md", | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -91,7 +101,11 @@ func TestRender_links(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString("a.md", input, setting.AppSubURL, nil) | 		buffer, err := RenderString(&RenderContext{ | ||||||
|  | 			Filename:  "a.md", | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 	// Text that should be turned into URL
 | 	// Text that should be turned into URL
 | ||||||
|  | @ -187,8 +201,12 @@ func TestRender_email(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString("a.md", input, setting.AppSubURL, nil) | 		res, err := RenderString(&RenderContext{ | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 			Filename:  "a.md", | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(res)) | ||||||
| 	} | 	} | ||||||
| 	// Text that should be turned into email link
 | 	// Text that should be turned into email link
 | ||||||
| 
 | 
 | ||||||
|  | @ -242,7 +260,11 @@ func TestRender_emoji(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		expected = strings.ReplaceAll(expected, "&", "&") | 		expected = strings.ReplaceAll(expected, "&", "&") | ||||||
| 		buffer := RenderString("a.md", input, setting.AppSubURL, nil) | 		buffer, err := RenderString(&RenderContext{ | ||||||
|  | 			Filename:  "a.md", | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -291,9 +313,17 @@ func TestRender_ShortLinks(t *testing.T) { | ||||||
| 	tree := util.URLJoin(AppSubURL, "src", "master") | 	tree := util.URLJoin(AppSubURL, "src", "master") | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected, expectedWiki string) { | 	test := func(input, expected, expectedWiki string) { | ||||||
| 		buffer := markdown.RenderString(input, tree, nil) | 		buffer, err := markdown.RenderString(&RenderContext{ | ||||||
|  | 			URLPrefix: tree, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 		buffer = markdown.RenderWiki([]byte(input), setting.AppSubURL, localMetas) | 		buffer, err = markdown.RenderString(&RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 			IsWiki:    true, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -395,16 +425,22 @@ func Test_ParseClusterFuzz(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " | 	data := "<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " | ||||||
| 
 | 
 | ||||||
| 	val, err := PostProcess([]byte(data), "https://example.com", localMetas, false) | 	var res strings.Builder | ||||||
| 
 | 	err := PostProcess(&RenderContext{ | ||||||
|  | 		URLPrefix: "https://example.com", | ||||||
|  | 		Metas:     localMetas, | ||||||
|  | 	}, strings.NewReader(data), &res) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.NotContains(t, string(val), "<html") | 	assert.NotContains(t, res.String(), "<html") | ||||||
| 
 | 
 | ||||||
| 	data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " | 	data = "<!DOCTYPE html>\n<A><maTH><tr><MN><bodY ÿ><temPlate></template><tH><tr></A><tH><d<bodY " | ||||||
| 
 | 
 | ||||||
| 	val, err = PostProcess([]byte(data), "https://example.com", localMetas, false) | 	res.Reset() | ||||||
|  | 	err = PostProcess(&RenderContext{ | ||||||
|  | 		URLPrefix: "https://example.com", | ||||||
|  | 		Metas:     localMetas, | ||||||
|  | 	}, strings.NewReader(data), &res) | ||||||
| 
 | 
 | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 
 | 	assert.NotContains(t, res.String(), "<html") | ||||||
| 	assert.NotContains(t, string(val), "<html") |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ package markdown | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 
 | 
 | ||||||
|  | @ -73,17 +74,17 @@ func (l *limitWriter) CloseWithError(err error) error { | ||||||
| 	return l.w.CloseWithError(err) | 	return l.w.CloseWithError(err) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewGiteaParseContext creates a parser.Context with the gitea context set
 | // newParserContext creates a parser.Context with the render context set
 | ||||||
| func NewGiteaParseContext(urlPrefix string, metas map[string]string, isWiki bool) parser.Context { | func newParserContext(ctx *markup.RenderContext) parser.Context { | ||||||
| 	pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) | 	pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) | ||||||
| 	pc.Set(urlPrefixKey, urlPrefix) | 	pc.Set(urlPrefixKey, ctx.URLPrefix) | ||||||
| 	pc.Set(isWikiKey, isWiki) | 	pc.Set(isWikiKey, ctx.IsWiki) | ||||||
| 	pc.Set(renderMetasKey, metas) | 	pc.Set(renderMetasKey, ctx.Metas) | ||||||
| 	return pc | 	return pc | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // actualRender renders Markdown to HTML without handling special links.
 | // actualRender renders Markdown to HTML without handling special links.
 | ||||||
| func actualRender(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown bool) []byte { | func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	once.Do(func() { | 	once.Do(func() { | ||||||
| 		converter = goldmark.New( | 		converter = goldmark.New( | ||||||
| 			goldmark.WithExtensions(extension.Table, | 			goldmark.WithExtensions(extension.Table, | ||||||
|  | @ -169,7 +170,7 @@ func actualRender(body []byte, urlPrefix string, metas map[string]string, wikiMa | ||||||
| 		limit: setting.UI.MaxDisplayFileSize * 3, | 		limit: setting.UI.MaxDisplayFileSize * 3, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// FIXME: should we include a timeout that closes the pipe to abort the parser and sanitizer if it takes too long?
 | 	// FIXME: should we include a timeout that closes the pipe to abort the renderer and sanitizer if it takes too long?
 | ||||||
| 	go func() { | 	go func() { | ||||||
| 		defer func() { | 		defer func() { | ||||||
| 			err := recover() | 			err := recover() | ||||||
|  | @ -184,18 +185,26 @@ func actualRender(body []byte, urlPrefix string, metas map[string]string, wikiMa | ||||||
| 			_ = lw.CloseWithError(fmt.Errorf("%v", err)) | 			_ = lw.CloseWithError(fmt.Errorf("%v", err)) | ||||||
| 		}() | 		}() | ||||||
| 
 | 
 | ||||||
| 		pc := NewGiteaParseContext(urlPrefix, metas, wikiMarkdown) | 		// FIXME: Don't read all to memory, but goldmark doesn't support
 | ||||||
| 		if err := converter.Convert(giteautil.NormalizeEOL(body), lw, parser.WithContext(pc)); err != nil { | 		pc := newParserContext(ctx) | ||||||
|  | 		buf, err := ioutil.ReadAll(input) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("Unable to ReadAll: %v", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if err := converter.Convert(giteautil.NormalizeEOL(buf), lw, parser.WithContext(pc)); err != nil { | ||||||
| 			log.Error("Unable to render: %v", err) | 			log.Error("Unable to render: %v", err) | ||||||
| 			_ = lw.CloseWithError(err) | 			_ = lw.CloseWithError(err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		_ = lw.Close() | 		_ = lw.Close() | ||||||
| 	}() | 	}() | ||||||
| 	return markup.SanitizeReader(rd).Bytes() | 	buf := markup.SanitizeReader(rd) | ||||||
|  | 	_, err := io.Copy(output, buf) | ||||||
|  | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown bool) (ret []byte) { | func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		err := recover() | 		err := recover() | ||||||
| 		if err == nil { | 		if err == nil { | ||||||
|  | @ -206,9 +215,13 @@ func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown | ||||||
| 		if log.IsDebug() { | 		if log.IsDebug() { | ||||||
| 			log.Debug("Panic in markdown: %v\n%s", err, string(log.Stack(2))) | 			log.Debug("Panic in markdown: %v\n%s", err, string(log.Stack(2))) | ||||||
| 		} | 		} | ||||||
| 		ret = markup.SanitizeBytes(body) | 		ret := markup.SanitizeReader(input) | ||||||
|  | 		_, err = io.Copy(output, ret) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Error("SanitizeReader failed: %v", err) | ||||||
|  | 		} | ||||||
| 	}() | 	}() | ||||||
| 	return actualRender(body, urlPrefix, metas, wikiMarkdown) | 	return actualRender(ctx, input, output) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
|  | @ -217,48 +230,59 @@ var ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	markup.RegisterParser(Parser{}) | 	markup.RegisterRenderer(Renderer{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Parser implements markup.Parser
 | // Renderer implements markup.Renderer
 | ||||||
| type Parser struct{} | type Renderer struct{} | ||||||
| 
 | 
 | ||||||
| // Name implements markup.Parser
 | // Name implements markup.Renderer
 | ||||||
| func (Parser) Name() string { | func (Renderer) Name() string { | ||||||
| 	return MarkupName | 	return MarkupName | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NeedPostProcess implements markup.Parser
 | // NeedPostProcess implements markup.Renderer
 | ||||||
| func (Parser) NeedPostProcess() bool { return true } | func (Renderer) NeedPostProcess() bool { return true } | ||||||
| 
 | 
 | ||||||
| // Extensions implements markup.Parser
 | // Extensions implements markup.Renderer
 | ||||||
| func (Parser) Extensions() []string { | func (Renderer) Extensions() []string { | ||||||
| 	return setting.Markdown.FileExtensions | 	return setting.Markdown.FileExtensions | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render implements markup.Parser
 | // Render implements markup.Renderer
 | ||||||
| func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	return render(rawBytes, urlPrefix, metas, isWiki) | 	return render(ctx, input, output) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render renders Markdown to HTML with all specific handling stuff.
 | // Render renders Markdown to HTML with all specific handling stuff.
 | ||||||
| func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte { | func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	return markup.Render("a.md", rawBytes, urlPrefix, metas) | 	if ctx.Filename == "" { | ||||||
|  | 		ctx.Filename = "a.md" | ||||||
|  | 	} | ||||||
|  | 	return markup.Render(ctx, input, output) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RenderString renders Markdown string to HTML with all specific handling stuff and return string
 | ||||||
|  | func RenderString(ctx *markup.RenderContext, content string) (string, error) { | ||||||
|  | 	var buf strings.Builder | ||||||
|  | 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return buf.String(), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderRaw renders Markdown to HTML without handling special links.
 | // RenderRaw renders Markdown to HTML without handling special links.
 | ||||||
| func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { | func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	return render(body, urlPrefix, map[string]string{}, wikiMarkdown) | 	return render(ctx, input, output) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderString renders Markdown to HTML with special links and returns string type.
 | // RenderRawString renders Markdown to HTML without handling special links and return string
 | ||||||
| func RenderString(raw, urlPrefix string, metas map[string]string) string { | func RenderRawString(ctx *markup.RenderContext, content string) (string, error) { | ||||||
| 	return markup.RenderString("a.md", raw, urlPrefix, metas) | 	var buf strings.Builder | ||||||
| } | 	if err := RenderRaw(ctx, strings.NewReader(content), &buf); err != nil { | ||||||
| 
 | 		return "", err | ||||||
| // RenderWiki renders markdown wiki page to HTML and return HTML string
 | 	} | ||||||
| func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string { | 	return buf.String(), nil | ||||||
| 	return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IsMarkdownFile reports whether name looks like a Markdown file
 | // IsMarkdownFile reports whether name looks like a Markdown file
 | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	. "code.gitea.io/gitea/modules/markup/markdown" | 	. "code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | @ -31,10 +32,17 @@ func TestRender_StandardLinks(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected, expectedWiki string) { | 	test := func(input, expected, expectedWiki string) { | ||||||
| 		buffer := RenderString(input, setting.AppSubURL, nil) | 		buffer, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 		bufferWiki := RenderWiki([]byte(input), setting.AppSubURL, nil) | 
 | ||||||
| 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(bufferWiki)) | 		buffer, err = RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 			IsWiki:    true, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` | 	googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` | ||||||
|  | @ -74,7 +82,10 @@ func TestRender_Images(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString(input, setting.AppSubURL, nil) | 		buffer, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -261,7 +272,12 @@ func TestTotal_RenderWiki(t *testing.T) { | ||||||
| 	answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) | 	answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) | ||||||
| 
 | 
 | ||||||
| 	for i := 0; i < len(sameCases); i++ { | 	for i := 0; i < len(sameCases); i++ { | ||||||
| 		line := RenderWiki([]byte(sameCases[i]), AppSubURL, localMetas) | 		line, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: AppSubURL, | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 			IsWiki:    true, | ||||||
|  | 		}, sameCases[i]) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, answers[i], line) | 		assert.Equal(t, answers[i], line) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -279,7 +295,11 @@ func TestTotal_RenderWiki(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for i := 0; i < len(testCases); i += 2 { | 	for i := 0; i < len(testCases); i += 2 { | ||||||
| 		line := RenderWiki([]byte(testCases[i]), AppSubURL, nil) | 		line, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: AppSubURL, | ||||||
|  | 			IsWiki:    true, | ||||||
|  | 		}, testCases[i]) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, testCases[i+1], line) | 		assert.Equal(t, testCases[i+1], line) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | @ -288,31 +308,40 @@ func TestTotal_RenderString(t *testing.T) { | ||||||
| 	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) | 	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) | ||||||
| 
 | 
 | ||||||
| 	for i := 0; i < len(sameCases); i++ { | 	for i := 0; i < len(sameCases); i++ { | ||||||
| 		line := RenderString(sameCases[i], util.URLJoin(AppSubURL, "src", "master/"), localMetas) | 		line, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: util.URLJoin(AppSubURL, "src", "master/"), | ||||||
|  | 			Metas:     localMetas, | ||||||
|  | 		}, sameCases[i]) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, answers[i], line) | 		assert.Equal(t, answers[i], line) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	testCases := []string{} | 	testCases := []string{} | ||||||
| 
 | 
 | ||||||
| 	for i := 0; i < len(testCases); i += 2 { | 	for i := 0; i < len(testCases); i += 2 { | ||||||
| 		line := RenderString(testCases[i], AppSubURL, nil) | 		line, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: AppSubURL, | ||||||
|  | 		}, testCases[i]) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, testCases[i+1], line) | 		assert.Equal(t, testCases[i+1], line) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestRender_RenderParagraphs(t *testing.T) { | func TestRender_RenderParagraphs(t *testing.T) { | ||||||
| 	test := func(t *testing.T, str string, cnt int) { | 	test := func(t *testing.T, str string, cnt int) { | ||||||
| 		unix := []byte(str) | 		res, err := RenderRawString(&markup.RenderContext{}, str) | ||||||
| 		res := string(RenderRaw(unix, "", false)) | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.Count(res, "<p"), cnt, "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | 		assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | ||||||
| 
 | 
 | ||||||
| 		mac := []byte(strings.ReplaceAll(str, "\n", "\r")) | 		mac := strings.ReplaceAll(str, "\n", "\r") | ||||||
| 		res = string(RenderRaw(mac, "", false)) | 		res, err = RenderRawString(&markup.RenderContext{}, mac) | ||||||
| 		assert.Equal(t, strings.Count(res, "<p"), cnt, "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | ||||||
| 
 | 
 | ||||||
| 		dos := []byte(strings.ReplaceAll(str, "\n", "\r\n")) | 		dos := strings.ReplaceAll(str, "\n", "\r\n") | ||||||
| 		res = string(RenderRaw(dos, "", false)) | 		res, err = RenderRawString(&markup.RenderContext{}, dos) | ||||||
| 		assert.Equal(t, strings.Count(res, "<p"), cnt, "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | 		assert.NoError(t, err) | ||||||
|  | 		assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	test(t, "\nOne\nTwo\nThree", 1) | 	test(t, "\nOne\nTwo\nThree", 1) | ||||||
|  | @ -337,7 +366,8 @@ func TestMarkdownRenderRaw(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, testcase := range testcases { | 	for _, testcase := range testcases { | ||||||
| 		_ = RenderRaw(testcase, "", false) | 		_, err := RenderRawString(&markup.RenderContext{}, string(testcase)) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -348,7 +378,8 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) { | ||||||
| 	expected := `<p><a href="/image1" rel="nofollow"><img src="/image1" alt="image1"></a><br> | 	expected := `<p><a href="/image1" rel="nofollow"><img src="/image1" alt="image1"></a><br> | ||||||
| <a href="/image2" rel="nofollow"><img src="/image2" alt="image2"></a></p> | <a href="/image2" rel="nofollow"><img src="/image2" alt="image2"></a></p> | ||||||
| ` | ` | ||||||
| 	res := string(RenderRaw([]byte(testcase), "", false)) | 	res, err := RenderRawString(&markup.RenderContext{}, testcase) | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Equal(t, expected, res) | 	assert.Equal(t, expected, res) | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,143 +0,0 @@ | ||||||
| // Copyright 2017 The Gitea Authors. All rights reserved.
 |  | ||||||
| // Use of this source code is governed by a MIT-style
 |  | ||||||
| // license that can be found in the LICENSE file.
 |  | ||||||
| 
 |  | ||||||
| package markup |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
| 	"code.gitea.io/gitea/modules/setting" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Init initialize regexps for markdown parsing
 |  | ||||||
| func Init() { |  | ||||||
| 	getIssueFullPattern() |  | ||||||
| 	NewSanitizer() |  | ||||||
| 	if len(setting.Markdown.CustomURLSchemes) > 0 { |  | ||||||
| 		CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// since setting maybe changed extensions, this will reload all parser extensions mapping
 |  | ||||||
| 	extParsers = make(map[string]Parser) |  | ||||||
| 	for _, parser := range parsers { |  | ||||||
| 		for _, ext := range parser.Extensions() { |  | ||||||
| 			extParsers[strings.ToLower(ext)] = parser |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Parser defines an interface for parsering markup file to HTML
 |  | ||||||
| type Parser interface { |  | ||||||
| 	Name() string // markup format name
 |  | ||||||
| 	Extensions() []string |  | ||||||
| 	NeedPostProcess() bool |  | ||||||
| 	Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| var ( |  | ||||||
| 	extParsers = make(map[string]Parser) |  | ||||||
| 	parsers    = make(map[string]Parser) |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // RegisterParser registers a new markup file parser
 |  | ||||||
| func RegisterParser(parser Parser) { |  | ||||||
| 	parsers[parser.Name()] = parser |  | ||||||
| 	for _, ext := range parser.Extensions() { |  | ||||||
| 		extParsers[strings.ToLower(ext)] = parser |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetParserByFileName get parser by filename
 |  | ||||||
| func GetParserByFileName(filename string) Parser { |  | ||||||
| 	extension := strings.ToLower(filepath.Ext(filename)) |  | ||||||
| 	return extParsers[extension] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetParserByType returns a parser according type
 |  | ||||||
| func GetParserByType(tp string) Parser { |  | ||||||
| 	return parsers[tp] |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Render renders markup file to HTML with all specific handling stuff.
 |  | ||||||
| func Render(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) []byte { |  | ||||||
| 	return renderFile(filename, rawBytes, urlPrefix, metas, false) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // RenderByType renders markup to HTML with special links and returns string type.
 |  | ||||||
| func RenderByType(tp string, rawBytes []byte, urlPrefix string, metas map[string]string) []byte { |  | ||||||
| 	return renderByType(tp, rawBytes, urlPrefix, metas, false) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // RenderString renders Markdown to HTML with special links and returns string type.
 |  | ||||||
| func RenderString(filename string, raw, urlPrefix string, metas map[string]string) string { |  | ||||||
| 	return string(renderFile(filename, []byte(raw), urlPrefix, metas, false)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // RenderWiki renders markdown wiki page to HTML and return HTML string
 |  | ||||||
| func RenderWiki(filename string, rawBytes []byte, urlPrefix string, metas map[string]string) string { |  | ||||||
| 	return string(renderFile(filename, rawBytes, urlPrefix, metas, true)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func render(parser Parser, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { |  | ||||||
| 	result := parser.Render(rawBytes, urlPrefix, metas, isWiki) |  | ||||||
| 	if parser.NeedPostProcess() { |  | ||||||
| 		var err error |  | ||||||
| 		// TODO: one day the error should be returned.
 |  | ||||||
| 		result, err = PostProcess(result, urlPrefix, metas, isWiki) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error("PostProcess: %v", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return SanitizeBytes(result) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func renderByType(tp string, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { |  | ||||||
| 	if parser, ok := parsers[tp]; ok { |  | ||||||
| 		return render(parser, rawBytes, urlPrefix, metas, isWiki) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func renderFile(filename string, rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { |  | ||||||
| 	extension := strings.ToLower(filepath.Ext(filename)) |  | ||||||
| 	if parser, ok := extParsers[extension]; ok { |  | ||||||
| 		return render(parser, rawBytes, urlPrefix, metas, isWiki) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Type returns if markup format via the filename
 |  | ||||||
| func Type(filename string) string { |  | ||||||
| 	if parser := GetParserByFileName(filename); parser != nil { |  | ||||||
| 		return parser.Name() |  | ||||||
| 	} |  | ||||||
| 	return "" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IsMarkupFile reports whether file is a markup type file
 |  | ||||||
| func IsMarkupFile(name, markup string) bool { |  | ||||||
| 	if parser := GetParserByFileName(name); parser != nil { |  | ||||||
| 		return parser.Name() == markup |  | ||||||
| 	} |  | ||||||
| 	return false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // IsReadmeFile reports whether name looks like a README file
 |  | ||||||
| // based on its name. If an extension is provided, it will strictly
 |  | ||||||
| // match that extension.
 |  | ||||||
| // Note that the '.' should be provided in ext, e.g ".md"
 |  | ||||||
| func IsReadmeFile(name string, ext ...string) bool { |  | ||||||
| 	name = strings.ToLower(name) |  | ||||||
| 	if len(ext) > 0 { |  | ||||||
| 		return name == "readme"+ext[0] |  | ||||||
| 	} |  | ||||||
| 	if len(name) < 6 { |  | ||||||
| 		return false |  | ||||||
| 	} else if len(name) == 6 { |  | ||||||
| 		return name == "readme" |  | ||||||
| 	} |  | ||||||
| 	return name[:7] == "readme." |  | ||||||
| } |  | ||||||
|  | @ -8,9 +8,9 @@ import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
|  | 	"io" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/log" |  | ||||||
| 	"code.gitea.io/gitea/modules/markup" | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 
 | 
 | ||||||
|  | @ -18,58 +18,62 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
| 	markup.RegisterParser(Parser{}) | 	markup.RegisterRenderer(Renderer{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Parser implements markup.Parser for orgmode
 | // Renderer implements markup.Renderer for orgmode
 | ||||||
| type Parser struct { | type Renderer struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Name implements markup.Parser
 | // Name implements markup.Renderer
 | ||||||
| func (Parser) Name() string { | func (Renderer) Name() string { | ||||||
| 	return "orgmode" | 	return "orgmode" | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NeedPostProcess implements markup.Parser
 | // NeedPostProcess implements markup.Renderer
 | ||||||
| func (Parser) NeedPostProcess() bool { return true } | func (Renderer) NeedPostProcess() bool { return true } | ||||||
| 
 | 
 | ||||||
| // Extensions implements markup.Parser
 | // Extensions implements markup.Renderer
 | ||||||
| func (Parser) Extensions() []string { | func (Renderer) Extensions() []string { | ||||||
| 	return []string{".org"} | 	return []string{".org"} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render renders orgmode rawbytes to HTML
 | // Render renders orgmode rawbytes to HTML
 | ||||||
| func Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	htmlWriter := org.NewHTMLWriter() | 	htmlWriter := org.NewHTMLWriter() | ||||||
| 
 | 
 | ||||||
| 	renderer := &Renderer{ | 	w := &Writer{ | ||||||
| 		HTMLWriter: htmlWriter, | 		HTMLWriter: htmlWriter, | ||||||
| 		URLPrefix:  urlPrefix, | 		URLPrefix:  ctx.URLPrefix, | ||||||
| 		IsWiki:     isWiki, | 		IsWiki:     ctx.IsWiki, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	htmlWriter.ExtendingWriter = renderer | 	htmlWriter.ExtendingWriter = w | ||||||
| 
 | 
 | ||||||
| 	res, err := org.New().Silent().Parse(bytes.NewReader(rawBytes), "").Write(renderer) | 	res, err := org.New().Silent().Parse(input, "").Write(w) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Panic in orgmode.Render: %v Just returning the rawBytes", err) | 		return fmt.Errorf("orgmode.Render failed: %v", err) | ||||||
| 		return rawBytes |  | ||||||
| 	} | 	} | ||||||
| 	return []byte(res) | 	_, err = io.Copy(output, strings.NewReader(res)) | ||||||
|  | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // RenderString reners orgmode string to HTML string
 | // RenderString renders orgmode string to HTML string
 | ||||||
| func RenderString(rawContent string, urlPrefix string, metas map[string]string, isWiki bool) string { | func RenderString(ctx *markup.RenderContext, content string) (string, error) { | ||||||
| 	return string(Render([]byte(rawContent), urlPrefix, metas, isWiki)) | 	var buf strings.Builder | ||||||
|  | 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return buf.String(), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Render reners orgmode string to HTML string
 | // Render renders orgmode string to HTML string
 | ||||||
| func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte { | func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { | ||||||
| 	return Render(rawBytes, urlPrefix, metas, isWiki) | 	return Render(ctx, input, output) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Renderer implements org.Writer
 | // Writer implements org.Writer
 | ||||||
| type Renderer struct { | type Writer struct { | ||||||
| 	*org.HTMLWriter | 	*org.HTMLWriter | ||||||
| 	URLPrefix string | 	URLPrefix string | ||||||
| 	IsWiki    bool | 	IsWiki    bool | ||||||
|  | @ -78,7 +82,7 @@ type Renderer struct { | ||||||
| var byteMailto = []byte("mailto:") | var byteMailto = []byte("mailto:") | ||||||
| 
 | 
 | ||||||
| // WriteRegularLink renders images, links or videos
 | // WriteRegularLink renders images, links or videos
 | ||||||
| func (r *Renderer) WriteRegularLink(l org.RegularLink) { | func (r *Writer) WriteRegularLink(l org.RegularLink) { | ||||||
| 	link := []byte(html.EscapeString(l.URL)) | 	link := []byte(html.EscapeString(l.URL)) | ||||||
| 	if l.Protocol == "file" { | 	if l.Protocol == "file" { | ||||||
| 		link = link[len("file:"):] | 		link = link[len("file:"):] | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 
 | 
 | ||||||
|  | @ -23,7 +24,10 @@ func TestRender_StandardLinks(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString(input, setting.AppSubURL, nil, false) | 		buffer, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -40,7 +44,10 @@ func TestRender_Images(t *testing.T) { | ||||||
| 	setting.AppSubURL = AppSubURL | 	setting.AppSubURL = AppSubURL | ||||||
| 
 | 
 | ||||||
| 	test := func(input, expected string) { | 	test := func(input, expected string) { | ||||||
| 		buffer := RenderString(input, setting.AppSubURL, nil, false) | 		buffer, err := RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: setting.AppSubURL, | ||||||
|  | 		}, input) | ||||||
|  | 		assert.NoError(t, err) | ||||||
| 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | 		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										201
									
								
								modules/markup/renderer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								modules/markup/renderer.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | ||||||
|  | // Copyright 2017 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package markup | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  | 	"sync" | ||||||
|  | 
 | ||||||
|  | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Init initialize regexps for markdown parsing
 | ||||||
|  | func Init() { | ||||||
|  | 	getIssueFullPattern() | ||||||
|  | 	NewSanitizer() | ||||||
|  | 	if len(setting.Markdown.CustomURLSchemes) > 0 { | ||||||
|  | 		CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// since setting maybe changed extensions, this will reload all renderer extensions mapping
 | ||||||
|  | 	extRenderers = make(map[string]Renderer) | ||||||
|  | 	for _, renderer := range renderers { | ||||||
|  | 		for _, ext := range renderer.Extensions() { | ||||||
|  | 			extRenderers[strings.ToLower(ext)] = renderer | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RenderContext represents a render context
 | ||||||
|  | type RenderContext struct { | ||||||
|  | 	Ctx         context.Context | ||||||
|  | 	Filename    string | ||||||
|  | 	Type        string | ||||||
|  | 	IsWiki      bool | ||||||
|  | 	URLPrefix   string | ||||||
|  | 	Metas       map[string]string | ||||||
|  | 	DefaultLink string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Renderer defines an interface for rendering markup file to HTML
 | ||||||
|  | type Renderer interface { | ||||||
|  | 	Name() string // markup format name
 | ||||||
|  | 	Extensions() []string | ||||||
|  | 	Render(ctx *RenderContext, input io.Reader, output io.Writer) error | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	extRenderers = make(map[string]Renderer) | ||||||
|  | 	renderers    = make(map[string]Renderer) | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // RegisterRenderer registers a new markup file renderer
 | ||||||
|  | func RegisterRenderer(renderer Renderer) { | ||||||
|  | 	renderers[renderer.Name()] = renderer | ||||||
|  | 	for _, ext := range renderer.Extensions() { | ||||||
|  | 		extRenderers[strings.ToLower(ext)] = renderer | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetRendererByFileName get renderer by filename
 | ||||||
|  | func GetRendererByFileName(filename string) Renderer { | ||||||
|  | 	extension := strings.ToLower(filepath.Ext(filename)) | ||||||
|  | 	return extRenderers[extension] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetRendererByType returns a renderer according type
 | ||||||
|  | func GetRendererByType(tp string) Renderer { | ||||||
|  | 	return renderers[tp] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Render renders markup file to HTML with all specific handling stuff.
 | ||||||
|  | func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||||
|  | 	if ctx.Type != "" { | ||||||
|  | 		return renderByType(ctx, input, output) | ||||||
|  | 	} else if ctx.Filename != "" { | ||||||
|  | 		return renderFile(ctx, input, output) | ||||||
|  | 	} | ||||||
|  | 	return errors.New("Render options both filename and type missing") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // RenderString renders Markup string to HTML with all specific handling stuff and return string
 | ||||||
|  | func RenderString(ctx *RenderContext, content string) (string, error) { | ||||||
|  | 	var buf strings.Builder | ||||||
|  | 	if err := Render(ctx, strings.NewReader(content), &buf); err != nil { | ||||||
|  | 		return "", err | ||||||
|  | 	} | ||||||
|  | 	return buf.String(), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func render(ctx *RenderContext, parser Renderer, input io.Reader, output io.Writer) error { | ||||||
|  | 	var wg sync.WaitGroup | ||||||
|  | 	var err error | ||||||
|  | 	pr, pw := io.Pipe() | ||||||
|  | 	defer func() { | ||||||
|  | 		_ = pr.Close() | ||||||
|  | 		_ = pw.Close() | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	pr2, pw2 := io.Pipe() | ||||||
|  | 	defer func() { | ||||||
|  | 		_ = pr2.Close() | ||||||
|  | 		_ = pw2.Close() | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		buf := SanitizeReader(pr2) | ||||||
|  | 		_, err = io.Copy(output, buf) | ||||||
|  | 		_ = pr2.Close() | ||||||
|  | 		wg.Done() | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	wg.Add(1) | ||||||
|  | 	go func() { | ||||||
|  | 		err = PostProcess(ctx, pr, pw2) | ||||||
|  | 		_ = pr.Close() | ||||||
|  | 		_ = pw2.Close() | ||||||
|  | 		wg.Done() | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	if err1 := parser.Render(ctx, input, pw); err1 != nil { | ||||||
|  | 		return err1 | ||||||
|  | 	} | ||||||
|  | 	_ = pw.Close() | ||||||
|  | 
 | ||||||
|  | 	wg.Wait() | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrUnsupportedRenderType represents
 | ||||||
|  | type ErrUnsupportedRenderType struct { | ||||||
|  | 	Type string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrUnsupportedRenderType) Error() string { | ||||||
|  | 	return fmt.Sprintf("Unsupported render type: %s", err.Type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||||
|  | 	if renderer, ok := renderers[ctx.Type]; ok { | ||||||
|  | 		return render(ctx, renderer, input, output) | ||||||
|  | 	} | ||||||
|  | 	return ErrUnsupportedRenderType{ctx.Type} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render
 | ||||||
|  | type ErrUnsupportedRenderExtension struct { | ||||||
|  | 	Extension string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrUnsupportedRenderExtension) Error() string { | ||||||
|  | 	return fmt.Sprintf("Unsupported render extension: %s", err.Extension) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { | ||||||
|  | 	extension := strings.ToLower(filepath.Ext(ctx.Filename)) | ||||||
|  | 	if renderer, ok := extRenderers[extension]; ok { | ||||||
|  | 		return render(ctx, renderer, input, output) | ||||||
|  | 	} | ||||||
|  | 	return ErrUnsupportedRenderExtension{extension} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Type returns if markup format via the filename
 | ||||||
|  | func Type(filename string) string { | ||||||
|  | 	if parser := GetRendererByFileName(filename); parser != nil { | ||||||
|  | 		return parser.Name() | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsMarkupFile reports whether file is a markup type file
 | ||||||
|  | func IsMarkupFile(name, markup string) bool { | ||||||
|  | 	if parser := GetRendererByFileName(name); parser != nil { | ||||||
|  | 		return parser.Name() == markup | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsReadmeFile reports whether name looks like a README file
 | ||||||
|  | // based on its name. If an extension is provided, it will strictly
 | ||||||
|  | // match that extension.
 | ||||||
|  | // Note that the '.' should be provided in ext, e.g ".md"
 | ||||||
|  | func IsReadmeFile(name string, ext ...string) bool { | ||||||
|  | 	name = strings.ToLower(name) | ||||||
|  | 	if len(ext) > 0 { | ||||||
|  | 		return name == "readme"+ext[0] | ||||||
|  | 	} | ||||||
|  | 	if len(name) < 6 { | ||||||
|  | 		return false | ||||||
|  | 	} else if len(name) == 6 { | ||||||
|  | 		return name == "readme" | ||||||
|  | 	} | ||||||
|  | 	return name[:7] == "readme." | ||||||
|  | } | ||||||
|  | @ -104,14 +104,18 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *model | ||||||
| 	// mail only sent to added assignees and not self-assignee
 | 	// mail only sent to added assignees and not self-assignee
 | ||||||
| 	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { | 	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { | ||||||
| 		ct := fmt.Sprintf("Assigned #%d.", issue.Index) | 		ct := fmt.Sprintf("Assigned #%d.", issue.Index) | ||||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{assignee}) | 		if err := mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{assignee}); err != nil { | ||||||
|  | 			log.Error("Error in SendIssueAssignedMail for issue[%d] to assignee[%d]: %v", issue.ID, assignee.ID, err) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (m *mailNotifier) NotifyPullReviewRequest(doer *models.User, issue *models.Issue, reviewer *models.User, isRequest bool, comment *models.Comment) { | func (m *mailNotifier) NotifyPullReviewRequest(doer *models.User, issue *models.Issue, reviewer *models.User, isRequest bool, comment *models.Comment) { | ||||||
| 	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { | 	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { | ||||||
| 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) | 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) | ||||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{reviewer}) | 		if err := mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{reviewer}); err != nil { | ||||||
|  | 			log.Error("Error in SendIssueAssignedMail for issue[%d] to reviewer[%d]: %v", issue.ID, reviewer.ID, err) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -13,14 +13,14 @@ import ( | ||||||
| 	"gopkg.in/ini.v1" | 	"gopkg.in/ini.v1" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ExternalMarkupParsers represents the external markup parsers
 | // ExternalMarkupRenderers represents the external markup renderers
 | ||||||
| var ( | var ( | ||||||
| 	ExternalMarkupParsers  []MarkupParser | 	ExternalMarkupRenderers []MarkupRenderer | ||||||
| 	ExternalSanitizerRules []MarkupSanitizerRule | 	ExternalSanitizerRules  []MarkupSanitizerRule | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // MarkupParser defines the external parser configured in ini
 | // MarkupRenderer defines the external parser configured in ini
 | ||||||
| type MarkupParser struct { | type MarkupRenderer struct { | ||||||
| 	Enabled         bool | 	Enabled         bool | ||||||
| 	MarkupName      string | 	MarkupName      string | ||||||
| 	Command         string | 	Command         string | ||||||
|  | @ -124,7 +124,7 @@ func newMarkupRenderer(name string, sec *ini.Section) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ExternalMarkupParsers = append(ExternalMarkupParsers, MarkupParser{ | 	ExternalMarkupRenderers = append(ExternalMarkupRenderers, MarkupRenderer{ | ||||||
| 		Enabled:         sec.Key("ENABLED").MustBool(false), | 		Enabled:         sec.Key("ENABLED").MustBool(false), | ||||||
| 		MarkupName:      name, | 		MarkupName:      name, | ||||||
| 		FileExtensions:  exts, | 		FileExtensions:  exts, | ||||||
|  |  | ||||||
|  | @ -665,7 +665,11 @@ func RenderCommitMessageLink(msg, urlPrefix, urlDefault string, metas map[string | ||||||
| 	cleanMsg := template.HTMLEscapeString(msg) | 	cleanMsg := template.HTMLEscapeString(msg) | ||||||
| 	// we can safely assume that it will not return any error, since there
 | 	// we can safely assume that it will not return any error, since there
 | ||||||
| 	// shouldn't be any special HTML.
 | 	// shouldn't be any special HTML.
 | ||||||
| 	fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, urlDefault, metas) | 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||||
|  | 		URLPrefix:   urlPrefix, | ||||||
|  | 		DefaultLink: urlDefault, | ||||||
|  | 		Metas:       metas, | ||||||
|  | 	}, cleanMsg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderCommitMessage: %v", err) | 		log.Error("RenderCommitMessage: %v", err) | ||||||
| 		return "" | 		return "" | ||||||
|  | @ -692,7 +696,11 @@ func RenderCommitMessageLinkSubject(msg, urlPrefix, urlDefault string, metas map | ||||||
| 
 | 
 | ||||||
| 	// we can safely assume that it will not return any error, since there
 | 	// we can safely assume that it will not return any error, since there
 | ||||||
| 	// shouldn't be any special HTML.
 | 	// shouldn't be any special HTML.
 | ||||||
| 	renderedMessage, err := markup.RenderCommitMessageSubject([]byte(template.HTMLEscapeString(msgLine)), urlPrefix, urlDefault, metas) | 	renderedMessage, err := markup.RenderCommitMessageSubject(&markup.RenderContext{ | ||||||
|  | 		URLPrefix:   urlPrefix, | ||||||
|  | 		DefaultLink: urlDefault, | ||||||
|  | 		Metas:       metas, | ||||||
|  | 	}, template.HTMLEscapeString(msgLine)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderCommitMessageSubject: %v", err) | 		log.Error("RenderCommitMessageSubject: %v", err) | ||||||
| 		return template.HTML("") | 		return template.HTML("") | ||||||
|  | @ -714,7 +722,10 @@ func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.H | ||||||
| 		return template.HTML("") | 		return template.HTML("") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	renderedMessage, err := markup.RenderCommitMessage([]byte(template.HTMLEscapeString(msgLine)), urlPrefix, "", metas) | 	renderedMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: urlPrefix, | ||||||
|  | 		Metas:     metas, | ||||||
|  | 	}, template.HTMLEscapeString(msgLine)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderCommitMessage: %v", err) | 		log.Error("RenderCommitMessage: %v", err) | ||||||
| 		return "" | 		return "" | ||||||
|  | @ -724,7 +735,10 @@ func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.H | ||||||
| 
 | 
 | ||||||
| // RenderIssueTitle renders issue/pull title with defined post processors
 | // RenderIssueTitle renders issue/pull title with defined post processors
 | ||||||
| func RenderIssueTitle(text, urlPrefix string, metas map[string]string) template.HTML { | func RenderIssueTitle(text, urlPrefix string, metas map[string]string) template.HTML { | ||||||
| 	renderedText, err := markup.RenderIssueTitle([]byte(template.HTMLEscapeString(text)), urlPrefix, metas) | 	renderedText, err := markup.RenderIssueTitle(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: urlPrefix, | ||||||
|  | 		Metas:     metas, | ||||||
|  | 	}, template.HTMLEscapeString(text)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderIssueTitle: %v", err) | 		log.Error("RenderIssueTitle: %v", err) | ||||||
| 		return template.HTML("") | 		return template.HTML("") | ||||||
|  | @ -734,7 +748,7 @@ func RenderIssueTitle(text, urlPrefix string, metas map[string]string) template. | ||||||
| 
 | 
 | ||||||
| // RenderEmoji renders html text with emoji post processors
 | // RenderEmoji renders html text with emoji post processors
 | ||||||
| func RenderEmoji(text string) template.HTML { | func RenderEmoji(text string) template.HTML { | ||||||
| 	renderedText, err := markup.RenderEmoji([]byte(template.HTMLEscapeString(text))) | 	renderedText, err := markup.RenderEmoji(template.HTMLEscapeString(text)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderEmoji: %v", err) | 		log.Error("RenderEmoji: %v", err) | ||||||
| 		return template.HTML("") | 		return template.HTML("") | ||||||
|  | @ -758,7 +772,10 @@ func ReactionToEmoji(reaction string) template.HTML { | ||||||
| // RenderNote renders the contents of a git-notes file as a commit message.
 | // RenderNote renders the contents of a git-notes file as a commit message.
 | ||||||
| func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML { | func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML { | ||||||
| 	cleanMsg := template.HTMLEscapeString(msg) | 	cleanMsg := template.HTMLEscapeString(msg) | ||||||
| 	fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, "", metas) | 	fullMessage, err := markup.RenderCommitMessage(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: urlPrefix, | ||||||
|  | 		Metas:     metas, | ||||||
|  | 	}, cleanMsg) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("RenderNote: %v", err) | 		log.Error("RenderNote: %v", err) | ||||||
| 		return "" | 		return "" | ||||||
|  |  | ||||||
|  | @ -5,11 +5,11 @@ | ||||||
| package misc | package misc | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | @ -55,7 +55,6 @@ func Markdown(ctx *context.APIContext) { | ||||||
| 	case "comment": | 	case "comment": | ||||||
| 		fallthrough | 		fallthrough | ||||||
| 	case "gfm": | 	case "gfm": | ||||||
| 		md := []byte(form.Text) |  | ||||||
| 		urlPrefix := form.Context | 		urlPrefix := form.Context | ||||||
| 		meta := map[string]string{} | 		meta := map[string]string{} | ||||||
| 		if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { | 		if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) { | ||||||
|  | @ -77,22 +76,19 @@ func Markdown(ctx *context.APIContext) { | ||||||
| 		if form.Mode == "gfm" { | 		if form.Mode == "gfm" { | ||||||
| 			meta["mode"] = "document" | 			meta["mode"] = "document" | ||||||
| 		} | 		} | ||||||
| 		if form.Wiki { | 
 | ||||||
| 			_, err := ctx.Write([]byte(markdown.RenderWiki(md, urlPrefix, meta))) | 		if err := markdown.Render(&markup.RenderContext{ | ||||||
| 			if err != nil { | 			URLPrefix: urlPrefix, | ||||||
| 				ctx.InternalServerError(err) | 			Metas:     meta, | ||||||
| 				return | 			IsWiki:    form.Wiki, | ||||||
| 			} | 		}, strings.NewReader(form.Text), ctx.Resp); err != nil { | ||||||
| 		} else { | 			ctx.InternalServerError(err) | ||||||
| 			_, err := ctx.Write(markdown.Render(md, urlPrefix, meta)) | 			return | ||||||
| 			if err != nil { |  | ||||||
| 				ctx.InternalServerError(err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	default: | 	default: | ||||||
| 		_, err := ctx.Write(markdown.RenderRaw([]byte(form.Text), "", false)) | 		if err := markdown.RenderRaw(&markup.RenderContext{ | ||||||
| 		if err != nil { | 			URLPrefix: form.Context, | ||||||
|  | 		}, strings.NewReader(form.Text), ctx.Resp); err != nil { | ||||||
| 			ctx.InternalServerError(err) | 			ctx.InternalServerError(err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  | @ -120,14 +116,8 @@ func MarkdownRaw(ctx *context.APIContext) { | ||||||
| 	//     "$ref": "#/responses/MarkdownRender"
 | 	//     "$ref": "#/responses/MarkdownRender"
 | ||||||
| 	//   "422":
 | 	//   "422":
 | ||||||
| 	//     "$ref": "#/responses/validationError"
 | 	//     "$ref": "#/responses/validationError"
 | ||||||
| 
 | 	defer ctx.Req.Body.Close() | ||||||
| 	body, err := ioutil.ReadAll(ctx.Req.Body) | 	if err := markdown.RenderRaw(&markup.RenderContext{}, ctx.Req.Body, ctx.Resp); err != nil { | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "", err) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	_, err = ctx.Write(markdown.RenderRaw(body, "", false)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		ctx.InternalServerError(err) | 		ctx.InternalServerError(err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -143,7 +143,7 @@ func GlobalInit(ctx context.Context) { | ||||||
| 	NewServices() | 	NewServices() | ||||||
| 
 | 
 | ||||||
| 	highlight.NewContext() | 	highlight.NewContext() | ||||||
| 	external.RegisterParsers() | 	external.RegisterRenderers() | ||||||
| 	markup.Init() | 	markup.Init() | ||||||
| 
 | 
 | ||||||
| 	if setting.EnableSQLite3 { | 	if setting.EnableSQLite3 { | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
|  | @ -37,7 +38,15 @@ func Home(ctx *context.Context) { | ||||||
| 	ctx.Data["PageIsUserProfile"] = true | 	ctx.Data["PageIsUserProfile"] = true | ||||||
| 	ctx.Data["Title"] = org.DisplayName() | 	ctx.Data["Title"] = org.DisplayName() | ||||||
| 	if len(org.Description) != 0 { | 	if len(org.Description) != 0 { | ||||||
| 		ctx.Data["RenderedDescription"] = string(markdown.Render([]byte(org.Description), ctx.Repo.RepoLink, map[string]string{"mode": "document"})) | 		desc, err := markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 			Metas:     map[string]string{"mode": "document"}, | ||||||
|  | 		}, org.Description) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.Data["RenderedDescription"] = desc | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var orderBy models.SearchOrderBy | 	var orderBy models.SearchOrderBy | ||||||
|  |  | ||||||
|  | @ -10,7 +10,6 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"io/ioutil" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | @ -117,14 +116,7 @@ func setCsvCompareContext(ctx *context.Context) { | ||||||
| 			} | 			} | ||||||
| 			defer reader.Close() | 			defer reader.Close() | ||||||
| 
 | 
 | ||||||
| 			b, err := ioutil.ReadAll(reader) | 			return csv_module.CreateReaderAndGuessDelimiter(charset.ToUTF8WithFallbackReader(reader)) | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, err |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			b = charset.ToUTF8WithFallback(b) |  | ||||||
| 
 |  | ||||||
| 			return csv_module.CreateReaderAndGuessDelimiter(b), nil |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		baseReader, err := csvReaderFromCommit(baseCommit) | 		baseReader, err := csvReaderFromCommit(baseCommit) | ||||||
|  |  | ||||||
|  | @ -1131,8 +1131,14 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["IssueWatch"] = iw | 	ctx.Data["IssueWatch"] = iw | ||||||
| 
 | 
 | ||||||
| 	issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink, | 	issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
| 		ctx.Repo.Repository.ComposeMetas())) | 		URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, issue.Content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	repo := ctx.Repo.Repository | 	repo := ctx.Repo.Repository | ||||||
| 
 | 
 | ||||||
|  | @ -1289,9 +1295,14 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink, | 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
| 				ctx.Repo.Repository.ComposeMetas())) | 				URLPrefix: ctx.Repo.RepoLink, | ||||||
| 
 | 				Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 			}, comment.Content) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("RenderString", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			// Check tag.
 | 			// Check tag.
 | ||||||
| 			tag, ok = marked[comment.PosterID] | 			tag, ok = marked[comment.PosterID] | ||||||
| 			if ok { | 			if ok { | ||||||
|  | @ -1359,8 +1370,14 @@ func ViewIssue(ctx *context.Context) { | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview { | 		} else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview || comment.Type == models.CommentTypeDismissReview { | ||||||
| 			comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink, | 			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
| 				ctx.Repo.Repository.ComposeMetas())) | 				URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 				Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 			}, comment.Content) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("RenderString", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
| 			if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) { | 			if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) { | ||||||
| 				ctx.ServerError("LoadReview", err) | 				ctx.ServerError("LoadReview", err) | ||||||
| 				return | 				return | ||||||
|  | @ -1708,10 +1725,20 @@ func UpdateIssueContent(ctx *context.Context) { | ||||||
| 	files := ctx.QueryStrings("files[]") | 	files := ctx.QueryStrings("files[]") | ||||||
| 	if err := updateAttachments(issue, files); err != nil { | 	if err := updateAttachments(issue, files); err != nil { | ||||||
| 		ctx.ServerError("UpdateAttachments", err) | 		ctx.ServerError("UpdateAttachments", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	content, err := markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Query("context"), | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, issue.Content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||||
| 		"content":     string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), | 		"content":     content, | ||||||
| 		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content), | 		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  | @ -2125,10 +2152,20 @@ func UpdateCommentContent(ctx *context.Context) { | ||||||
| 	files := ctx.QueryStrings("files[]") | 	files := ctx.QueryStrings("files[]") | ||||||
| 	if err := updateAttachments(comment, files); err != nil { | 	if err := updateAttachments(comment, files); err != nil { | ||||||
| 		ctx.ServerError("UpdateAttachments", err) | 		ctx.ServerError("UpdateAttachments", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	content, err := markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Query("context"), | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, comment.Content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.JSON(http.StatusOK, map[string]interface{}{ | 	ctx.JSON(http.StatusOK, map[string]interface{}{ | ||||||
| 		"content":     string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())), | 		"content":     content, | ||||||
| 		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), | 		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -296,20 +296,13 @@ func LFSFileGet(ctx *context.Context) { | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		d, _ := ioutil.ReadAll(dataRc) | 		buf := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc)) | ||||||
| 		buf = charset.ToUTF8WithFallback(append(buf, d...)) |  | ||||||
| 
 | 
 | ||||||
| 		// Building code view blocks with line number on server side.
 | 		// Building code view blocks with line number on server side.
 | ||||||
| 		var fileContent string | 		fileContent, _ := ioutil.ReadAll(buf) | ||||||
| 		if content, err := charset.ToUTF8WithErr(buf); err != nil { |  | ||||||
| 			log.Error("ToUTF8WithErr: %v", err) |  | ||||||
| 			fileContent = string(buf) |  | ||||||
| 		} else { |  | ||||||
| 			fileContent = content |  | ||||||
| 		} |  | ||||||
| 
 | 
 | ||||||
| 		var output bytes.Buffer | 		var output bytes.Buffer | ||||||
| 		lines := strings.Split(fileContent, "\n") | 		lines := strings.Split(string(fileContent), "\n") | ||||||
| 		//Remove blank line at the end of file
 | 		//Remove blank line at the end of file
 | ||||||
| 		if len(lines) > 0 && lines[len(lines)-1] == "" { | 		if len(lines) > 0 && lines[len(lines)-1] == "" { | ||||||
| 			lines = lines[:len(lines)-1] | 			lines = lines[:len(lines)-1] | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/structs" | 	"code.gitea.io/gitea/modules/structs" | ||||||
|  | @ -84,7 +85,14 @@ func Milestones(ctx *context.Context) { | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	for _, m := range miles { | 	for _, m := range miles { | ||||||
| 		m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) | 		m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 			Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 		}, m.Content) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["Milestones"] = miles | 	ctx.Data["Milestones"] = miles | ||||||
| 
 | 
 | ||||||
|  | @ -269,7 +277,14 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	milestone.RenderedContent = string(markdown.Render([]byte(milestone.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) | 	milestone.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, milestone.Content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Title"] = milestone.Name | 	ctx.Data["Title"] = milestone.Name | ||||||
| 	ctx.Data["Milestone"] = milestone | 	ctx.Data["Milestone"] = milestone | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | @ -77,7 +78,14 @@ func Projects(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for i := range projects { | 	for i := range projects { | ||||||
| 		projects[i].RenderedContent = string(markdown.Render([]byte(projects[i].Description), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) | 		projects[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 			Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 		}, projects[i].Description) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Projects"] = projects | 	ctx.Data["Projects"] = projects | ||||||
|  | @ -311,7 +319,14 @@ func ViewProject(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 	ctx.Data["LinkedPRs"] = linkedPrsMap | 	ctx.Data["LinkedPRs"] = linkedPrsMap | ||||||
| 
 | 
 | ||||||
| 	project.RenderedContent = string(markdown.Render([]byte(project.Description), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas())) | 	project.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, project.Description) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) | 	ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(models.UnitTypeProjects) | ||||||
| 	ctx.Data["Project"] = project | 	ctx.Data["Project"] = project | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/convert" | 	"code.gitea.io/gitea/modules/convert" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/upload" | 	"code.gitea.io/gitea/modules/upload" | ||||||
|  | @ -132,7 +133,14 @@ func releasesOrTags(ctx *context.Context, isTagList bool) { | ||||||
| 			ctx.ServerError("calReleaseNumCommitsBehind", err) | 			ctx.ServerError("calReleaseNumCommitsBehind", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		r.Note = markdown.RenderString(r.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) | 		r.Note, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 			Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 		}, r.Note) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Releases"] = releases | 	ctx.Data["Releases"] = releases | ||||||
|  | @ -182,7 +190,14 @@ func SingleRelease(ctx *context.Context) { | ||||||
| 		ctx.ServerError("calReleaseNumCommitsBehind", err) | 		ctx.ServerError("calReleaseNumCommitsBehind", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	release.Note = markdown.RenderString(release.Note, ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()) | 	release.Note, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeMetas(), | ||||||
|  | 	}, release.Note) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("RenderString", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Releases"] = []*models.Release{release} | 	ctx.Data["Releases"] = []*models.Release{release} | ||||||
| 	ctx.HTML(http.StatusOK, tplReleases) | 	ctx.HTML(http.StatusOK, tplReleases) | ||||||
|  |  | ||||||
|  | @ -324,13 +324,26 @@ func renderDirectory(ctx *context.Context, treeLink string) { | ||||||
| 				ctx.Data["IsTextFile"] = true | 				ctx.Data["IsTextFile"] = true | ||||||
| 				ctx.Data["FileSize"] = fileSize | 				ctx.Data["FileSize"] = fileSize | ||||||
| 			} else { | 			} else { | ||||||
| 				d, _ := ioutil.ReadAll(dataRc) | 				rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc)) | ||||||
| 				buf = charset.ToUTF8WithFallback(append(buf, d...)) |  | ||||||
| 
 | 
 | ||||||
| 				if markupType := markup.Type(readmeFile.name); markupType != "" { | 				if markupType := markup.Type(readmeFile.name); markupType != "" { | ||||||
| 					ctx.Data["IsMarkup"] = true | 					ctx.Data["IsMarkup"] = true | ||||||
| 					ctx.Data["MarkupType"] = string(markupType) | 					ctx.Data["MarkupType"] = string(markupType) | ||||||
| 					ctx.Data["FileContent"] = string(markup.Render(readmeFile.name, buf, readmeTreelink, ctx.Repo.Repository.ComposeDocumentMetas())) | 					var result strings.Builder | ||||||
|  | 					err := markup.Render(&markup.RenderContext{ | ||||||
|  | 						Filename:  readmeFile.name, | ||||||
|  | 						URLPrefix: readmeTreelink, | ||||||
|  | 						Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
|  | 					}, rd, &result) | ||||||
|  | 					if err != nil { | ||||||
|  | 						log.Error("Render failed: %v then fallback", err) | ||||||
|  | 						bs, _ := ioutil.ReadAll(rd) | ||||||
|  | 						ctx.Data["FileContent"] = strings.ReplaceAll( | ||||||
|  | 							gotemplate.HTMLEscapeString(string(bs)), "\n", `<br>`, | ||||||
|  | 						) | ||||||
|  | 					} else { | ||||||
|  | 						ctx.Data["FileContent"] = result.String() | ||||||
|  | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.Data["IsRenderedHTML"] = true | 					ctx.Data["IsRenderedHTML"] = true | ||||||
| 					ctx.Data["FileContent"] = strings.ReplaceAll( | 					ctx.Data["FileContent"] = strings.ReplaceAll( | ||||||
|  | @ -481,21 +494,30 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | ||||||
| 			break | 			break | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		d, _ := ioutil.ReadAll(dataRc) | 		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc)) | ||||||
| 		buf = charset.ToUTF8WithFallback(append(buf, d...)) |  | ||||||
| 		readmeExist := markup.IsReadmeFile(blob.Name()) | 		readmeExist := markup.IsReadmeFile(blob.Name()) | ||||||
| 		ctx.Data["ReadmeExist"] = readmeExist | 		ctx.Data["ReadmeExist"] = readmeExist | ||||||
| 		if markupType := markup.Type(blob.Name()); markupType != "" { | 		if markupType := markup.Type(blob.Name()); markupType != "" { | ||||||
| 			ctx.Data["IsMarkup"] = true | 			ctx.Data["IsMarkup"] = true | ||||||
| 			ctx.Data["MarkupType"] = markupType | 			ctx.Data["MarkupType"] = markupType | ||||||
| 			ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeDocumentMetas())) | 			var result strings.Builder | ||||||
|  | 			err := markup.Render(&markup.RenderContext{ | ||||||
|  | 				Filename:  blob.Name(), | ||||||
|  | 				URLPrefix: path.Dir(treeLink), | ||||||
|  | 				Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
|  | 			}, rd, &result) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("Render", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			ctx.Data["FileContent"] = result.String() | ||||||
| 		} else if readmeExist { | 		} else if readmeExist { | ||||||
| 			ctx.Data["IsRenderedHTML"] = true | 			ctx.Data["IsRenderedHTML"] = true | ||||||
| 			ctx.Data["FileContent"] = strings.ReplaceAll( | 			ctx.Data["FileContent"] = strings.ReplaceAll( | ||||||
| 				gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`, | 				gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`, | ||||||
| 			) | 			) | ||||||
| 		} else { | 		} else { | ||||||
| 			buf = charset.ToUTF8WithFallback(buf) | 			buf, _ := ioutil.ReadAll(rd) | ||||||
| 			lineNums := linesBytesCount(buf) | 			lineNums := linesBytesCount(buf) | ||||||
| 			ctx.Data["NumLines"] = strconv.Itoa(lineNums) | 			ctx.Data["NumLines"] = strconv.Itoa(lineNums) | ||||||
| 			ctx.Data["NumLinesSet"] = true | 			ctx.Data["NumLinesSet"] = true | ||||||
|  | @ -532,11 +554,20 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if markupType := markup.Type(blob.Name()); markupType != "" { | 		if markupType := markup.Type(blob.Name()); markupType != "" { | ||||||
| 			d, _ := ioutil.ReadAll(dataRc) | 			rd := io.MultiReader(bytes.NewReader(buf), dataRc) | ||||||
| 			buf = append(buf, d...) |  | ||||||
| 			ctx.Data["IsMarkup"] = true | 			ctx.Data["IsMarkup"] = true | ||||||
| 			ctx.Data["MarkupType"] = markupType | 			ctx.Data["MarkupType"] = markupType | ||||||
| 			ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeDocumentMetas())) | 			var result strings.Builder | ||||||
|  | 			err := markup.Render(&markup.RenderContext{ | ||||||
|  | 				Filename:  blob.Name(), | ||||||
|  | 				URLPrefix: path.Dir(treeLink), | ||||||
|  | 				Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
|  | 			}, rd, &result) | ||||||
|  | 			if err != nil { | ||||||
|  | 				ctx.ServerError("Render", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			ctx.Data["FileContent"] = result.String() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| package repo | package repo | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io/ioutil" | 	"io/ioutil" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | @ -211,12 +212,34 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { | ||||||
| 		return nil, nil | 		return nil, nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	metas := ctx.Repo.Repository.ComposeDocumentMetas() | 	var rctx = &markup.RenderContext{ | ||||||
| 	ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas) | 		URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 		Metas:     ctx.Repo.Repository.ComposeDocumentMetas(), | ||||||
|  | 		IsWiki:    true, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var buf strings.Builder | ||||||
|  | 	if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil { | ||||||
|  | 		ctx.ServerError("Render", err) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	ctx.Data["content"] = buf.String() | ||||||
|  | 
 | ||||||
|  | 	buf.Reset() | ||||||
|  | 	if err := markdown.Render(rctx, bytes.NewReader(sidebarContent), &buf); err != nil { | ||||||
|  | 		ctx.ServerError("Render", err) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
| 	ctx.Data["sidebarPresent"] = sidebarContent != nil | 	ctx.Data["sidebarPresent"] = sidebarContent != nil | ||||||
| 	ctx.Data["sidebarContent"] = markdown.RenderWiki(sidebarContent, ctx.Repo.RepoLink, metas) | 	ctx.Data["sidebarContent"] = buf.String() | ||||||
|  | 
 | ||||||
|  | 	buf.Reset() | ||||||
|  | 	if err := markdown.Render(rctx, bytes.NewReader(footerContent), &buf); err != nil { | ||||||
|  | 		ctx.ServerError("Render", err) | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
| 	ctx.Data["footerPresent"] = footerContent != nil | 	ctx.Data["footerPresent"] = footerContent != nil | ||||||
| 	ctx.Data["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas) | 	ctx.Data["footerContent"] = buf.String() | ||||||
| 
 | 
 | ||||||
| 	// get commit count - wiki revisions
 | 	// get commit count - wiki revisions
 | ||||||
| 	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) | 	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename) | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues" | 	issue_indexer "code.gitea.io/gitea/modules/indexer/issues" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | @ -267,7 +268,15 @@ func Milestones(ctx *context.Context) { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		milestones[i].RenderedContent = string(markdown.Render([]byte(milestones[i].Content), milestones[i].Repo.Link(), milestones[i].Repo.ComposeMetas())) | 		milestones[i].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: milestones[i].Repo.Link(), | ||||||
|  | 			Metas:     milestones[i].Repo.ComposeMetas(), | ||||||
|  | 		}, milestones[i].Content) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if milestones[i].Repo.IsTimetrackerEnabled() { | 		if milestones[i].Repo.IsTimetrackerEnabled() { | ||||||
| 			err := milestones[i].LoadTotalTrackedTime() | 			err := milestones[i].LoadTotalTrackedTime() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | @ -110,7 +111,15 @@ func Profile(ctx *context.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(ctxUser.Description) != 0 { | 	if len(ctxUser.Description) != 0 { | ||||||
| 		ctx.Data["RenderedDescription"] = string(markdown.Render([]byte(ctxUser.Description), ctx.Repo.RepoLink, map[string]string{"mode": "document"})) | 		content, err := markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 			URLPrefix: ctx.Repo.RepoLink, | ||||||
|  | 			Metas:     map[string]string{"mode": "document"}, | ||||||
|  | 		}, ctxUser.Description) | ||||||
|  | 		if err != nil { | ||||||
|  | 			ctx.ServerError("RenderString", err) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.Data["RenderedDescription"] = content | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID) | 	showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID) | ||||||
|  |  | ||||||
|  | @ -95,11 +95,17 @@ func TestCSVDiff(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		var baseReader *csv.Reader | 		var baseReader *csv.Reader | ||||||
| 		if len(c.base) > 0 { | 		if len(c.base) > 0 { | ||||||
| 			baseReader = csv_module.CreateReaderAndGuessDelimiter([]byte(c.base)) | 			baseReader, err = csv_module.CreateReaderAndGuessDelimiter(strings.NewReader(c.base)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Errorf("CreateReaderAndGuessDelimiter failed: %s", err) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		var headReader *csv.Reader | 		var headReader *csv.Reader | ||||||
| 		if len(c.head) > 0 { | 		if len(c.head) > 0 { | ||||||
| 			headReader = csv_module.CreateReaderAndGuessDelimiter([]byte(c.head)) | 			headReader, err = csv_module.CreateReaderAndGuessDelimiter(strings.NewReader(c.head)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Errorf("CreateReaderAndGuessDelimiter failed: %s", err) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		result, err := CreateCsvDiff(diff.Files[0], baseReader, headReader) | 		result, err := CreateCsvDiff(diff.Files[0], baseReader, headReader) | ||||||
|  |  | ||||||
|  | @ -174,8 +174,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | ||||||
| 	SendAsync(msg) | 	SendAsync(msg) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []string, fromMention bool, info string) []*Message { | func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []string, fromMention bool, info string) ([]*Message, error) { | ||||||
| 
 |  | ||||||
| 	var ( | 	var ( | ||||||
| 		subject string | 		subject string | ||||||
| 		link    string | 		link    string | ||||||
|  | @ -199,7 +198,14 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// This is the body of the new issue or comment, not the mail body
 | 	// This is the body of the new issue or comment, not the mail body
 | ||||||
| 	body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas())) | 	body, err := markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: ctx.Issue.Repo.HTMLURL(), | ||||||
|  | 		Metas:     ctx.Issue.Repo.ComposeMetas(), | ||||||
|  | 	}, ctx.Content) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) | 	actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) | ||||||
| 
 | 
 | ||||||
| 	if actName != "new" { | 	if actName != "new" { | ||||||
|  | @ -240,14 +246,13 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str | ||||||
| 	// TODO: i18n templates?
 | 	// TODO: i18n templates?
 | ||||||
| 	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { | 	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { | ||||||
| 		subject = sanitizeSubject(mailSubject.String()) | 		subject = sanitizeSubject(mailSubject.String()) | ||||||
|  | 		if subject == "" { | ||||||
|  | 			subject = fallback | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err) | 		log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if subject == "" { |  | ||||||
| 		subject = fallback |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	subject = emoji.ReplaceAliases(subject) | 	subject = emoji.ReplaceAliases(subject) | ||||||
| 
 | 
 | ||||||
| 	mailMeta["Subject"] = subject | 	mailMeta["Subject"] = subject | ||||||
|  | @ -275,7 +280,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str | ||||||
| 		msgs = append(msgs, msg) | 		msgs = append(msgs, msg) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return msgs | 	return msgs, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func sanitizeSubject(subject string) string { | func sanitizeSubject(subject string) string { | ||||||
|  | @ -288,21 +293,26 @@ func sanitizeSubject(subject string) string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SendIssueAssignedMail composes and sends issue assigned email
 | // SendIssueAssignedMail composes and sends issue assigned email
 | ||||||
| func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) { | func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) error { | ||||||
| 	langMap := make(map[string][]string) | 	langMap := make(map[string][]string) | ||||||
| 	for _, user := range recipients { | 	for _, user := range recipients { | ||||||
| 		langMap[user.Language] = append(langMap[user.Language], user.Email) | 		langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for lang, tos := range langMap { | 	for lang, tos := range langMap { | ||||||
| 		SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ | 		msgs, err := composeIssueCommentMessages(&mailCommentContext{ | ||||||
| 			Issue:      issue, | 			Issue:      issue, | ||||||
| 			Doer:       doer, | 			Doer:       doer, | ||||||
| 			ActionType: models.ActionType(0), | 			ActionType: models.ActionType(0), | ||||||
| 			Content:    content, | 			Content:    content, | ||||||
| 			Comment:    comment, | 			Comment:    comment, | ||||||
| 		}, lang, tos, false, "issue assigned")) | 		}, lang, tos, false, "issue assigned") | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		SendAsyncs(msgs) | ||||||
| 	} | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // actionToTemplate returns the type and name of the action facing the user
 | // actionToTemplate returns the type and name of the action facing the user
 | ||||||
|  |  | ||||||
|  | @ -146,7 +146,11 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visite | ||||||
| 		// working backwards from the last (possibly) incomplete batch. If len(receivers) can be 0 this
 | 		// working backwards from the last (possibly) incomplete batch. If len(receivers) can be 0 this
 | ||||||
| 		// starting condition will need to be changed slightly
 | 		// starting condition will need to be changed slightly
 | ||||||
| 		for i := ((len(receivers) - 1) / MailBatchSize) * MailBatchSize; i >= 0; i -= MailBatchSize { | 		for i := ((len(receivers) - 1) / MailBatchSize) * MailBatchSize; i >= 0; i -= MailBatchSize { | ||||||
| 			SendAsyncs(composeIssueCommentMessages(ctx, lang, receivers[i:], fromMention, "issue comments")) | 			msgs, err := composeIssueCommentMessages(ctx, lang, receivers[i:], fromMention, "issue comments") | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			SendAsyncs(msgs) | ||||||
| 			receivers = receivers[:i] | 			receivers = receivers[:i] | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import ( | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/markup" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/translation" | 	"code.gitea.io/gitea/modules/translation" | ||||||
|  | @ -48,7 +49,15 @@ func MailNewRelease(rel *models.Release) { | ||||||
| func mailNewRelease(lang string, tos []string, rel *models.Release) { | func mailNewRelease(lang string, tos []string, rel *models.Release) { | ||||||
| 	locale := translation.NewLocale(lang) | 	locale := translation.NewLocale(lang) | ||||||
| 
 | 
 | ||||||
| 	rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) | 	var err error | ||||||
|  | 	rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{ | ||||||
|  | 		URLPrefix: rel.Repo.Link(), | ||||||
|  | 		Metas:     rel.Repo.ComposeMetas(), | ||||||
|  | 	}, rel.Note) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("markdown.RenderString(%d): %v", rel.RepoID, err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) | 	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) | ||||||
| 	mailMeta := map[string]interface{}{ | 	mailMeta := map[string]interface{}{ | ||||||
|  |  | ||||||
|  | @ -58,8 +58,9 @@ func TestComposeIssueCommentMessage(t *testing.T) { | ||||||
| 	InitMailRender(stpl, btpl) | 	InitMailRender(stpl, btpl) | ||||||
| 
 | 
 | ||||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | 	msgs, err := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | ||||||
| 		Content: "test body", Comment: comment}, "en-US", tos, false, "issue comment") | 		Content: "test body", Comment: comment}, "en-US", tos, false, "issue comment") | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, msgs, 2) | 	assert.Len(t, msgs, 2) | ||||||
| 	gomailMsg := msgs[0].ToMessage() | 	gomailMsg := msgs[0].ToMessage() | ||||||
| 	mailto := gomailMsg.GetHeader("To") | 	mailto := gomailMsg.GetHeader("To") | ||||||
|  | @ -92,8 +93,9 @@ func TestComposeIssueMessage(t *testing.T) { | ||||||
| 	InitMailRender(stpl, btpl) | 	InitMailRender(stpl, btpl) | ||||||
| 
 | 
 | ||||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue, | 	msgs, err := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue, | ||||||
| 		Content: "test body"}, "en-US", tos, false, "issue create") | 		Content: "test body"}, "en-US", tos, false, "issue create") | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, msgs, 2) | 	assert.Len(t, msgs, 2) | ||||||
| 
 | 
 | ||||||
| 	gomailMsg := msgs[0].ToMessage() | 	gomailMsg := msgs[0].ToMessage() | ||||||
|  | @ -218,7 +220,8 @@ func TestTemplateServices(t *testing.T) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message { | func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message { | ||||||
| 	msgs := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info) | 	msgs, err := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info) | ||||||
|  | 	assert.NoError(t, err) | ||||||
| 	assert.Len(t, msgs, 1) | 	assert.Len(t, msgs, 1) | ||||||
| 	return msgs[0] | 	return msgs[0] | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue