summaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2021-04-20 06:25:08 +0800
committerGitHub <noreply@github.com>2021-04-19 18:25:08 -0400
commit9d99f6ab19ac3f97af3ca126720e9075c127a652 (patch)
treeb817b4582a871f83b91ad7977fe772fc3501c1e8 /modules
parentc9cc6698d2172625854cd063301e63602204a2a1 (diff)
downloadgitea-9d99f6ab19ac3f97af3ca126720e9075c127a652.tar.gz
gitea-9d99f6ab19ac3f97af3ca126720e9075c127a652.zip
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>
Diffstat (limited to 'modules')
-rw-r--r--modules/charset/charset.go49
-rw-r--r--modules/csv/csv.go26
-rw-r--r--modules/csv/csv_test.go7
-rw-r--r--modules/markup/csv/csv.go123
-rw-r--r--modules/markup/csv/csv_test.go11
-rw-r--r--modules/markup/external/external.go60
-rw-r--r--modules/markup/html.go236
-rw-r--r--modules/markup/html_internal_test.go59
-rw-r--r--modules/markup/html_test.go64
-rw-r--r--modules/markup/markdown/markdown.go96
-rw-r--r--modules/markup/markdown/markdown_test.go73
-rw-r--r--modules/markup/markup.go143
-rw-r--r--modules/markup/orgmode/orgmode.go60
-rw-r--r--modules/markup/orgmode/orgmode_test.go11
-rw-r--r--modules/markup/renderer.go201
-rw-r--r--modules/markup/renderer_test.go (renamed from modules/markup/markup_test.go)0
-rw-r--r--modules/notification/mail/mail.go8
-rw-r--r--modules/setting/markup.go12
-rw-r--r--modules/templates/helper.go29
19 files changed, 744 insertions, 524 deletions
diff --git a/modules/charset/charset.go b/modules/charset/charset.go
index a7e427db99..3000864c2e 100644
--- a/modules/charset/charset.go
+++ b/modules/charset/charset.go
@@ -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
diff --git a/modules/csv/csv.go b/modules/csv/csv.go
index 1aa78fdeec..bf433f77d2 100644
--- a/modules/csv/csv.go
+++ b/modules/csv/csv.go
@@ -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.
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index 3a7584e21d..3cc09c40aa 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -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)
}
diff --git a/modules/markup/csv/csv.go b/modules/markup/csv/csv.go
index 68c89166b5..6572b0ee1e 100644
--- a/modules/markup/csv/csv.go
+++ b/modules/markup/csv/csv.go
@@ -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
-
- 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()
+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
+}
- rd := csv.CreateReaderAndGuessDelimiter(rawBytes)
+// Render implements markup.Renderer
+func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+ var tmpBlock = bufio.NewWriter(output)
- writeField := func(element, class, field string) {
- tmpBlock.WriteString("<")
- tmpBlock.WriteString(element)
- if len(class) > 0 {
- tmpBlock.WriteString(" class=\"")
- tmpBlock.WriteString(class)
- tmpBlock.WriteString("\"")
+ // FIXME: don't read all to memory
+ rawBytes, err := ioutil.ReadAll(input)
+ if err != nil {
+ return err
+ }
+
+ if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < int64(len(rawBytes)) {
+ if _, 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
+ }
+
+ rd, err := csv.CreateReaderAndGuessDelimiter(bytes.NewReader(rawBytes))
+ if err != nil {
+ return err
}
- tmpBlock.WriteString(`<table class="data-table">`)
+ 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))
+ if err := writeField(tmpBlock, element, "line-num", strconv.Itoa(row)); err != nil {
+ return err
+ }
for _, field := range fields {
- writeField(element, "", field)
+ 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()
}
diff --git a/modules/markup/csv/csv_test.go b/modules/markup/csv/csv_test.go
index 5438ebdf5c..613762f86c 100644
--- a/modules/markup/csv/csv_test.go
+++ b/modules/markup/csv/csv_test.go
@@ -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())
}
}
diff --git a/modules/markup/external/external.go b/modules/markup/external/external.go
index 6e7e59970d..62814c9914 100644
--- a/modules/markup/external/external.go
+++ b/modules/markup/external/external.go
@@ -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
}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index bec9ba2fb4..7c4c10ee22 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -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
diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go
index 7e4bb6f22f..330750a47a 100644
--- a/modules/markup/html_internal_test.go
+++ b/modules/markup/html_internal_test.go
@@ -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")
diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go
index 1e39be401b..3425c3d3a8 100644
--- a/modules/markup/html_test.go
+++ b/modules/markup/html_test.go
@@ -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, "&", "&amp;")
- 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")
}
diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go
index 5bb0fbd652..87fae2a23b 100644
--- a/modules/markup/markdown/markdown.go
+++ b/modules/markup/markdown/markdown.go
@@ -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)
}
-// 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)
+// 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
}
-// 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)
+// RenderRaw renders Markdown to HTML without handling special links.
+func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
+ return render(ctx, input, output)
}
-// 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
diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go
index 0e340763ae..5997dbccdc 100644
--- a/modules/markup/markdown/markdown_test.go
+++ b/modules/markup/markdown/markdown_test.go
@@ -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)
-
- 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)
-
- 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)
+ 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 := 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 := 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)
}
diff --git a/modules/markup/markup.go b/modules/markup/markup.go
deleted file mode 100644
index bc35757775..0000000000
--- a/modules/markup/markup.go
+++ /dev/null
@@ -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."
-}
diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go
index b445b76956..96e67f90cf 100644
--- a/modules/markup/orgmode/orgmode.go
+++ b/modules/markup/orgmode/orgmode.go
@@ -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:"):]
diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go
index 020a3f592a..da89326e9e 100644
--- a/modules/markup/orgmode/orgmode_test.go
+++ b/modules/markup/orgmode/orgmode_test.go
@@ -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))
}
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
new file mode 100644
index 0000000000..7cc81574ba
--- /dev/null
+++ b/modules/markup/renderer.go
@@ -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."
+}
diff --git a/modules/markup/markup_test.go b/modules/markup/renderer_test.go
index 118fa2632b..118fa2632b 100644
--- a/modules/markup/markup_test.go
+++ b/modules/markup/renderer_test.go
diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go
index 9c000da0f6..eb45409faf 100644
--- a/modules/notification/mail/mail.go
+++ b/modules/notification/mail/mail.go
@@ -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)
+ }
}
}
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
index 36cba68262..f0849a863a 100644
--- a/modules/setting/markup.go
+++ b/modules/setting/markup.go
@@ -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,
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 7e33f26209..7b175bfab3 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -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 ""