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")
 | 
			
		||||
	//routers.GlobalInit()
 | 
			
		||||
	external.RegisterParsers()
 | 
			
		||||
	external.RegisterRenderers()
 | 
			
		||||
	markup.Init()
 | 
			
		||||
	c := routes.NormalRoutes()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/references"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
| 
						 | 
				
			
			@ -1178,8 +1179,13 @@ func findCodeComments(e Engine, opts FindCommentsOptions, issue *Issue, currentU
 | 
			
		|||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
 | 
			
		||||
			issue.Repo.ComposeMetas()))
 | 
			
		||||
		var err error
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -863,7 +863,10 @@ func (repo *Repository) getUsersWithAccessMode(e Engine, mode AccessMode) (_ []*
 | 
			
		|||
 | 
			
		||||
// DescriptionHTML does special handles to description and return HTML string.
 | 
			
		||||
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 {
 | 
			
		||||
		log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
 | 
			
		||||
		return template.HTML(markup.Sanitize(repo.Description))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,13 +5,14 @@
 | 
			
		|||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/gobwas/glob"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -49,9 +50,9 @@ func (gt GiteaTemplate) Globs() []glob.Glob {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	gt.globs = make([]glob.Glob, 0)
 | 
			
		||||
	lines := strings.Split(string(util.NormalizeEOL(gt.Content)), "\n")
 | 
			
		||||
	for _, line := range lines {
 | 
			
		||||
		line = strings.TrimSpace(line)
 | 
			
		||||
	scanner := bufio.NewScanner(bytes.NewReader(gt.Content))
 | 
			
		||||
	for scanner.Scan() {
 | 
			
		||||
		line := strings.TrimSpace(scanner.Text())
 | 
			
		||||
		if line == "" || strings.HasPrefix(line, "#") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,8 @@ package charset
 | 
			
		|||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +23,33 @@ import (
 | 
			
		|||
// UTF8BOM is the utf-8 byte-order marker
 | 
			
		||||
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
 | 
			
		||||
func ToUTF8WithErr(content []byte) (string, error) {
 | 
			
		||||
	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
 | 
			
		||||
func ToUTF8WithFallback(content []byte) []byte {
 | 
			
		||||
	charsetLabel, err := DetectEncoding(content)
 | 
			
		||||
	if err != nil || charsetLabel == "UTF-8" {
 | 
			
		||||
		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)
 | 
			
		||||
	bs, _ := ioutil.ReadAll(ToUTF8WithFallbackReader(bytes.NewReader(content)))
 | 
			
		||||
	return bs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToUTF8 converts content to UTF8 encoding and ignore error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,9 @@ package csv
 | 
			
		|||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/csv"
 | 
			
		||||
	stdcsv "encoding/csv"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,17 +20,31 @@ import (
 | 
			
		|||
var quoteRegexp = regexp.MustCompile(`["'][\s\S]+?["']`)
 | 
			
		||||
 | 
			
		||||
// CreateReader creates a csv.Reader with the given delimiter.
 | 
			
		||||
func CreateReader(rawBytes []byte, delimiter rune) *csv.Reader {
 | 
			
		||||
	rd := csv.NewReader(bytes.NewReader(rawBytes))
 | 
			
		||||
func CreateReader(input io.Reader, delimiter rune) *stdcsv.Reader {
 | 
			
		||||
	rd := stdcsv.NewReader(input)
 | 
			
		||||
	rd.Comma = delimiter
 | 
			
		||||
	rd.TrimLeadingSpace = true
 | 
			
		||||
	return rd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateReaderAndGuessDelimiter tries to guess the field delimiter from the content and creates a csv.Reader.
 | 
			
		||||
func CreateReaderAndGuessDelimiter(rawBytes []byte) *csv.Reader {
 | 
			
		||||
	delimiter := guessDelimiter(rawBytes)
 | 
			
		||||
	return CreateReader(rawBytes, delimiter)
 | 
			
		||||
func CreateReaderAndGuessDelimiter(rd io.Reader) (*stdcsv.Reader, error) {
 | 
			
		||||
	var data = make([]byte, 1e4)
 | 
			
		||||
	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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,20 +5,23 @@
 | 
			
		|||
package csv
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCreateReader(t *testing.T) {
 | 
			
		||||
	rd := CreateReader([]byte{}, ',')
 | 
			
		||||
	rd := CreateReader(bytes.NewReader([]byte{}), ',')
 | 
			
		||||
	assert.Equal(t, ',', rd.Comma)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateReaderAndGuessDelimiter(t *testing.T) {
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,11 @@
 | 
			
		|||
package markup
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"html"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strconv"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/csv"
 | 
			
		||||
| 
						 | 
				
			
			@ -16,55 +18,89 @@ import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	markup.RegisterParser(Parser{})
 | 
			
		||||
	markup.RegisterRenderer(Renderer{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parser implements markup.Parser for csv files
 | 
			
		||||
type Parser struct {
 | 
			
		||||
// Renderer implements markup.Renderer for csv files
 | 
			
		||||
type Renderer struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name implements markup.Parser
 | 
			
		||||
func (Parser) Name() string {
 | 
			
		||||
// Name implements markup.Renderer
 | 
			
		||||
func (Renderer) Name() string {
 | 
			
		||||
	return "csv"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NeedPostProcess implements markup.Parser
 | 
			
		||||
func (Parser) NeedPostProcess() bool { return false }
 | 
			
		||||
// NeedPostProcess implements markup.Renderer
 | 
			
		||||
func (Renderer) NeedPostProcess() bool { return false }
 | 
			
		||||
 | 
			
		||||
// Extensions implements markup.Parser
 | 
			
		||||
func (Parser) Extensions() []string {
 | 
			
		||||
// Extensions implements markup.Renderer
 | 
			
		||||
func (Renderer) Extensions() []string {
 | 
			
		||||
	return []string{".csv", ".tsv"}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render implements markup.Parser
 | 
			
		||||
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
 | 
			
		||||
	var tmpBlock bytes.Buffer
 | 
			
		||||
func writeField(w io.Writer, element, class, field string) error {
 | 
			
		||||
	if _, err := io.WriteString(w, "<"); err != nil {
 | 
			
		||||
		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)) {
 | 
			
		||||
		tmpBlock.WriteString("<pre>")
 | 
			
		||||
		tmpBlock.WriteString(html.EscapeString(string(rawBytes)))
 | 
			
		||||
		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("\"")
 | 
			
		||||
		if _, err := tmpBlock.WriteString("<pre>"); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		tmpBlock.WriteString(">")
 | 
			
		||||
		tmpBlock.WriteString(html.EscapeString(field))
 | 
			
		||||
		tmpBlock.WriteString("</")
 | 
			
		||||
		tmpBlock.WriteString(element)
 | 
			
		||||
		tmpBlock.WriteString(">")
 | 
			
		||||
		if _, err := tmpBlock.WriteString(html.EscapeString(string(rawBytes))); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		_, err = tmpBlock.WriteString("</pre>")
 | 
			
		||||
		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
 | 
			
		||||
	for {
 | 
			
		||||
		fields, err := rd.Read()
 | 
			
		||||
| 
						 | 
				
			
			@ -74,20 +110,29 @@ func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string,
 | 
			
		|||
		if err != nil {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		tmpBlock.WriteString("<tr>")
 | 
			
		||||
		if _, err := tmpBlock.WriteString("<tr>"); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		element := "td"
 | 
			
		||||
		if row == 1 {
 | 
			
		||||
			element = "th"
 | 
			
		||||
		}
 | 
			
		||||
		writeField(element, "line-num", strconv.Itoa(row))
 | 
			
		||||
		for _, field := range fields {
 | 
			
		||||
			writeField(element, "", field)
 | 
			
		||||
		if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		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++
 | 
			
		||||
	}
 | 
			
		||||
	tmpBlock.WriteString("</table>")
 | 
			
		||||
 | 
			
		||||
	return tmpBlock.Bytes()
 | 
			
		||||
	if _, err = tmpBlock.WriteString("</table>"); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return tmpBlock.Flush()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,13 +5,16 @@
 | 
			
		|||
package markup
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestRenderCSV(t *testing.T) {
 | 
			
		||||
	var parser Parser
 | 
			
		||||
	var render Renderer
 | 
			
		||||
	var kases = map[string]string{
 | 
			
		||||
		"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>",
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +23,9 @@ func TestRenderCSV(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range kases {
 | 
			
		||||
		res := parser.Render([]byte(k), "", nil, false)
 | 
			
		||||
		assert.EqualValues(t, v, string(res))
 | 
			
		||||
		var buf strings.Builder
 | 
			
		||||
		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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
| 
						 | 
				
			
			@ -19,32 +19,32 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RegisterParsers registers all supported third part parsers according settings
 | 
			
		||||
func RegisterParsers() {
 | 
			
		||||
	for _, parser := range setting.ExternalMarkupParsers {
 | 
			
		||||
		if parser.Enabled && parser.Command != "" && len(parser.FileExtensions) > 0 {
 | 
			
		||||
			markup.RegisterParser(&Parser{parser})
 | 
			
		||||
// RegisterRenderers registers all supported third part renderers according settings
 | 
			
		||||
func RegisterRenderers() {
 | 
			
		||||
	for _, renderer := range setting.ExternalMarkupRenderers {
 | 
			
		||||
		if renderer.Enabled && renderer.Command != "" && len(renderer.FileExtensions) > 0 {
 | 
			
		||||
			markup.RegisterRenderer(&Renderer{renderer})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parser implements markup.Parser for external tools
 | 
			
		||||
type Parser struct {
 | 
			
		||||
	setting.MarkupParser
 | 
			
		||||
// Renderer implements markup.Renderer for external tools
 | 
			
		||||
type Renderer struct {
 | 
			
		||||
	setting.MarkupRenderer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name returns the external tool name
 | 
			
		||||
func (p *Parser) Name() string {
 | 
			
		||||
func (p *Renderer) Name() string {
 | 
			
		||||
	return p.MarkupName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NeedPostProcess implements markup.Parser
 | 
			
		||||
func (p *Parser) NeedPostProcess() bool {
 | 
			
		||||
	return p.MarkupParser.NeedPostProcess
 | 
			
		||||
// NeedPostProcess implements markup.Renderer
 | 
			
		||||
func (p *Renderer) NeedPostProcess() bool {
 | 
			
		||||
	return p.MarkupRenderer.NeedPostProcess
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Extensions returns the supported extensions of the tool
 | 
			
		||||
func (p *Parser) Extensions() []string {
 | 
			
		||||
func (p *Renderer) Extensions() []string {
 | 
			
		||||
	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.
 | 
			
		||||
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 (
 | 
			
		||||
		bs           []byte
 | 
			
		||||
		buf          = bytes.NewBuffer(bs)
 | 
			
		||||
		rd           = bytes.NewReader(rawBytes)
 | 
			
		||||
		urlRawPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
 | 
			
		||||
 | 
			
		||||
		command = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), urlPrefix,
 | 
			
		||||
		urlRawPrefix = strings.Replace(ctx.URLPrefix, "/src/", "/raw/", 1)
 | 
			
		||||
		command      = strings.NewReplacer(envMark("GITEA_PREFIX_SRC"), ctx.URLPrefix,
 | 
			
		||||
			envMark("GITEA_PREFIX_RAW"), urlRawPrefix).Replace(p.Command)
 | 
			
		||||
		commands = strings.Fields(command)
 | 
			
		||||
		args     = commands[1:]
 | 
			
		||||
| 
						 | 
				
			
			@ -73,8 +69,7 @@ func (p *Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]stri
 | 
			
		|||
		// write to temp file
 | 
			
		||||
		f, err := ioutil.TempFile("", "gitea_input")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("%s create temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
			return []byte("")
 | 
			
		||||
			return fmt.Errorf("%s create temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
		}
 | 
			
		||||
		tmpPath := f.Name()
 | 
			
		||||
		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 {
 | 
			
		||||
			f.Close()
 | 
			
		||||
			log.Error("%s write data to temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
			return []byte("")
 | 
			
		||||
			return fmt.Errorf("%s write data to temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = f.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("%s close temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
			return []byte("")
 | 
			
		||||
			return fmt.Errorf("%s close temp file when rendering %s failed: %v", p.Name(), p.Command, err)
 | 
			
		||||
		}
 | 
			
		||||
		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.Env = append(
 | 
			
		||||
		os.Environ(),
 | 
			
		||||
		"GITEA_PREFIX_SRC="+urlPrefix,
 | 
			
		||||
		"GITEA_PREFIX_SRC="+ctx.URLPrefix,
 | 
			
		||||
		"GITEA_PREFIX_RAW="+urlRawPrefix,
 | 
			
		||||
	)
 | 
			
		||||
	if !p.IsInputFile {
 | 
			
		||||
		cmd.Stdin = rd
 | 
			
		||||
		cmd.Stdin = input
 | 
			
		||||
	}
 | 
			
		||||
	cmd.Stdout = buf
 | 
			
		||||
	cmd.Stdout = output
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		log.Error("%s render run command %s %v failed: %v", p.Name(), commands[0], args, err)
 | 
			
		||||
		return []byte("")
 | 
			
		||||
		return fmt.Errorf("%s render run command %s %v failed: %v", p.Name(), commands[0], args, err)
 | 
			
		||||
	}
 | 
			
		||||
	return buf.Bytes()
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,8 @@ package markup
 | 
			
		|||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
| 
						 | 
				
			
			@ -144,7 +146,7 @@ func (p *postProcessError) Error() string {
 | 
			
		|||
	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{
 | 
			
		||||
	fullIssuePatternProcessor,
 | 
			
		||||
| 
						 | 
				
			
			@ -159,34 +161,17 @@ var defaultProcessors = []processor{
 | 
			
		|||
	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
 | 
			
		||||
// data, and ensures its validity. Transformations include: replacing links and
 | 
			
		||||
// emails with HTML links, parsing shortlinks in the format of [[Link]], like
 | 
			
		||||
// MediaWiki, linking issues in the format #ID, and mentions in the format
 | 
			
		||||
// @user, and others.
 | 
			
		||||
func PostProcess(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
	urlPrefix string,
 | 
			
		||||
	metas map[string]string,
 | 
			
		||||
	isWikiMarkdown bool,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	// create the context from the parameters
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		metas:          metas,
 | 
			
		||||
		urlPrefix:      urlPrefix,
 | 
			
		||||
		isWikiMarkdown: isWikiMarkdown,
 | 
			
		||||
		procs:          defaultProcessors,
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.postProcess(rawHTML)
 | 
			
		||||
	ctx *RenderContext,
 | 
			
		||||
	input io.Reader,
 | 
			
		||||
	output io.Writer,
 | 
			
		||||
) error {
 | 
			
		||||
	return postProcess(ctx, defaultProcessors, input, output)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var commitMessageProcessors = []processor{
 | 
			
		||||
| 
						 | 
				
			
			@ -205,23 +190,18 @@ var commitMessageProcessors = []processor{
 | 
			
		|||
// the shortLinkProcessor and will add a defaultLinkProcessor if defaultLink is
 | 
			
		||||
// set, which changes every text node into a link to the passed default link.
 | 
			
		||||
func RenderCommitMessage(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
	urlPrefix, defaultLink string,
 | 
			
		||||
	metas map[string]string,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		metas:     metas,
 | 
			
		||||
		urlPrefix: urlPrefix,
 | 
			
		||||
		procs:     commitMessageProcessors,
 | 
			
		||||
	}
 | 
			
		||||
	if defaultLink != "" {
 | 
			
		||||
	ctx *RenderContext,
 | 
			
		||||
	content string,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
	var procs = commitMessageProcessors
 | 
			
		||||
	if ctx.DefaultLink != "" {
 | 
			
		||||
		// we don't have to fear data races, because being
 | 
			
		||||
		// commitMessageProcessors of fixed len and cap, every time we append
 | 
			
		||||
		// something to it the slice is realloc+copied, so append always
 | 
			
		||||
		// 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{
 | 
			
		||||
| 
						 | 
				
			
			@ -245,83 +225,72 @@ var emojiProcessors = []processor{
 | 
			
		|||
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
 | 
			
		||||
// which changes every text node into a link to the passed default link.
 | 
			
		||||
func RenderCommitMessageSubject(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
	urlPrefix, defaultLink string,
 | 
			
		||||
	metas map[string]string,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		metas:     metas,
 | 
			
		||||
		urlPrefix: urlPrefix,
 | 
			
		||||
		procs:     commitMessageSubjectProcessors,
 | 
			
		||||
	}
 | 
			
		||||
	if defaultLink != "" {
 | 
			
		||||
	ctx *RenderContext,
 | 
			
		||||
	content string,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
	var procs = commitMessageSubjectProcessors
 | 
			
		||||
	if ctx.DefaultLink != "" {
 | 
			
		||||
		// we don't have to fear data races, because being
 | 
			
		||||
		// commitMessageSubjectProcessors of fixed len and cap, every time we
 | 
			
		||||
		// append something to it the slice is realloc+copied, so append always
 | 
			
		||||
		// 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
 | 
			
		||||
func RenderIssueTitle(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
	urlPrefix string,
 | 
			
		||||
	metas map[string]string,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		metas:     metas,
 | 
			
		||||
		urlPrefix: urlPrefix,
 | 
			
		||||
		procs: []processor{
 | 
			
		||||
			issueIndexPatternProcessor,
 | 
			
		||||
			sha1CurrentPatternProcessor,
 | 
			
		||||
			emojiShortCodeProcessor,
 | 
			
		||||
			emojiProcessor,
 | 
			
		||||
		},
 | 
			
		||||
	ctx *RenderContext,
 | 
			
		||||
	title string,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
	return renderProcessString(ctx, []processor{
 | 
			
		||||
		issueIndexPatternProcessor,
 | 
			
		||||
		sha1CurrentPatternProcessor,
 | 
			
		||||
		emojiShortCodeProcessor,
 | 
			
		||||
		emojiProcessor,
 | 
			
		||||
	}, title)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
// use a single special linkProcessor.
 | 
			
		||||
func RenderDescriptionHTML(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
	urlPrefix string,
 | 
			
		||||
	metas map[string]string,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		metas:     metas,
 | 
			
		||||
		urlPrefix: urlPrefix,
 | 
			
		||||
		procs: []processor{
 | 
			
		||||
			descriptionLinkProcessor,
 | 
			
		||||
			emojiShortCodeProcessor,
 | 
			
		||||
			emojiProcessor,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.postProcess(rawHTML)
 | 
			
		||||
	ctx *RenderContext,
 | 
			
		||||
	content string,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
	return renderProcessString(ctx, []processor{
 | 
			
		||||
		descriptionLinkProcessor,
 | 
			
		||||
		emojiShortCodeProcessor,
 | 
			
		||||
		emojiProcessor,
 | 
			
		||||
	}, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderEmoji for when we want to just process emoji and shortcodes
 | 
			
		||||
// in various places it isn't already run through the normal markdown procesor
 | 
			
		||||
func RenderEmoji(
 | 
			
		||||
	rawHTML []byte,
 | 
			
		||||
) ([]byte, error) {
 | 
			
		||||
	ctx := &postProcessCtx{
 | 
			
		||||
		procs: emojiProcessors,
 | 
			
		||||
	}
 | 
			
		||||
	return ctx.postProcess(rawHTML)
 | 
			
		||||
	content string,
 | 
			
		||||
) (string, error) {
 | 
			
		||||
	return renderProcessString(&RenderContext{}, emojiProcessors, content)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var tagCleaner = regexp.MustCompile(`<((?:/?\w+/\w+)|(?:/[\w ]+/)|(/?[hH][tT][mM][lL]\b)|(/?[hH][eE][aA][dD]\b))`)
 | 
			
		||||
var nulCleaner = strings.NewReplacer("\000", "")
 | 
			
		||||
 | 
			
		||||
func (ctx *postProcessCtx) postProcess(rawHTML []byte) ([]byte, error) {
 | 
			
		||||
	if ctx.procs == nil {
 | 
			
		||||
		ctx.procs = defaultProcessors
 | 
			
		||||
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
 | 
			
		||||
	// FIXME: don't read all content to memory
 | 
			
		||||
	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))
 | 
			
		||||
	// prepend "<html><body>"
 | 
			
		||||
	_, _ = res.WriteString("<html><body>")
 | 
			
		||||
| 
						 | 
				
			
			@ -335,11 +304,11 @@ func (ctx *postProcessCtx) postProcess(rawHTML []byte) ([]byte, error) {
 | 
			
		|||
	// parse the HTML
 | 
			
		||||
	nodes, err := html.ParseFragment(res, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, &postProcessError{"invalid HTML", err}
 | 
			
		||||
		return &postProcessError{"invalid HTML", err}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, node := range nodes {
 | 
			
		||||
		ctx.visitNode(node, true)
 | 
			
		||||
		visitNode(ctx, procs, node, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.
 | 
			
		||||
	for _, node := range nodes {
 | 
			
		||||
		err = html.Render(res, node)
 | 
			
		||||
	for _, node := range newNodes {
 | 
			
		||||
		err = html.Render(output, node)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, &postProcessError{"error rendering processed HTML", err}
 | 
			
		||||
			return &postProcessError{"error rendering processed HTML", err}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Everything done successfully, return parsed data.
 | 
			
		||||
	return res.Bytes(), nil
 | 
			
		||||
	return 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
 | 
			
		||||
	for idx, attr := range node.Attr {
 | 
			
		||||
		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 {
 | 
			
		||||
	case html.TextNode:
 | 
			
		||||
		if visitText {
 | 
			
		||||
			ctx.textNode(node)
 | 
			
		||||
			textNode(ctx, procs, node)
 | 
			
		||||
		}
 | 
			
		||||
	case html.ElementNode:
 | 
			
		||||
		if node.Data == "img" {
 | 
			
		||||
| 
						 | 
				
			
			@ -410,8 +371,8 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
 | 
			
		|||
				}
 | 
			
		||||
				link := []byte(attr.Val)
 | 
			
		||||
				if len(link) > 0 && !IsLink(link) {
 | 
			
		||||
					prefix := ctx.urlPrefix
 | 
			
		||||
					if ctx.isWikiMarkdown {
 | 
			
		||||
					prefix := ctx.URLPrefix
 | 
			
		||||
					if ctx.IsWiki {
 | 
			
		||||
						prefix = util.URLJoin(prefix, "wiki", "raw")
 | 
			
		||||
					}
 | 
			
		||||
					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 {
 | 
			
		||||
			ctx.visitNode(n, visitText)
 | 
			
		||||
			visitNode(ctx, procs, n, visitText)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// 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
 | 
			
		||||
// all kinds of special links handled by the post-processing.
 | 
			
		||||
func (ctx *postProcessCtx) textNode(node *html.Node) {
 | 
			
		||||
	for _, processor := range ctx.procs {
 | 
			
		||||
func textNode(ctx *RenderContext, procs []processor, node *html.Node) {
 | 
			
		||||
	for _, processor := range procs {
 | 
			
		||||
		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
 | 
			
		||||
	found, loc := references.FindFirstMentionBytes([]byte(node.Data))
 | 
			
		||||
	if !found {
 | 
			
		||||
| 
						 | 
				
			
			@ -617,26 +578,26 @@ func mentionProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
	}
 | 
			
		||||
	mention := node.Data[loc.Start:loc.End]
 | 
			
		||||
	var teams string
 | 
			
		||||
	teams, ok := ctx.metas["teams"]
 | 
			
		||||
	teams, ok := ctx.Metas["teams"]
 | 
			
		||||
	// FIXME: util.URLJoin may not be necessary here:
 | 
			
		||||
	// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | 
			
		||||
	// is an AppSubURL link we can probably fallback to concatenation.
 | 
			
		||||
	// team mention should follow @orgName/teamName style
 | 
			
		||||
	if ok && strings.Contains(mention, "/") {
 | 
			
		||||
		mentionOrgAndTeam := strings.Split(mention, "/")
 | 
			
		||||
		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"))
 | 
			
		||||
		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"))
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) {
 | 
			
		||||
func shortLinkProcessorFull(ctx *RenderContext, node *html.Node, noLink bool) {
 | 
			
		||||
	m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -741,13 +702,13 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) {
 | 
			
		|||
			link = url.PathEscape(link)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	urlPrefix := ctx.urlPrefix
 | 
			
		||||
	urlPrefix := ctx.URLPrefix
 | 
			
		||||
	if image {
 | 
			
		||||
		if !absoluteLink {
 | 
			
		||||
			if IsSameDomain(urlPrefix) {
 | 
			
		||||
				urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
 | 
			
		||||
			}
 | 
			
		||||
			if ctx.isWikiMarkdown {
 | 
			
		||||
			if ctx.IsWiki {
 | 
			
		||||
				link = util.URLJoin("wiki", "raw", link)
 | 
			
		||||
			}
 | 
			
		||||
			link = util.URLJoin(urlPrefix, link)
 | 
			
		||||
| 
						 | 
				
			
			@ -778,7 +739,7 @@ func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) {
 | 
			
		|||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if !absoluteLink {
 | 
			
		||||
			if ctx.isWikiMarkdown {
 | 
			
		||||
			if ctx.IsWiki {
 | 
			
		||||
				link = util.URLJoin("wiki", 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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	if ctx.Metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
 | 
			
		||||
| 
						 | 
				
			
			@ -811,7 +772,7 @@ func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
	matchOrg := linkParts[len(linkParts)-4]
 | 
			
		||||
	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,
 | 
			
		||||
		// and we should indicate that in the text somehow
 | 
			
		||||
		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) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	if ctx.Metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -832,8 +793,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
		ref   *references.RenderizableReference
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	_, exttrack := ctx.metas["format"]
 | 
			
		||||
	alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric
 | 
			
		||||
	_, exttrack := ctx.Metas["format"]
 | 
			
		||||
	alphanum := ctx.Metas["style"] == IssueNameStyleAlphanumeric
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
| 
						 | 
				
			
			@ -853,8 +814,8 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
	var link *html.Node
 | 
			
		||||
	reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
 | 
			
		||||
	if exttrack && !ref.IsPull {
 | 
			
		||||
		ctx.metas["index"] = ref.Issue
 | 
			
		||||
		link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue")
 | 
			
		||||
		ctx.Metas["index"] = ref.Issue
 | 
			
		||||
		link = createLink(com.Expand(ctx.Metas["format"], ctx.Metas), reftext, "ref-issue")
 | 
			
		||||
	} else {
 | 
			
		||||
		// 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
 | 
			
		||||
| 
						 | 
				
			
			@ -864,7 +825,7 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
			path = "pulls"
 | 
			
		||||
		}
 | 
			
		||||
		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 {
 | 
			
		||||
			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
 | 
			
		||||
func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
func fullSha1PatternProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	if ctx.Metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m := anySHA1Pattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
| 
						 | 
				
			
			@ -944,8 +905,7 @@ func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -968,7 +928,7 @@ func emojiShortCodeProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -983,8 +943,8 @@ func emojiProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
 | 
			
		||||
// sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that
 | 
			
		||||
// are assumed to be in the same repository.
 | 
			
		||||
func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil || ctx.metas["user"] == "" || ctx.metas["repo"] == "" || ctx.metas["repoPath"] == "" {
 | 
			
		||||
func sha1CurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	if ctx.Metas == nil || ctx.Metas["user"] == "" || ctx.Metas["repo"] == "" || ctx.Metas["repoPath"] == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	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.
 | 
			
		||||
	// Because of this, we check to make sure that a matched hash is actually
 | 
			
		||||
	// 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") {
 | 
			
		||||
			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],
 | 
			
		||||
		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.
 | 
			
		||||
func emailAddressProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	m := emailRegex.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -1023,7 +983,7 @@ func emailAddressProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
 | 
			
		||||
// linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | 
			
		||||
// markdown.
 | 
			
		||||
func linkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
func linkProcessor(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
	m := common.LinkRegex.FindStringIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			@ -1033,7 +993,7 @@ func linkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func genDefaultLinkProcessor(defaultLink string) processor {
 | 
			
		||||
	return func(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	return func(ctx *RenderContext, node *html.Node) {
 | 
			
		||||
		ch := &html.Node{
 | 
			
		||||
			Parent: node,
 | 
			
		||||
			Type:   html.TextNode,
 | 
			
		||||
| 
						 | 
				
			
			@ -1052,7 +1012,7 @@ func genDefaultLinkProcessor(defaultLink string) processor {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,8 +61,8 @@ var localMetas = map[string]string{
 | 
			
		|||
func TestRender_IssueIndexPattern(t *testing.T) {
 | 
			
		||||
	// numeric: render inputs without valid mentions
 | 
			
		||||
	test := func(s string) {
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, nil)
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, &postProcessCtx{metas: numericMetas})
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, &RenderContext{})
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, &RenderContext{Metas: numericMetas})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
		}
 | 
			
		||||
		expectedNil := fmt.Sprintf(expectedFmt, links...)
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, expectedNil, &postProcessCtx{metas: localMetas})
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, expectedNil, &RenderContext{Metas: localMetas})
 | 
			
		||||
 | 
			
		||||
		for i, index := range indices {
 | 
			
		||||
			links[i] = numericIssueLink(prefix, "ref-issue", index, marker)
 | 
			
		||||
		}
 | 
			
		||||
		expectedNum := fmt.Sprintf(expectedFmt, links...)
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, expectedNum, &postProcessCtx{metas: numericMetas})
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, expectedNum, &RenderContext{Metas: numericMetas})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// should render freestanding mentions
 | 
			
		||||
| 
						 | 
				
			
			@ -150,7 +150,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	// alphanumeric: render inputs without valid mentions
 | 
			
		||||
	test := func(s string) {
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, &postProcessCtx{metas: alphanumericMetas})
 | 
			
		||||
		testRenderIssueIndexPattern(t, s, s, &RenderContext{Metas: alphanumericMetas})
 | 
			
		||||
	}
 | 
			
		||||
	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)
 | 
			
		||||
		}
 | 
			
		||||
		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("test T-12 issue", "test %s issue", "T-12")
 | 
			
		||||
	test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *postProcessCtx) {
 | 
			
		||||
	if ctx == nil {
 | 
			
		||||
		ctx = new(postProcessCtx)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.procs = []processor{issueIndexPatternProcessor}
 | 
			
		||||
	if ctx.urlPrefix == "" {
 | 
			
		||||
		ctx.urlPrefix = AppSubURL
 | 
			
		||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, ctx *RenderContext) {
 | 
			
		||||
	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.Equal(t, expected, string(res))
 | 
			
		||||
	assert.Equal(t, expected, buf.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRender_AutoLink(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			@ -207,12 +204,22 @@ func TestRender_AutoLink(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
 | 
			
		||||
		buffer, err = PostProcess([]byte(input), setting.AppSubURL, localMetas, true)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
 | 
			
		||||
 | 
			
		||||
		buffer.Reset()
 | 
			
		||||
		err = PostProcess(&RenderContext{
 | 
			
		||||
			URLPrefix: setting.AppSubURL,
 | 
			
		||||
			Metas:     localMetas,
 | 
			
		||||
			IsWiki:    true,
 | 
			
		||||
		}, strings.NewReader(input), &buffer)
 | 
			
		||||
		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
 | 
			
		||||
| 
						 | 
				
			
			@ -235,15 +242,13 @@ func TestRender_FullIssueURLs(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	test := func(input, expected string) {
 | 
			
		||||
		ctx := new(postProcessCtx)
 | 
			
		||||
		ctx.procs = []processor{fullIssuePatternProcessor}
 | 
			
		||||
		if ctx.urlPrefix == "" {
 | 
			
		||||
			ctx.urlPrefix = AppSubURL
 | 
			
		||||
		}
 | 
			
		||||
		ctx.metas = localMetas
 | 
			
		||||
		result, err := ctx.postProcess([]byte(input))
 | 
			
		||||
		var result strings.Builder
 | 
			
		||||
		err := postProcess(&RenderContext{
 | 
			
		||||
			URLPrefix: AppSubURL,
 | 
			
		||||
			Metas:     localMetas,
 | 
			
		||||
		}, []processor{fullIssuePatternProcessor}, strings.NewReader(input), &result)
 | 
			
		||||
		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",
 | 
			
		||||
		"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
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +64,12 @@ func TestRender_CrossReferences(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -91,7 +101,11 @@ func TestRender_links(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
	// Text that should be turned into URL
 | 
			
		||||
| 
						 | 
				
			
			@ -187,8 +201,12 @@ func TestRender_email(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	test := func(input, expected string) {
 | 
			
		||||
		buffer := RenderString("a.md", input, setting.AppSubURL, nil)
 | 
			
		||||
		assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
 | 
			
		||||
		res, err := RenderString(&RenderContext{
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -242,7 +260,11 @@ func TestRender_emoji(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	test := func(input, expected string) {
 | 
			
		||||
		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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -291,9 +313,17 @@ func TestRender_ShortLinks(t *testing.T) {
 | 
			
		|||
	tree := util.URLJoin(AppSubURL, "src", "master")
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
		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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -395,16 +425,22 @@ func Test_ParseClusterFuzz(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	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.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 "
 | 
			
		||||
 | 
			
		||||
	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.NotContains(t, string(val), "<html")
 | 
			
		||||
	assert.NotContains(t, res.String(), "<html")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ package markdown
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,17 +74,17 @@ func (l *limitWriter) CloseWithError(err error) error {
 | 
			
		|||
	return l.w.CloseWithError(err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewGiteaParseContext creates a parser.Context with the gitea context set
 | 
			
		||||
func NewGiteaParseContext(urlPrefix string, metas map[string]string, isWiki bool) parser.Context {
 | 
			
		||||
// newParserContext creates a parser.Context with the render context set
 | 
			
		||||
func newParserContext(ctx *markup.RenderContext) parser.Context {
 | 
			
		||||
	pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
 | 
			
		||||
	pc.Set(urlPrefixKey, urlPrefix)
 | 
			
		||||
	pc.Set(isWikiKey, isWiki)
 | 
			
		||||
	pc.Set(renderMetasKey, metas)
 | 
			
		||||
	pc.Set(urlPrefixKey, ctx.URLPrefix)
 | 
			
		||||
	pc.Set(isWikiKey, ctx.IsWiki)
 | 
			
		||||
	pc.Set(renderMetasKey, ctx.Metas)
 | 
			
		||||
	return pc
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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() {
 | 
			
		||||
		converter = goldmark.New(
 | 
			
		||||
			goldmark.WithExtensions(extension.Table,
 | 
			
		||||
| 
						 | 
				
			
			@ -169,7 +170,7 @@ func actualRender(body []byte, urlPrefix string, metas map[string]string, wikiMa
 | 
			
		|||
		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() {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			err := recover()
 | 
			
		||||
| 
						 | 
				
			
			@ -184,18 +185,26 @@ func actualRender(body []byte, urlPrefix string, metas map[string]string, wikiMa
 | 
			
		|||
			_ = lw.CloseWithError(fmt.Errorf("%v", err))
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		pc := NewGiteaParseContext(urlPrefix, metas, wikiMarkdown)
 | 
			
		||||
		if err := converter.Convert(giteautil.NormalizeEOL(body), lw, parser.WithContext(pc)); err != nil {
 | 
			
		||||
		// FIXME: Don't read all to memory, but goldmark doesn't support
 | 
			
		||||
		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)
 | 
			
		||||
			_ = lw.CloseWithError(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		_ = 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() {
 | 
			
		||||
		err := recover()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -206,9 +215,13 @@ func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown
 | 
			
		|||
		if log.IsDebug() {
 | 
			
		||||
			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 (
 | 
			
		||||
| 
						 | 
				
			
			@ -217,48 +230,59 @@ var (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	markup.RegisterParser(Parser{})
 | 
			
		||||
	markup.RegisterRenderer(Renderer{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parser implements markup.Parser
 | 
			
		||||
type Parser struct{}
 | 
			
		||||
// Renderer implements markup.Renderer
 | 
			
		||||
type Renderer struct{}
 | 
			
		||||
 | 
			
		||||
// Name implements markup.Parser
 | 
			
		||||
func (Parser) Name() string {
 | 
			
		||||
// Name implements markup.Renderer
 | 
			
		||||
func (Renderer) Name() string {
 | 
			
		||||
	return MarkupName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NeedPostProcess implements markup.Parser
 | 
			
		||||
func (Parser) NeedPostProcess() bool { return true }
 | 
			
		||||
// NeedPostProcess implements markup.Renderer
 | 
			
		||||
func (Renderer) NeedPostProcess() bool { return true }
 | 
			
		||||
 | 
			
		||||
// Extensions implements markup.Parser
 | 
			
		||||
func (Parser) Extensions() []string {
 | 
			
		||||
// Extensions implements markup.Renderer
 | 
			
		||||
func (Renderer) Extensions() []string {
 | 
			
		||||
	return setting.Markdown.FileExtensions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render implements markup.Parser
 | 
			
		||||
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
 | 
			
		||||
	return render(rawBytes, urlPrefix, metas, isWiki)
 | 
			
		||||
// Render implements markup.Renderer
 | 
			
		||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 | 
			
		||||
	return render(ctx, input, output)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render renders Markdown to HTML with all specific handling stuff.
 | 
			
		||||
func Render(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
 | 
			
		||||
	return markup.Render("a.md", rawBytes, urlPrefix, metas)
 | 
			
		||||
func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 | 
			
		||||
	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.
 | 
			
		||||
func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte {
 | 
			
		||||
	return render(body, urlPrefix, map[string]string{}, wikiMarkdown)
 | 
			
		||||
func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 | 
			
		||||
	return render(ctx, input, output)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderString renders Markdown to HTML with special links and returns string type.
 | 
			
		||||
func RenderString(raw, urlPrefix string, metas map[string]string) string {
 | 
			
		||||
	return markup.RenderString("a.md", raw, urlPrefix, metas)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderWiki renders markdown wiki page to HTML and return HTML string
 | 
			
		||||
func RenderWiki(rawBytes []byte, urlPrefix string, metas map[string]string) string {
 | 
			
		||||
	return markup.RenderWiki("a.md", rawBytes, urlPrefix, metas)
 | 
			
		||||
// RenderRawString renders Markdown to HTML without handling special links and return string
 | 
			
		||||
func RenderRawString(ctx *markup.RenderContext, content string) (string, error) {
 | 
			
		||||
	var buf strings.Builder
 | 
			
		||||
	if err := RenderRaw(ctx, strings.NewReader(content), &buf); err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsMarkdownFile reports whether name looks like a Markdown file
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	. "code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -31,10 +32,17 @@ func TestRender_StandardLinks(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
		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>`
 | 
			
		||||
| 
						 | 
				
			
			@ -74,7 +82,10 @@ func TestRender_Images(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -261,7 +272,12 @@ func TestTotal_RenderWiki(t *testing.T) {
 | 
			
		|||
	answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/"))
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -279,7 +295,11 @@ func TestTotal_RenderWiki(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -288,31 +308,40 @@ func TestTotal_RenderString(t *testing.T) {
 | 
			
		|||
	answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/"))
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testCases := []string{}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRender_RenderParagraphs(t *testing.T) {
 | 
			
		||||
	test := func(t *testing.T, str string, cnt int) {
 | 
			
		||||
		unix := []byte(str)
 | 
			
		||||
		res := string(RenderRaw(unix, "", false))
 | 
			
		||||
		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)
 | 
			
		||||
		res, err := RenderRawString(&markup.RenderContext{}, str)
 | 
			
		||||
		assert.NoError(t, err)
 | 
			
		||||
		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"))
 | 
			
		||||
		res = string(RenderRaw(mac, "", false))
 | 
			
		||||
		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)
 | 
			
		||||
		mac := strings.ReplaceAll(str, "\n", "\r")
 | 
			
		||||
		res, err = RenderRawString(&markup.RenderContext{}, mac)
 | 
			
		||||
		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"))
 | 
			
		||||
		res = string(RenderRaw(dos, "", false))
 | 
			
		||||
		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)
 | 
			
		||||
		dos := strings.ReplaceAll(str, "\n", "\r\n")
 | 
			
		||||
		res, err = RenderRawString(&markup.RenderContext{}, dos)
 | 
			
		||||
		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)
 | 
			
		||||
| 
						 | 
				
			
			@ -337,7 +366,8 @@ func TestMarkdownRenderRaw(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	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>
 | 
			
		||||
<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)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"io"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -18,58 +18,62 @@ import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	markup.RegisterParser(Parser{})
 | 
			
		||||
	markup.RegisterRenderer(Renderer{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parser implements markup.Parser for orgmode
 | 
			
		||||
type Parser struct {
 | 
			
		||||
// Renderer implements markup.Renderer for orgmode
 | 
			
		||||
type Renderer struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Name implements markup.Parser
 | 
			
		||||
func (Parser) Name() string {
 | 
			
		||||
// Name implements markup.Renderer
 | 
			
		||||
func (Renderer) Name() string {
 | 
			
		||||
	return "orgmode"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NeedPostProcess implements markup.Parser
 | 
			
		||||
func (Parser) NeedPostProcess() bool { return true }
 | 
			
		||||
// NeedPostProcess implements markup.Renderer
 | 
			
		||||
func (Renderer) NeedPostProcess() bool { return true }
 | 
			
		||||
 | 
			
		||||
// Extensions implements markup.Parser
 | 
			
		||||
func (Parser) Extensions() []string {
 | 
			
		||||
// Extensions implements markup.Renderer
 | 
			
		||||
func (Renderer) Extensions() []string {
 | 
			
		||||
	return []string{".org"}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
 | 
			
		||||
	renderer := &Renderer{
 | 
			
		||||
	w := &Writer{
 | 
			
		||||
		HTMLWriter: htmlWriter,
 | 
			
		||||
		URLPrefix:  urlPrefix,
 | 
			
		||||
		IsWiki:     isWiki,
 | 
			
		||||
		URLPrefix:  ctx.URLPrefix,
 | 
			
		||||
		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 {
 | 
			
		||||
		log.Error("Panic in orgmode.Render: %v Just returning the rawBytes", err)
 | 
			
		||||
		return rawBytes
 | 
			
		||||
		return fmt.Errorf("orgmode.Render failed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	return []byte(res)
 | 
			
		||||
	_, err = io.Copy(output, strings.NewReader(res))
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenderString reners orgmode string to HTML string
 | 
			
		||||
func RenderString(rawContent string, urlPrefix string, metas map[string]string, isWiki bool) string {
 | 
			
		||||
	return string(Render([]byte(rawContent), urlPrefix, metas, isWiki))
 | 
			
		||||
// RenderString renders orgmode string to HTML 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Render reners orgmode string to HTML string
 | 
			
		||||
func (Parser) Render(rawBytes []byte, urlPrefix string, metas map[string]string, isWiki bool) []byte {
 | 
			
		||||
	return Render(rawBytes, urlPrefix, metas, isWiki)
 | 
			
		||||
// Render renders orgmode string to HTML string
 | 
			
		||||
func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
 | 
			
		||||
	return Render(ctx, input, output)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Renderer implements org.Writer
 | 
			
		||||
type Renderer struct {
 | 
			
		||||
// Writer implements org.Writer
 | 
			
		||||
type Writer struct {
 | 
			
		||||
	*org.HTMLWriter
 | 
			
		||||
	URLPrefix string
 | 
			
		||||
	IsWiki    bool
 | 
			
		||||
| 
						 | 
				
			
			@ -78,7 +82,7 @@ type Renderer struct {
 | 
			
		|||
var byteMailto = []byte("mailto:")
 | 
			
		||||
 | 
			
		||||
// 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))
 | 
			
		||||
	if l.Protocol == "file" {
 | 
			
		||||
		link = link[len("file:"):]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ import (
 | 
			
		|||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +24,10 @@ func TestRender_StandardLinks(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +44,10 @@ func TestRender_Images(t *testing.T) {
 | 
			
		|||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	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))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										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
 | 
			
		||||
	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled {
 | 
			
		||||
		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) {
 | 
			
		||||
	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled {
 | 
			
		||||
		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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ExternalMarkupParsers represents the external markup parsers
 | 
			
		||||
// ExternalMarkupRenderers represents the external markup renderers
 | 
			
		||||
var (
 | 
			
		||||
	ExternalMarkupParsers  []MarkupParser
 | 
			
		||||
	ExternalSanitizerRules []MarkupSanitizerRule
 | 
			
		||||
	ExternalMarkupRenderers []MarkupRenderer
 | 
			
		||||
	ExternalSanitizerRules  []MarkupSanitizerRule
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MarkupParser defines the external parser configured in ini
 | 
			
		||||
type MarkupParser struct {
 | 
			
		||||
// MarkupRenderer defines the external parser configured in ini
 | 
			
		||||
type MarkupRenderer struct {
 | 
			
		||||
	Enabled         bool
 | 
			
		||||
	MarkupName      string
 | 
			
		||||
	Command         string
 | 
			
		||||
| 
						 | 
				
			
			@ -124,7 +124,7 @@ func newMarkupRenderer(name string, sec *ini.Section) {
 | 
			
		|||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ExternalMarkupParsers = append(ExternalMarkupParsers, MarkupParser{
 | 
			
		||||
	ExternalMarkupRenderers = append(ExternalMarkupRenderers, MarkupRenderer{
 | 
			
		||||
		Enabled:         sec.Key("ENABLED").MustBool(false),
 | 
			
		||||
		MarkupName:      name,
 | 
			
		||||
		FileExtensions:  exts,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -665,7 +665,11 @@ func RenderCommitMessageLink(msg, urlPrefix, urlDefault string, metas map[string
 | 
			
		|||
	cleanMsg := template.HTMLEscapeString(msg)
 | 
			
		||||
	// we can safely assume that it will not return any error, since there
 | 
			
		||||
	// 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 {
 | 
			
		||||
		log.Error("RenderCommitMessage: %v", err)
 | 
			
		||||
		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
 | 
			
		||||
	// 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 {
 | 
			
		||||
		log.Error("RenderCommitMessageSubject: %v", err)
 | 
			
		||||
		return template.HTML("")
 | 
			
		||||
| 
						 | 
				
			
			@ -714,7 +722,10 @@ func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.H
 | 
			
		|||
		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 {
 | 
			
		||||
		log.Error("RenderCommitMessage: %v", err)
 | 
			
		||||
		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
 | 
			
		||||
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 {
 | 
			
		||||
		log.Error("RenderIssueTitle: %v", err)
 | 
			
		||||
		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
 | 
			
		||||
func RenderEmoji(text string) template.HTML {
 | 
			
		||||
	renderedText, err := markup.RenderEmoji([]byte(template.HTMLEscapeString(text)))
 | 
			
		||||
	renderedText, err := markup.RenderEmoji(template.HTMLEscapeString(text))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("RenderEmoji: %v", err)
 | 
			
		||||
		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.
 | 
			
		||||
func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML {
 | 
			
		||||
	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 {
 | 
			
		||||
		log.Error("RenderNote: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,11 +5,11 @@
 | 
			
		|||
package misc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +55,6 @@ func Markdown(ctx *context.APIContext) {
 | 
			
		|||
	case "comment":
 | 
			
		||||
		fallthrough
 | 
			
		||||
	case "gfm":
 | 
			
		||||
		md := []byte(form.Text)
 | 
			
		||||
		urlPrefix := form.Context
 | 
			
		||||
		meta := map[string]string{}
 | 
			
		||||
		if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
 | 
			
		||||
| 
						 | 
				
			
			@ -77,22 +76,19 @@ func Markdown(ctx *context.APIContext) {
 | 
			
		|||
		if form.Mode == "gfm" {
 | 
			
		||||
			meta["mode"] = "document"
 | 
			
		||||
		}
 | 
			
		||||
		if form.Wiki {
 | 
			
		||||
			_, err := ctx.Write([]byte(markdown.RenderWiki(md, urlPrefix, meta)))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.InternalServerError(err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			_, err := ctx.Write(markdown.Render(md, urlPrefix, meta))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.InternalServerError(err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		if err := markdown.Render(&markup.RenderContext{
 | 
			
		||||
			URLPrefix: urlPrefix,
 | 
			
		||||
			Metas:     meta,
 | 
			
		||||
			IsWiki:    form.Wiki,
 | 
			
		||||
		}, strings.NewReader(form.Text), ctx.Resp); err != nil {
 | 
			
		||||
			ctx.InternalServerError(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		_, err := ctx.Write(markdown.RenderRaw([]byte(form.Text), "", false))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		if err := markdown.RenderRaw(&markup.RenderContext{
 | 
			
		||||
			URLPrefix: form.Context,
 | 
			
		||||
		}, strings.NewReader(form.Text), ctx.Resp); err != nil {
 | 
			
		||||
			ctx.InternalServerError(err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -120,14 +116,8 @@ func MarkdownRaw(ctx *context.APIContext) {
 | 
			
		|||
	//     "$ref": "#/responses/MarkdownRender"
 | 
			
		||||
	//   "422":
 | 
			
		||||
	//     "$ref": "#/responses/validationError"
 | 
			
		||||
 | 
			
		||||
	body, err := ioutil.ReadAll(ctx.Req.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ctx.Write(markdown.RenderRaw(body, "", false))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	defer ctx.Req.Body.Close()
 | 
			
		||||
	if err := markdown.RenderRaw(&markup.RenderContext{}, ctx.Req.Body, ctx.Resp); err != nil {
 | 
			
		||||
		ctx.InternalServerError(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -143,7 +143,7 @@ func GlobalInit(ctx context.Context) {
 | 
			
		|||
	NewServices()
 | 
			
		||||
 | 
			
		||||
	highlight.NewContext()
 | 
			
		||||
	external.RegisterParsers()
 | 
			
		||||
	external.RegisterRenderers()
 | 
			
		||||
	markup.Init()
 | 
			
		||||
 | 
			
		||||
	if setting.EnableSQLite3 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +38,15 @@ func Home(ctx *context.Context) {
 | 
			
		|||
	ctx.Data["PageIsUserProfile"] = true
 | 
			
		||||
	ctx.Data["Title"] = org.DisplayName()
 | 
			
		||||
	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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ import (
 | 
			
		|||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
| 
						 | 
				
			
			@ -117,14 +116,7 @@ func setCsvCompareContext(ctx *context.Context) {
 | 
			
		|||
			}
 | 
			
		||||
			defer reader.Close()
 | 
			
		||||
 | 
			
		||||
			b, err := ioutil.ReadAll(reader)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			b = charset.ToUTF8WithFallback(b)
 | 
			
		||||
 | 
			
		||||
			return csv_module.CreateReaderAndGuessDelimiter(b), nil
 | 
			
		||||
			return csv_module.CreateReaderAndGuessDelimiter(charset.ToUTF8WithFallbackReader(reader))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		baseReader, err := csvReaderFromCommit(baseCommit)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1131,8 +1131,14 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		|||
	}
 | 
			
		||||
	ctx.Data["IssueWatch"] = iw
 | 
			
		||||
 | 
			
		||||
	issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink,
 | 
			
		||||
		ctx.Repo.Repository.ComposeMetas()))
 | 
			
		||||
	issue.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
		URLPrefix: ctx.Repo.RepoLink,
 | 
			
		||||
		Metas:     ctx.Repo.Repository.ComposeMetas(),
 | 
			
		||||
	}, issue.Content)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("RenderString", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo := ctx.Repo.Repository
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1289,9 +1295,14 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		|||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
 | 
			
		||||
				ctx.Repo.Repository.ComposeMetas()))
 | 
			
		||||
 | 
			
		||||
			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
				URLPrefix: ctx.Repo.RepoLink,
 | 
			
		||||
				Metas:     ctx.Repo.Repository.ComposeMetas(),
 | 
			
		||||
			}, comment.Content)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.ServerError("RenderString", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			// Check tag.
 | 
			
		||||
			tag, ok = marked[comment.PosterID]
 | 
			
		||||
			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 {
 | 
			
		||||
			comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
 | 
			
		||||
				ctx.Repo.Repository.ComposeMetas()))
 | 
			
		||||
			comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
 | 
			
		||||
				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) {
 | 
			
		||||
				ctx.ServerError("LoadReview", err)
 | 
			
		||||
				return
 | 
			
		||||
| 
						 | 
				
			
			@ -1708,10 +1725,20 @@ func UpdateIssueContent(ctx *context.Context) {
 | 
			
		|||
	files := ctx.QueryStrings("files[]")
 | 
			
		||||
	if err := updateAttachments(issue, files); err != nil {
 | 
			
		||||
		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{}{
 | 
			
		||||
		"content":     string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
 | 
			
		||||
		"content":     content,
 | 
			
		||||
		"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2125,10 +2152,20 @@ func UpdateCommentContent(ctx *context.Context) {
 | 
			
		|||
	files := ctx.QueryStrings("files[]")
 | 
			
		||||
	if err := updateAttachments(comment, files); err != nil {
 | 
			
		||||
		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{}{
 | 
			
		||||
		"content":     string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
 | 
			
		||||
		"content":     content,
 | 
			
		||||
		"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -296,20 +296,13 @@ func LFSFileGet(ctx *context.Context) {
 | 
			
		|||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		d, _ := ioutil.ReadAll(dataRc)
 | 
			
		||||
		buf = charset.ToUTF8WithFallback(append(buf, d...))
 | 
			
		||||
		buf := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
 | 
			
		||||
 | 
			
		||||
		// Building code view blocks with line number on server side.
 | 
			
		||||
		var fileContent string
 | 
			
		||||
		if content, err := charset.ToUTF8WithErr(buf); err != nil {
 | 
			
		||||
			log.Error("ToUTF8WithErr: %v", err)
 | 
			
		||||
			fileContent = string(buf)
 | 
			
		||||
		} else {
 | 
			
		||||
			fileContent = content
 | 
			
		||||
		}
 | 
			
		||||
		fileContent, _ := ioutil.ReadAll(buf)
 | 
			
		||||
 | 
			
		||||
		var output bytes.Buffer
 | 
			
		||||
		lines := strings.Split(fileContent, "\n")
 | 
			
		||||
		lines := strings.Split(string(fileContent), "\n")
 | 
			
		||||
		//Remove blank line at the end of file
 | 
			
		||||
		if len(lines) > 0 && lines[len(lines)-1] == "" {
 | 
			
		||||
			lines = lines[:len(lines)-1]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
| 
						 | 
				
			
			@ -84,7 +85,14 @@ func Milestones(ctx *context.Context) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -269,7 +277,14 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
 | 
			
		|||
		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["Milestone"] = milestone
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +78,14 @@ func Projects(ctx *context.Context) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
| 
						 | 
				
			
			@ -311,7 +319,14 @@ func ViewProject(ctx *context.Context) {
 | 
			
		|||
	}
 | 
			
		||||
	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["Project"] = project
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/upload"
 | 
			
		||||
| 
						 | 
				
			
			@ -132,7 +133,14 @@ func releasesOrTags(ctx *context.Context, isTagList bool) {
 | 
			
		|||
			ctx.ServerError("calReleaseNumCommitsBehind", err)
 | 
			
		||||
			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
 | 
			
		||||
| 
						 | 
				
			
			@ -182,7 +190,14 @@ func SingleRelease(ctx *context.Context) {
 | 
			
		|||
		ctx.ServerError("calReleaseNumCommitsBehind", err)
 | 
			
		||||
		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.HTML(http.StatusOK, tplReleases)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -324,13 +324,26 @@ func renderDirectory(ctx *context.Context, treeLink string) {
 | 
			
		|||
				ctx.Data["IsTextFile"] = true
 | 
			
		||||
				ctx.Data["FileSize"] = fileSize
 | 
			
		||||
			} else {
 | 
			
		||||
				d, _ := ioutil.ReadAll(dataRc)
 | 
			
		||||
				buf = charset.ToUTF8WithFallback(append(buf, d...))
 | 
			
		||||
				rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
 | 
			
		||||
 | 
			
		||||
				if markupType := markup.Type(readmeFile.name); markupType != "" {
 | 
			
		||||
					ctx.Data["IsMarkup"] = true
 | 
			
		||||
					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 {
 | 
			
		||||
					ctx.Data["IsRenderedHTML"] = true
 | 
			
		||||
					ctx.Data["FileContent"] = strings.ReplaceAll(
 | 
			
		||||
| 
						 | 
				
			
			@ -481,21 +494,30 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
 | 
			
		|||
			break
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		d, _ := ioutil.ReadAll(dataRc)
 | 
			
		||||
		buf = charset.ToUTF8WithFallback(append(buf, d...))
 | 
			
		||||
		rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
 | 
			
		||||
		readmeExist := markup.IsReadmeFile(blob.Name())
 | 
			
		||||
		ctx.Data["ReadmeExist"] = readmeExist
 | 
			
		||||
		if markupType := markup.Type(blob.Name()); markupType != "" {
 | 
			
		||||
			ctx.Data["IsMarkup"] = true
 | 
			
		||||
			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 {
 | 
			
		||||
			ctx.Data["IsRenderedHTML"] = true
 | 
			
		||||
			ctx.Data["FileContent"] = strings.ReplaceAll(
 | 
			
		||||
				gotemplate.HTMLEscapeString(string(buf)), "\n", `<br>`,
 | 
			
		||||
			)
 | 
			
		||||
		} else {
 | 
			
		||||
			buf = charset.ToUTF8WithFallback(buf)
 | 
			
		||||
			buf, _ := ioutil.ReadAll(rd)
 | 
			
		||||
			lineNums := linesBytesCount(buf)
 | 
			
		||||
			ctx.Data["NumLines"] = strconv.Itoa(lineNums)
 | 
			
		||||
			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 != "" {
 | 
			
		||||
			d, _ := ioutil.ReadAll(dataRc)
 | 
			
		||||
			buf = append(buf, d...)
 | 
			
		||||
			rd := io.MultiReader(bytes.NewReader(buf), dataRc)
 | 
			
		||||
			ctx.Data["IsMarkup"] = true
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
| 
						 | 
				
			
			@ -211,12 +212,34 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
 | 
			
		|||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	metas := ctx.Repo.Repository.ComposeDocumentMetas()
 | 
			
		||||
	ctx.Data["content"] = markdown.RenderWiki(data, ctx.Repo.RepoLink, metas)
 | 
			
		||||
	var rctx = &markup.RenderContext{
 | 
			
		||||
		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["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["footerContent"] = markdown.RenderWiki(footerContent, ctx.Repo.RepoLink, metas)
 | 
			
		||||
	ctx.Data["footerContent"] = buf.String()
 | 
			
		||||
 | 
			
		||||
	// get commit count - wiki revisions
 | 
			
		||||
	commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +268,15 @@ func Milestones(ctx *context.Context) {
 | 
			
		|||
			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() {
 | 
			
		||||
			err := milestones[i].LoadTotalTrackedTime()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
| 
						 | 
				
			
			@ -110,7 +111,15 @@ func Profile(ctx *context.Context) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,11 +95,17 @@ func TestCSVDiff(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
		var baseReader *csv.Reader
 | 
			
		||||
		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
 | 
			
		||||
		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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,8 +174,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
 | 
			
		|||
	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 (
 | 
			
		||||
		subject 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
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	if actName != "new" {
 | 
			
		||||
| 
						 | 
				
			
			@ -240,14 +246,13 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str
 | 
			
		|||
	// TODO: i18n templates?
 | 
			
		||||
	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil {
 | 
			
		||||
		subject = sanitizeSubject(mailSubject.String())
 | 
			
		||||
		if subject == "" {
 | 
			
		||||
			subject = fallback
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if subject == "" {
 | 
			
		||||
		subject = fallback
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subject = emoji.ReplaceAliases(subject)
 | 
			
		||||
 | 
			
		||||
	mailMeta["Subject"] = subject
 | 
			
		||||
| 
						 | 
				
			
			@ -275,7 +280,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str
 | 
			
		|||
		msgs = append(msgs, msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return msgs
 | 
			
		||||
	return msgs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sanitizeSubject(subject string) string {
 | 
			
		||||
| 
						 | 
				
			
			@ -288,21 +293,26 @@ func sanitizeSubject(subject string) string {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 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)
 | 
			
		||||
	for _, user := range recipients {
 | 
			
		||||
		langMap[user.Language] = append(langMap[user.Language], user.Email)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for lang, tos := range langMap {
 | 
			
		||||
		SendAsyncs(composeIssueCommentMessages(&mailCommentContext{
 | 
			
		||||
		msgs, err := composeIssueCommentMessages(&mailCommentContext{
 | 
			
		||||
			Issue:      issue,
 | 
			
		||||
			Doer:       doer,
 | 
			
		||||
			ActionType: models.ActionType(0),
 | 
			
		||||
			Content:    content,
 | 
			
		||||
			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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
		// starting condition will need to be changed slightly
 | 
			
		||||
		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]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup/markdown"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
| 
						 | 
				
			
			@ -48,7 +49,15 @@ func MailNewRelease(rel *models.Release) {
 | 
			
		|||
func mailNewRelease(lang string, tos []string, rel *models.Release) {
 | 
			
		||||
	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())
 | 
			
		||||
	mailMeta := map[string]interface{}{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -58,8 +58,9 @@ func TestComposeIssueCommentMessage(t *testing.T) {
 | 
			
		|||
	InitMailRender(stpl, btpl)
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, msgs, 2)
 | 
			
		||||
	gomailMsg := msgs[0].ToMessage()
 | 
			
		||||
	mailto := gomailMsg.GetHeader("To")
 | 
			
		||||
| 
						 | 
				
			
			@ -92,8 +93,9 @@ func TestComposeIssueMessage(t *testing.T) {
 | 
			
		|||
	InitMailRender(stpl, btpl)
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, msgs, 2)
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
	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)
 | 
			
		||||
	return msgs[0]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue