2014-04-10 18:20:58 +00:00
// Copyright 2014 The Gogs Authors. All rights reserved.
2018-10-30 22:26:28 +00:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2022-11-27 18:20:29 +00:00
// SPDX-License-Identifier: MIT
2014-04-10 18:20:58 +00:00
2016-02-20 22:10:05 +00:00
package markdown
2014-04-10 18:20:58 +00:00
import (
2021-03-15 23:20:05 +00:00
"fmt"
2024-03-01 07:11:51 +00:00
"html/template"
2021-03-15 23:20:05 +00:00
"io"
2020-08-04 19:56:37 +00:00
"strings"
2014-04-10 18:20:58 +00:00
2019-12-31 01:53:28 +00:00
"code.gitea.io/gitea/modules/log"
2017-04-21 07:01:08 +00:00
"code.gitea.io/gitea/modules/markup"
2019-12-31 01:53:28 +00:00
"code.gitea.io/gitea/modules/markup/common"
2022-09-13 16:33:37 +00:00
"code.gitea.io/gitea/modules/markup/markdown/math"
2016-11-10 16:24:48 +00:00
"code.gitea.io/gitea/modules/setting"
2019-12-31 01:53:28 +00:00
giteautil "code.gitea.io/gitea/modules/util"
2022-09-26 05:50:03 +00:00
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
2019-12-31 01:53:28 +00:00
"github.com/yuin/goldmark"
2022-09-26 05:50:03 +00:00
highlighting "github.com/yuin/goldmark-highlighting/v2"
2020-04-09 10:54:50 +00: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 18:20:58 +00:00
)
2022-01-20 17:46:10 +00:00
var (
2022-06-08 08:59:16 +00:00
renderContextKey = parser . NewContextKey ( )
2022-09-13 16:33:37 +00:00
renderConfigKey = parser . NewContextKey ( )
2022-01-20 17:46:10 +00:00
)
2018-02-27 07:09:18 +00:00
2021-03-15 23:20:05 +00:00
type limitWriter struct {
2021-11-19 10:46:47 +00:00
w io . Writer
2021-03-15 23:20:05 +00:00
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
}
2023-04-17 19:05:19 +00:00
return n , fmt . Errorf ( "rendered content too large - truncating render" )
2021-03-15 23:20:05 +00:00
}
n , err := l . w . Write ( data )
l . sum += int64 ( n )
return n , err
}
2021-04-19 22:25:08 +00: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 ( ) ) )
2022-06-08 08:59:16 +00:00
pc . Set ( renderContextKey , ctx )
2019-12-31 01:53:28 +00:00
return pc
2016-01-09 02:59:04 +00:00
}
2024-11-18 05:25:42 +00:00
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 {
2024-12-14 05:43:05 +00:00
languageBytes , _ := c . Language ( )
languageStr := giteautil . IfZero ( string ( languageBytes ) , "text" )
2024-11-18 05:25:42 +00:00
2024-12-14 05:43:05 +00:00
preClasses := "code-block"
2024-11-18 05:25:42 +00:00
if languageStr == "mermaid" || languageStr == "math" {
2024-12-14 05:43:05 +00:00
preClasses += " is-loading"
2024-11-18 05:25:42 +00:00
}
2024-12-14 05:43:05 +00:00
err := r . ctx . RenderInternal . FormatWithSafeAttrs ( w , ` <pre class="%s"> ` , preClasses )
2024-11-18 05:25:42 +00:00
if err != nil {
return
}
2024-12-14 05:43:05 +00:00
// 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 , ` <code class="chroma language-%s display"> ` , languageStr )
2024-11-18 05:25:42 +00:00
if err != nil {
return
}
} else {
_ , err := w . WriteString ( "</code></pre>" )
if err != nil {
return
}
}
}
2023-04-17 19:05:19 +00:00
// SpecializedMarkdown sets up the Gitea specific markdown extensions
2024-11-18 05:25:42 +00:00
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 ) ,
2019-12-31 01:53:28 +00:00
) ,
2024-11-18 05:25:42 +00:00
highlighting . WithWrapperRenderer ( r . highlightingRenderer ) ,
2019-12-31 01:53:28 +00:00
) ,
2024-12-14 05:43:05 +00:00
math . NewExtension ( & ctx . RenderInternal , math . Options {
Enabled : setting . Markdown . EnableMath ,
ParseDollarInline : true ,
ParseDollarBlock : true ,
2024-12-17 01:15:18 +00:00
ParseSquareBlock : true , // TODO: this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping, it should be deprecated in the future (by some config options)
// ParseBracketInline: true, // TODO: this is also a bad syntax "\( ... \)", it also conflicts, it should be deprecated in the future
2024-12-14 05:43:05 +00:00
} ) ,
2024-11-18 05:25:42 +00:00
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
2023-04-17 19:05:19 +00:00
}
2014-04-10 18:20:58 +00:00
2024-11-18 05:25:42 +00:00
// 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 )
2021-03-15 23:20:05 +00:00
lw := & limitWriter {
2021-11-19 10:46:47 +00:00
w : output ,
2021-03-15 23:20:05 +00:00
limit : setting . UI . MaxDisplayFileSize * 3 ,
2015-09-01 12:32:02 +00:00
}
2021-03-15 23:20:05 +00:00
2021-11-19 10:46:47 +00:00
// FIXME: should we include a timeout to abort the renderer if it takes too long?
defer func ( ) {
err := recover ( )
if err == nil {
2021-04-19 22:25:08 +00:00
return
}
2021-11-19 10:46:47 +00:00
log . Warn ( "Unable to render markdown due to panic in goldmark: %v" , err )
2024-11-18 05:25:42 +00:00
if ( ! setting . IsProd && ! setting . IsInTesting ) || log . IsDebug ( ) {
log . Error ( "Panic in markdown: %v\n%s" , err , log . Stack ( 2 ) )
2021-03-15 23:20:05 +00:00
}
} ( )
2021-11-19 10:46:47 +00:00
// FIXME: Don't read all to memory, but goldmark doesn't support
pc := newParserContext ( ctx )
buf , err := io . ReadAll ( input )
if err != nil {
log . Error ( "Unable to ReadAll: %v" , err )
return err
}
2022-09-13 16:33:37 +00:00
buf = giteautil . NormalizeEOL ( buf )
2023-06-13 06:44:47 +00:00
// Preserve original length.
bufWithMetadataLength := len ( buf )
2022-09-13 16:33:37 +00:00
rc := & RenderConfig {
2024-11-22 05:48:09 +00:00
Meta : markup . RenderMetaAsDetails ,
2022-09-13 16:33:37 +00:00
Icon : "table" ,
Lang : "" ,
}
buf , _ = ExtractMetadataBytes ( buf , rc )
2023-06-13 06:44:47 +00:00
metaLength := bufWithMetadataLength - len ( buf )
if metaLength < 0 {
metaLength = 0
}
rc . metaLength = metaLength
2022-09-13 16:33:37 +00:00
pc . Set ( renderConfigKey , rc )
if err := converter . Convert ( buf , lw , parser . WithContext ( pc ) ) ; err != nil {
2021-11-19 10:46:47 +00:00
log . Error ( "Unable to render: %v" , err )
return err
}
return nil
2021-03-15 23:20:05 +00:00
}
2022-01-20 17:46:10 +00:00
// MarkupName describes markup's name
var MarkupName = "markdown"
2017-04-21 07:01:08 +00:00
func init ( ) {
2021-04-19 22:25:08 +00:00
markup . RegisterRenderer ( Renderer { } )
2017-04-21 07:01:08 +00:00
}
2021-04-19 22:25:08 +00:00
// Renderer implements markup.Renderer
type Renderer struct { }
2017-04-21 07:01:08 +00:00
2022-06-16 03:33:23 +00:00
var _ markup . PostProcessRenderer = ( * Renderer ) ( nil )
2021-04-19 22:25:08 +00:00
// Name implements markup.Renderer
func ( Renderer ) Name ( ) string {
2017-04-21 07:01:08 +00:00
return MarkupName
}
2022-06-16 03:33:23 +00:00
// NeedPostProcess implements markup.PostProcessRenderer
2021-04-19 22:25:08 +00:00
func ( Renderer ) NeedPostProcess ( ) bool { return true }
2021-04-13 07:06:31 +00:00
2021-04-19 22:25:08 +00:00
// Extensions implements markup.Renderer
func ( Renderer ) Extensions ( ) [ ] string {
2017-04-21 07:01:08 +00:00
return setting . Markdown . FileExtensions
}
2021-06-23 21:09:51 +00:00
// SanitizerRules implements markup.Renderer
func ( Renderer ) SanitizerRules ( ) [ ] setting . MarkupSanitizerRule {
return [ ] setting . MarkupSanitizerRule { }
}
2021-04-19 22:25:08 +00: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-16 17:17:57 +00:00
}
// Render renders Markdown to HTML with all specific handling stuff.
2021-04-19 22:25:08 +00:00
func Render ( ctx * markup . RenderContext , input io . Reader , output io . Writer ) error {
2024-11-22 05:48:09 +00:00
ctx . RenderOptions . MarkupType = MarkupName
2021-04-19 22:25:08 +00:00
return markup . Render ( ctx , input , output )
2017-09-16 17:17:57 +00:00
}
2021-04-19 22:25:08 +00:00
// RenderString renders Markdown string to HTML with all specific handling stuff and return string
2024-03-01 07:11:51 +00:00
func RenderString ( ctx * markup . RenderContext , content string ) ( template . HTML , error ) {
2021-04-19 22:25:08 +00:00
var buf strings . Builder
if err := Render ( ctx , strings . NewReader ( content ) , & buf ) ; err != nil {
return "" , err
}
2024-03-01 07:11:51 +00:00
return template . HTML ( buf . String ( ) ) , nil
2020-05-24 08:14:26 +00:00
}
2021-04-19 22:25:08 +00:00
// RenderRaw renders Markdown to HTML without handling special links.
func RenderRaw ( ctx * markup . RenderContext , input io . Reader , output io . Writer ) error {
2021-11-19 10:46:47 +00:00
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 )
2017-09-16 17:17:57 +00:00
}
2021-04-19 22:25:08 +00: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-16 17:17:57 +00:00
}