github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/markdown/markdown.go (about)

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