| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- // Copyright 2014 The Gogs Authors. All rights reserved.
- // Copyright 2018 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package markdown
-
- import (
- "errors"
- "html/template"
- "io"
- "strings"
-
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/markup"
- "code.gitea.io/gitea/modules/markup/common"
- "code.gitea.io/gitea/modules/markup/markdown/math"
- "code.gitea.io/gitea/modules/setting"
- giteautil "code.gitea.io/gitea/modules/util"
-
- chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
- "github.com/yuin/goldmark"
- highlighting "github.com/yuin/goldmark-highlighting/v2"
- meta "github.com/yuin/goldmark-meta"
- "github.com/yuin/goldmark/extension"
- "github.com/yuin/goldmark/parser"
- "github.com/yuin/goldmark/renderer"
- "github.com/yuin/goldmark/renderer/html"
- "github.com/yuin/goldmark/util"
- )
-
- var (
- renderContextKey = parser.NewContextKey()
- renderConfigKey = parser.NewContextKey()
- )
-
- type limitWriter struct {
- w io.Writer
- sum int64
- limit int64
- }
-
- // Write implements the standard Write interface:
- func (l *limitWriter) Write(data []byte) (int, error) {
- leftToWrite := l.limit - l.sum
- if leftToWrite < int64(len(data)) {
- n, err := l.w.Write(data[:leftToWrite])
- l.sum += int64(n)
- if err != nil {
- return n, err
- }
- return n, errors.New("rendered content too large - truncating render")
- }
- n, err := l.w.Write(data)
- l.sum += int64(n)
- return n, err
- }
-
- // 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(renderContextKey, ctx)
- return pc
- }
-
- type GlodmarkRender struct {
- ctx *markup.RenderContext
-
- goldmarkMarkdown goldmark.Markdown
- }
-
- func (r *GlodmarkRender) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error {
- return r.goldmarkMarkdown.Convert(source, writer, opts...)
- }
-
- func (r *GlodmarkRender) Renderer() renderer.Renderer {
- return r.goldmarkMarkdown.Renderer()
- }
-
- func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) {
- if entering {
- languageBytes, _ := c.Language()
- languageStr := giteautil.IfZero(string(languageBytes), "text")
-
- preClasses := "code-block"
- if languageStr == "mermaid" || languageStr == "math" {
- preClasses += " is-loading"
- }
-
- // include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
- // the "display" class is used by "js/markup/math.ts" to render the code element as a block
- // the "math.ts" strictly depends on the structure: <pre class="code-block is-loading"><code class="language-math display">...</code></pre>
- err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `<div class="code-block-container code-overflow-scroll"><pre class="%s"><code class="chroma language-%s display">`, preClasses, languageStr)
- if err != nil {
- return
- }
- } else {
- _, err := w.WriteString("</code></pre></div>")
- if err != nil {
- return
- }
- }
- }
-
- // SpecializedMarkdown sets up the Gitea specific markdown extensions
- func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender {
- // TODO: it could use a pool to cache the renderers to reuse them with different contexts
- // at the moment it is fast enough (see the benchmarks)
- r := &GlodmarkRender{ctx: ctx}
- r.goldmarkMarkdown = goldmark.New(
- goldmark.WithExtensions(
- extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)),
- extension.Strikethrough,
- extension.TaskList,
- extension.DefinitionList,
- common.FootnoteExtension,
- highlighting.NewHighlighting(
- highlighting.WithFormatOptions(
- chromahtml.WithClasses(true),
- chromahtml.PreventSurroundingPre(true),
- ),
- highlighting.WithWrapperRenderer(r.highlightingRenderer),
- ),
- math.NewExtension(&ctx.RenderInternal, math.Options{
- Enabled: setting.Markdown.EnableMath,
- ParseInlineDollar: setting.Markdown.MathCodeBlockOptions.ParseInlineDollar,
- ParseInlineParentheses: setting.Markdown.MathCodeBlockOptions.ParseInlineParentheses, // this is a bad syntax "\( ... \)", it conflicts with normal markdown escaping
- ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar,
- ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping
- }),
- meta.Meta,
- ),
- goldmark.WithParserOptions(
- parser.WithAttribute(),
- parser.WithAutoHeadingID(),
- parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)),
- ),
- goldmark.WithRendererOptions(html.WithUnsafe()),
- )
-
- // Override the original Tasklist renderer!
- r.goldmarkMarkdown.Renderer().AddOptions(
- renderer.WithNodeRenderers(util.Prioritized(NewHTMLRenderer(&ctx.RenderInternal), 10)),
- )
-
- return r
- }
-
- // render calls goldmark render to convert Markdown to HTML
- // NOTE: The output of this method MUST get sanitized separately!!!
- func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- converter := SpecializedMarkdown(ctx)
- lw := &limitWriter{
- w: output,
- limit: setting.UI.MaxDisplayFileSize * 3,
- }
-
- // FIXME: Don't read all to memory, but goldmark doesn't support
- buf, err := io.ReadAll(input)
- if err != nil {
- log.Error("Unable to ReadAll: %v", err)
- return err
- }
- buf = giteautil.NormalizeEOL(buf)
-
- // FIXME: should we include a timeout to abort the renderer if it takes too long?
- defer func() {
- err := recover()
- if err == nil {
- return
- }
-
- log.Error("Panic in markdown: %v\n%s", err, log.Stack(2))
- escapedHTML := template.HTMLEscapeString(giteautil.UnsafeBytesToString(buf))
- _, _ = output.Write(giteautil.UnsafeStringToBytes(escapedHTML))
- }()
-
- pc := newParserContext(ctx)
-
- // Preserve original length.
- bufWithMetadataLength := len(buf)
-
- rc := &RenderConfig{Meta: markup.RenderMetaAsDetails}
- buf, _ = ExtractMetadataBytes(buf, rc)
-
- metaLength := max(bufWithMetadataLength-len(buf), 0)
- rc.metaLength = metaLength
-
- pc.Set(renderConfigKey, rc)
-
- if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {
- log.Error("Unable to render: %v", err)
- return err
- }
-
- return nil
- }
-
- // MarkupName describes markup's name
- var MarkupName = "markdown"
-
- func init() {
- markup.RegisterRenderer(Renderer{})
- }
-
- // Renderer implements markup.Renderer
- type Renderer struct{}
-
- var _ markup.PostProcessRenderer = (*Renderer)(nil)
-
- // Name implements markup.Renderer
- func (Renderer) Name() string {
- return MarkupName
- }
-
- // NeedPostProcess implements markup.PostProcessRenderer
- func (Renderer) NeedPostProcess() bool { return true }
-
- // Extensions implements markup.Renderer
- func (Renderer) Extensions() []string {
- return setting.Markdown.FileExtensions
- }
-
- // SanitizerRules implements markup.Renderer
- func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
- return []setting.MarkupSanitizerRule{}
- }
-
- // 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(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- ctx.RenderOptions.MarkupType = MarkupName
- return markup.Render(ctx, input, output)
- }
-
- // RenderString renders Markdown string to HTML with all specific handling stuff and return string
- func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
- var buf strings.Builder
- if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
- return "", err
- }
- return template.HTML(buf.String()), nil
- }
-
- // RenderRaw renders Markdown to HTML without handling special links.
- func RenderRaw(ctx *markup.RenderContext, input io.Reader, output io.Writer) error {
- rd, wr := io.Pipe()
- defer func() {
- _ = rd.Close()
- _ = wr.Close()
- }()
-
- go func() {
- if err := render(ctx, input, wr); err != nil {
- _ = wr.CloseWithError(err)
- return
- }
- _ = wr.Close()
- }()
-
- return markup.SanitizeReader(rd, "", output)
- }
-
- // 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
- }
|