code.gitea.io/gitea@v1.19.3/modules/markup/markdown/markdown.go (about)

     1  // Copyright 2014 The Gogs Authors. All rights reserved.
     2  // Copyright 2018 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package markdown
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"strings"
    11  	"sync"
    12  
    13  	"code.gitea.io/gitea/modules/log"
    14  	"code.gitea.io/gitea/modules/markup"
    15  	"code.gitea.io/gitea/modules/markup/common"
    16  	"code.gitea.io/gitea/modules/markup/markdown/math"
    17  	"code.gitea.io/gitea/modules/setting"
    18  	giteautil "code.gitea.io/gitea/modules/util"
    19  
    20  	chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
    21  	"github.com/yuin/goldmark"
    22  	highlighting "github.com/yuin/goldmark-highlighting/v2"
    23  	meta "github.com/yuin/goldmark-meta"
    24  	"github.com/yuin/goldmark/extension"
    25  	"github.com/yuin/goldmark/parser"
    26  	"github.com/yuin/goldmark/renderer"
    27  	"github.com/yuin/goldmark/renderer/html"
    28  	"github.com/yuin/goldmark/util"
    29  )
    30  
    31  var (
    32  	converter goldmark.Markdown
    33  	once      = sync.Once{}
    34  )
    35  
    36  var (
    37  	urlPrefixKey     = parser.NewContextKey()
    38  	isWikiKey        = parser.NewContextKey()
    39  	renderMetasKey   = parser.NewContextKey()
    40  	renderContextKey = parser.NewContextKey()
    41  	renderConfigKey  = parser.NewContextKey()
    42  )
    43  
    44  type limitWriter struct {
    45  	w     io.Writer
    46  	sum   int64
    47  	limit int64
    48  }
    49  
    50  // Write implements the standard Write interface:
    51  func (l *limitWriter) Write(data []byte) (int, error) {
    52  	leftToWrite := l.limit - l.sum
    53  	if leftToWrite < int64(len(data)) {
    54  		n, err := l.w.Write(data[:leftToWrite])
    55  		l.sum += int64(n)
    56  		if err != nil {
    57  			return n, err
    58  		}
    59  		return n, fmt.Errorf("Rendered content too large - truncating render")
    60  	}
    61  	n, err := l.w.Write(data)
    62  	l.sum += int64(n)
    63  	return n, err
    64  }
    65  
    66  // newParserContext creates a parser.Context with the render context set
    67  func newParserContext(ctx *markup.RenderContext) parser.Context {
    68  	pc := parser.NewContext(parser.WithIDs(newPrefixedIDs()))
    69  	pc.Set(urlPrefixKey, ctx.URLPrefix)
    70  	pc.Set(isWikiKey, ctx.IsWiki)
    71  	pc.Set(renderMetasKey, ctx.Metas)
    72  	pc.Set(renderContextKey, ctx)
    73  	return pc
    74  }
    75  
    76  // actualRender renders Markdown to HTML without handling special links.
    77  func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
    78  	once.Do(func() {
    79  		converter = goldmark.New(
    80  			goldmark.WithExtensions(
    81  				extension.NewTable(
    82  					extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
    83  				extension.Strikethrough,
    84  				extension.TaskList,
    85  				extension.DefinitionList,
    86  				common.FootnoteExtension,
    87  				highlighting.NewHighlighting(
    88  					highlighting.WithFormatOptions(
    89  						chromahtml.WithClasses(true),
    90  						chromahtml.PreventSurroundingPre(true),
    91  					),
    92  					highlighting.WithWrapperRenderer(func(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
    93  						if entering {
    94  							language, _ := c.Language()
    95  							if language == nil {
    96  								language = []byte("text")
    97  							}
    98  
    99  							languageStr := string(language)
   100  
   101  							preClasses := []string{"code-block"}
   102  							if languageStr == "mermaid" || languageStr == "math" {
   103  								preClasses = append(preClasses, "is-loading")
   104  							}
   105  
   106  							_, err := w.WriteString(`<pre class="` + strings.Join(preClasses, " ") + `">`)
   107  							if err != nil {
   108  								return
   109  							}
   110  
   111  							// include language-x class as part of commonmark spec
   112  							_, err = w.WriteString(`<code class="chroma language-` + string(language) + `">`)
   113  							if err != nil {
   114  								return
   115  							}
   116  						} else {
   117  							_, err := w.WriteString("</code></pre>")
   118  							if err != nil {
   119  								return
   120  							}
   121  						}
   122  					}),
   123  				),
   124  				math.NewExtension(
   125  					math.Enabled(setting.Markdown.EnableMath),
   126  				),
   127  				meta.Meta,
   128  			),
   129  			goldmark.WithParserOptions(
   130  				parser.WithAttribute(),
   131  				parser.WithAutoHeadingID(),
   132  				parser.WithASTTransformers(
   133  					util.Prioritized(&ASTTransformer{}, 10000),
   134  				),
   135  			),
   136  			goldmark.WithRendererOptions(
   137  				html.WithUnsafe(),
   138  			),
   139  		)
   140  
   141  		// Override the original Tasklist renderer!
   142  		converter.Renderer().AddOptions(
   143  			renderer.WithNodeRenderers(
   144  				util.Prioritized(NewHTMLRenderer(), 10),
   145  			),
   146  		)
   147  	})
   148  
   149  	lw := &limitWriter{
   150  		w:     output,
   151  		limit: setting.UI.MaxDisplayFileSize * 3,
   152  	}
   153  
   154  	// FIXME: should we include a timeout to abort the renderer if it takes too long?
   155  	defer func() {
   156  		err := recover()
   157  		if err == nil {
   158  			return
   159  		}
   160  
   161  		log.Warn("Unable to render markdown due to panic in goldmark: %v", err)
   162  		if log.IsDebug() {
   163  			log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
   164  		}
   165  	}()
   166  
   167  	// FIXME: Don't read all to memory, but goldmark doesn't support
   168  	pc := newParserContext(ctx)
   169  	buf, err := io.ReadAll(input)
   170  	if err != nil {
   171  		log.Error("Unable to ReadAll: %v", err)
   172  		return err
   173  	}
   174  	buf = giteautil.NormalizeEOL(buf)
   175  
   176  	rc := &RenderConfig{
   177  		Meta: "table",
   178  		Icon: "table",
   179  		Lang: "",
   180  	}
   181  	buf, _ = ExtractMetadataBytes(buf, rc)
   182  
   183  	pc.Set(renderConfigKey, rc)
   184  
   185  	if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {
   186  		log.Error("Unable to render: %v", err)
   187  		return err
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  // Note: The output of this method must get sanitized.
   194  func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
   195  	defer func() {
   196  		err := recover()
   197  		if err == nil {
   198  			return
   199  		}
   200  
   201  		log.Warn("Unable to render markdown due to panic in goldmark - will return raw bytes")
   202  		if log.IsDebug() {
   203  			log.Debug("Panic in markdown: %v\n%s", err, log.Stack(2))
   204  		}
   205  		_, err = io.Copy(output, input)
   206  		if err != nil {
   207  			log.Error("io.Copy failed: %v", err)
   208  		}
   209  	}()
   210  	return actualRender(ctx, input, output)
   211  }
   212  
   213  // MarkupName describes markup's name
   214  var MarkupName = "markdown"
   215  
   216  func init() {
   217  	markup.RegisterRenderer(Renderer{})
   218  }
   219  
   220  // Renderer implements markup.Renderer
   221  type Renderer struct{}
   222  
   223  var _ markup.PostProcessRenderer = (*Renderer)(nil)
   224  
   225  // Name implements markup.Renderer
   226  func (Renderer) Name() string {
   227  	return MarkupName
   228  }
   229  
   230  // NeedPostProcess implements markup.PostProcessRenderer
   231  func (Renderer) NeedPostProcess() bool { return true }
   232  
   233  // Extensions implements markup.Renderer
   234  func (Renderer) Extensions() []string {
   235  	return setting.Markdown.FileExtensions
   236  }
   237  
   238  // SanitizerRules implements markup.Renderer
   239  func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
   240  	return []setting.MarkupSanitizerRule{}
   241  }
   242  
   243  // Render implements markup.Renderer
   244  func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
   245  	return render(ctx, input, output)
   246  }
   247  
   248  // Render renders Markdown to HTML with all specific handling stuff.
   249  func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
   250  	if ctx.Type == "" {
   251  		ctx.Type = MarkupName
   252  	}
   253  	return markup.Render(ctx, input, output)
   254  }
   255  
   256  // RenderString renders Markdown string to HTML with all specific handling stuff and return string
   257  func RenderString(ctx *markup.RenderContext, content string) (string, error) {
   258  	var buf strings.Builder
   259  	if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
   260  		return "", err
   261  	}
   262  	return buf.String(), nil
   263  }
   264  
   265  // RenderRaw renders Markdown to HTML without handling special links.
   266  func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
   267  	rd, wr := io.Pipe()
   268  	defer func() {
   269  		_ = rd.Close()
   270  		_ = wr.Close()
   271  	}()
   272  
   273  	go func() {
   274  		if err := render(ctx, input, wr); err != nil {
   275  			_ = wr.CloseWithError(err)
   276  			return
   277  		}
   278  		_ = wr.Close()
   279  	}()
   280  
   281  	return markup.SanitizeReader(rd, "", output)
   282  }
   283  
   284  // RenderRawString renders Markdown to HTML without handling special links and return string
   285  func RenderRawString(ctx *markup.RenderContext, content string) (string, error) {
   286  	var buf strings.Builder
   287  	if err := RenderRaw(ctx, strings.NewReader(content), &buf); err != nil {
   288  		return "", err
   289  	}
   290  	return buf.String(), nil
   291  }