2014-04-10 14:20:58 -04:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2018-10-30 17:26:28 -05:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2014-04-10 14:20:58 -04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2016-02-20 17:10:05 -05:00
package markdown
2014-04-10 14:20:58 -04:00
import (
2021-03-15 23:20:05 +00:00
"fmt"
"io"
2020-08-04 21:56:37 +02:00
"strings"
2019-12-31 01:53:28 +00:00
"sync"
2014-04-10 14:20:58 -04:00
2019-12-31 01:53:28 +00:00
"code.gitea.io/gitea/modules/log"
2017-04-21 15:01:08 +08:00
"code.gitea.io/gitea/modules/markup"
2019-12-31 01:53:28 +00:00
"code.gitea.io/gitea/modules/markup/common"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2019-12-31 01:53:28 +00:00
giteautil "code.gitea.io/gitea/modules/util"
2020-06-30 17:34:03 -04:00
chromahtml "github.com/alecthomas/chroma/formatters/html"
2019-12-31 01:53:28 +00:00
"github.com/yuin/goldmark"
2021-03-15 23:20:05 +00:00
highlighting "github.com/yuin/goldmark-highlighting"
2020-04-09 11:54:50 +01:00
meta "github.com/yuin/goldmark-meta"
2019-12-31 01:53:28 +00:00
"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"
2014-04-10 14:20:58 -04:00
)
2019-12-31 01:53:28 +00:00
var converter goldmark . Markdown
var once = sync . Once { }
2014-04-10 14:20:58 -04:00
2019-12-31 01:53:28 +00:00
var urlPrefixKey = parser . NewContextKey ( )
var isWikiKey = parser . NewContextKey ( )
2020-05-24 09:14:26 +01:00
var renderMetasKey = parser . NewContextKey ( )
2018-02-27 08:09:18 +01:00
2021-03-15 23:20:05 +00:00
type closesWithError interface {
io . WriteCloser
CloseWithError ( err error ) error
}
type limitWriter struct {
w closesWithError
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
}
_ = l . w . Close ( )
return n , fmt . Errorf ( "Rendered content too large - truncating render" )
}
n , err := l . w . Write ( data )
l . sum += int64 ( n )
return n , err
}
// Close closes the writer
func ( l * limitWriter ) Close ( ) error {
return l . w . Close ( )
}
// CloseWithError closes the writer
func ( l * limitWriter ) CloseWithError ( err error ) error {
return l . w . CloseWithError ( err )
}
2021-04-20 06:25:08 +08:00
// newParserContext creates a parser.Context with the render context set
func newParserContext ( ctx * markup . RenderContext ) parser . Context {
2019-12-31 01:53:28 +00:00
pc := parser . NewContext ( parser . WithIDs ( newPrefixedIDs ( ) ) )
2021-04-20 06:25:08 +08:00
pc . Set ( urlPrefixKey , ctx . URLPrefix )
pc . Set ( isWikiKey , ctx . IsWiki )
pc . Set ( renderMetasKey , ctx . Metas )
2019-12-31 01:53:28 +00:00
return pc
2016-01-09 10:59:04 +08:00
}
2021-03-15 23:20:05 +00:00
// actualRender renders Markdown to HTML without handling special links.
2021-04-20 06:25:08 +08:00
func actualRender ( ctx * markup . RenderContext , input io . Reader , output io . Writer ) error {
2019-12-31 01:53:28 +00:00
once . Do ( func ( ) {
converter = goldmark . New (
2021-08-02 17:11:18 +01:00
goldmark . WithExtensions (
extension . NewTable (
extension . WithTableCellAlignMethod ( extension . TableCellAlignAttribute ) ) ,
2019-12-31 01:53:28 +00:00
extension . Strikethrough ,
extension . TaskList ,
extension . DefinitionList ,
common . FootnoteExtension ,
2020-06-30 17:34:03 -04:00
highlighting . NewHighlighting (
highlighting . WithFormatOptions (
chromahtml . WithClasses ( true ) ,
chromahtml . PreventSurroundingPre ( true ) ,
) ,
highlighting . WithWrapperRenderer ( func ( w util . BufWriter , c highlighting . CodeBlockContext , entering bool ) {
if entering {
2020-08-04 21:56:37 +02:00
language , _ := c . Language ( )
if language == nil {
language = [ ] byte ( "text" )
}
languageStr := string ( language )
preClasses := [ ] string { }
if languageStr == "mermaid" {
preClasses = append ( preClasses , "is-loading" )
}
if len ( preClasses ) > 0 {
_ , err := w . WriteString ( ` <pre class=" ` + strings . Join ( preClasses , " " ) + ` "> ` )
if err != nil {
return
}
} else {
_ , err := w . WriteString ( ` <pre> ` )
if err != nil {
return
}
}
2020-06-30 17:34:03 -04:00
// include language-x class as part of commonmark spec
2020-08-04 21:56:37 +02:00
_ , err := w . WriteString ( ` <code class="chroma language- ` + string ( language ) + ` "> ` )
2020-06-30 17:34:03 -04:00
if err != nil {
return
}
} else {
_ , err := w . WriteString ( "</code></pre>" )
if err != nil {
return
}
}
} ) ,
) ,
2020-04-24 14:22:36 +01:00
meta . Meta ,
2019-12-31 01:53:28 +00:00
) ,
goldmark . WithParserOptions (
parser . WithAttribute ( ) ,
parser . WithAutoHeadingID ( ) ,
parser . WithASTTransformers (
2020-04-24 14:22:36 +01:00
util . Prioritized ( & ASTTransformer { } , 10000 ) ,
2019-12-31 01:53:28 +00:00
) ,
) ,
goldmark . WithRendererOptions (
html . WithUnsafe ( ) ,
) ,
)
// Override the original Tasklist renderer!
converter . Renderer ( ) . AddOptions (
renderer . WithNodeRenderers (
2020-04-24 14:22:36 +01:00
util . Prioritized ( NewHTMLRenderer ( ) , 10 ) ,
2019-12-31 01:53:28 +00:00
) ,
)
} )
2014-04-10 14:20:58 -04:00
2021-03-15 23:20:05 +00:00
rd , wr := io . Pipe ( )
defer func ( ) {
_ = rd . Close ( )
_ = wr . Close ( )
} ( )
lw := & limitWriter {
w : wr ,
limit : setting . UI . MaxDisplayFileSize * 3 ,
2015-09-01 08:32:02 -04:00
}
2021-03-15 23:20:05 +00:00
2021-04-20 06:25:08 +08:00
// FIXME: should we include a timeout that closes the pipe to abort the renderer and sanitizer if it takes too long?
2021-03-15 23:20:05 +00:00
go func ( ) {
defer func ( ) {
err := recover ( )
if err == nil {
return
}
log . Warn ( "Unable to render markdown due to panic in goldmark: %v" , err )
if log . IsDebug ( ) {
log . Debug ( "Panic in markdown: %v\n%s" , err , string ( log . Stack ( 2 ) ) )
}
_ = lw . CloseWithError ( fmt . Errorf ( "%v" , err ) )
} ( )
2021-04-20 06:25:08 +08:00
// FIXME: Don't read all to memory, but goldmark doesn't support
pc := newParserContext ( ctx )
2021-09-22 13:38:34 +08:00
buf , err := io . ReadAll ( input )
2021-04-20 06:25:08 +08:00
if err != nil {
log . Error ( "Unable to ReadAll: %v" , err )
return
}
if err := converter . Convert ( giteautil . NormalizeEOL ( buf ) , lw , parser . WithContext ( pc ) ) ; err != nil {
2021-03-15 23:20:05 +00:00
log . Error ( "Unable to render: %v" , err )
_ = lw . CloseWithError ( err )
return
}
_ = lw . Close ( )
} ( )
2021-06-23 23:09:51 +02:00
buf := markup . SanitizeReader ( rd , "" )
2021-04-20 06:25:08 +08:00
_ , err := io . Copy ( output , buf )
return err
2021-03-15 23:20:05 +00:00
}
2021-04-20 06:25:08 +08:00
func render ( ctx * markup . RenderContext , input io . Reader , output io . Writer ) error {
2021-03-15 23:20:05 +00:00
defer func ( ) {
err := recover ( )
if err == nil {
return
}
log . Warn ( "Unable to render markdown due to panic in goldmark - will return sanitized raw bytes" )
if log . IsDebug ( ) {
log . Debug ( "Panic in markdown: %v\n%s" , err , string ( log . Stack ( 2 ) ) )
}
2021-06-23 23:09:51 +02:00
ret := markup . SanitizeReader ( input , "" )
2021-04-20 06:25:08 +08:00
_ , err = io . Copy ( output , ret )
if err != nil {
log . Error ( "SanitizeReader failed: %v" , err )
}
2021-03-15 23:20:05 +00:00
} ( )
2021-04-20 06:25:08 +08:00
return actualRender ( ctx , input , output )
2014-05-05 13:08:01 -04:00
}
2017-04-21 15:01:08 +08:00
var (
// MarkupName describes markup's name
MarkupName = "markdown"
)
func init ( ) {
2021-04-20 06:25:08 +08:00
markup . RegisterRenderer ( Renderer { } )
2017-04-21 15:01:08 +08:00
}
2021-04-20 06:25:08 +08:00
// Renderer implements markup.Renderer
type Renderer struct { }
2017-04-21 15:01:08 +08:00
2021-04-20 06:25:08 +08:00
// Name implements markup.Renderer
func ( Renderer ) Name ( ) string {
2017-04-21 15:01:08 +08:00
return MarkupName
}
2021-04-20 06:25:08 +08:00
// NeedPostProcess implements markup.Renderer
func ( Renderer ) NeedPostProcess ( ) bool { return true }
2021-04-13 15:06:31 +08:00
2021-04-20 06:25:08 +08:00
// Extensions implements markup.Renderer
func ( Renderer ) Extensions ( ) [ ] string {
2017-04-21 15:01:08 +08:00
return setting . Markdown . FileExtensions
}
2021-06-23 23:09:51 +02:00
// SanitizerRules implements markup.Renderer
func ( Renderer ) SanitizerRules ( ) [ ] setting . MarkupSanitizerRule {
return [ ] setting . MarkupSanitizerRule { }
}
2021-04-20 06:25:08 +08:00
// Render implements markup.Renderer
func ( Renderer ) Render ( ctx * markup . RenderContext , input io . Reader , output io . Writer ) error {
return render ( ctx , input , output )
2017-09-17 01:17:57 +08:00
}
// Render renders Markdown to HTML with all specific handling stuff.
2021-04-20 06:25:08 +08:00
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 )
2017-09-17 01:17:57 +08:00
}
2021-04-20 06:25:08 +08:00
// 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
2020-05-24 09:14:26 +01:00
}
2021-04-20 06:25:08 +08:00
// 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 )
2017-09-17 01:17:57 +08:00
}
2021-04-20 06:25:08 +08:00
// 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
2017-09-17 01:17:57 +08:00
}
// IsMarkdownFile reports whether name looks like a Markdown file
// based on its extension.
func IsMarkdownFile ( name string ) bool {
return markup . IsMarkupFile ( name , MarkupName )
2017-04-21 15:01:08 +08:00
}