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