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.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2014-04-10 14:20:58 -04:00
2016-02-20 17:10:05 -05:00
package markdown
2014-04-10 14:20:58 -04:00
import (
2026-01-23 15:24:58 -05:00
"bytes"
2025-04-01 12:14:01 +02:00
"errors"
2024-03-01 15:11:51 +08:00
"html/template"
2021-03-15 23:20:05 +00:00
"io"
2020-08-04 21:56:37 +02:00
"strings"
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"
2022-09-13 17:33:37 +01:00
"code.gitea.io/gitea/modules/markup/markdown/math"
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"
2022-09-26 07:50:03 +02:00
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
2019-12-31 01:53:28 +00:00
"github.com/yuin/goldmark"
2022-09-26 07:50:03 +02:00
highlighting "github.com/yuin/goldmark-highlighting/v2"
2026-01-23 15:24:58 -05:00
"github.com/yuin/goldmark/ast"
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"
2026-01-23 15:24:58 -05:00
"github.com/yuin/goldmark/text"
2019-12-31 01:53:28 +00:00
"github.com/yuin/goldmark/util"
2014-04-10 14:20:58 -04:00
)
2022-01-20 18:46:10 +01:00
var (
2022-06-08 09:59:16 +01:00
renderContextKey = parser . NewContextKey ( )
2022-09-13 17:33:37 +01:00
renderConfigKey = parser . NewContextKey ( )
2022-01-20 18:46:10 +01:00
)
2018-02-27 08:09:18 +01:00
2021-03-15 23:20:05 +00:00
type limitWriter struct {
2021-11-19 11:46:47 +01: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
}
2025-04-01 12:14:01 +02:00
return n , errors . New ( "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-20 06:25:08 +08:00
// newParserContext creates a parser.Context with the render context set
func newParserContext ( ctx * markup . RenderContext ) parser . Context {
2026-01-23 15:24:58 -05:00
pc := parser . NewContext ( )
2022-06-08 09:59:16 +01:00
pc . Set ( renderContextKey , ctx )
2019-12-31 01:53:28 +00:00
return pc
2016-01-09 10:59:04 +08:00
}
2024-11-18 13:25:42 +08: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 13:43:05 +08:00
languageBytes , _ := c . Language ( )
languageStr := giteautil . IfZero ( string ( languageBytes ) , "text" )
2024-11-18 13:25:42 +08:00
2024-12-14 13:43:05 +08:00
preClasses := "code-block"
2024-11-18 13:25:42 +08:00
if languageStr == "mermaid" || languageStr == "math" {
2024-12-14 13:43:05 +08:00
preClasses += " is-loading"
2024-11-18 13:25:42 +08:00
}
2024-12-14 13:43:05 +08: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>
2025-04-19 14:53:39 +09:00
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 )
2024-11-18 13:25:42 +08:00
if err != nil {
return
}
} else {
2025-04-19 14:53:39 +09:00
_ , err := w . WriteString ( "</code></pre></div>" )
2024-11-18 13:25:42 +08:00
if err != nil {
return
}
}
}
2026-01-23 15:24:58 -05:00
type goldmarkEmphasisParser struct {
parser . InlineParser
}
func goldmarkNewEmphasisParser ( ) parser . InlineParser {
return & goldmarkEmphasisParser { parser . NewEmphasisParser ( ) }
}
func ( s * goldmarkEmphasisParser ) Parse ( parent ast . Node , block text . Reader , pc parser . Context ) ast . Node {
line , _ := block . PeekLine ( )
if len ( line ) > 1 && line [ 0 ] == '_' {
// a special trick to avoid parsing emphasis in filenames like "module/__init__.py"
end := bytes . IndexByte ( line [ 1 : ] , '_' )
mark := bytes . Index ( line , [ ] byte ( "_.py" ) )
// check whether the "end" matches "_.py" or "__.py"
if mark != - 1 && ( end == mark || end == mark - 1 ) {
return nil
}
}
return s . InlineParser . Parse ( parent , block , pc )
}
func goldmarkDefaultParser ( ) parser . Parser {
return parser . NewParser ( parser . WithBlockParsers ( parser . DefaultBlockParsers ( ) ... ) ,
parser . WithInlineParsers ( [ ] util . PrioritizedValue {
util . Prioritized ( parser . NewCodeSpanParser ( ) , 100 ) ,
util . Prioritized ( parser . NewLinkParser ( ) , 200 ) ,
util . Prioritized ( parser . NewAutoLinkParser ( ) , 300 ) ,
util . Prioritized ( parser . NewRawHTMLParser ( ) , 400 ) ,
util . Prioritized ( goldmarkNewEmphasisParser ( ) , 500 ) ,
} ... ) ,
parser . WithParagraphTransformers ( parser . DefaultParagraphTransformers ( ) ... ) ,
)
}
2023-04-18 03:05:19 +08:00
// SpecializedMarkdown sets up the Gitea specific markdown extensions
2024-11-18 13:25:42 +08: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 (
2026-01-23 15:24:58 -05:00
goldmark . WithParser ( goldmarkDefaultParser ( ) ) ,
2024-11-18 13:25:42 +08:00
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 13:25:42 +08:00
highlighting . WithWrapperRenderer ( r . highlightingRenderer ) ,
2019-12-31 01:53:28 +00:00
) ,
2024-12-14 13:43:05 +08:00
math . NewExtension ( & ctx . RenderInternal , math . Options {
2025-04-05 11:56:48 +08:00
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
2024-12-14 13:43:05 +08:00
} ) ,
2024-11-18 13:25:42 +08:00
) ,
goldmark . WithParserOptions (
parser . WithAttribute ( ) ,
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-18 03:05:19 +08:00
}
2014-04-10 14:20:58 -04:00
2024-11-18 13:25:42 +08: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 11:46:47 +01:00
w : output ,
2021-03-15 23:20:05 +00:00
limit : setting . UI . MaxDisplayFileSize * 3 ,
2015-09-01 08:32:02 -04:00
}
2021-03-15 23:20:05 +00:00
2025-03-14 03:14:35 +08:00
// 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 )
2021-11-19 11:46:47 +01: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-20 06:25:08 +08:00
return
}
2021-11-19 11:46:47 +01:00
2025-03-14 03:14:35 +08:00
log . Error ( "Panic in markdown: %v\n%s" , err , log . Stack ( 2 ) )
escapedHTML := template . HTMLEscapeString ( giteautil . UnsafeBytesToString ( buf ) )
_ , _ = output . Write ( giteautil . UnsafeStringToBytes ( escapedHTML ) )
2021-03-15 23:20:05 +00:00
} ( )
2021-11-19 11:46:47 +01:00
pc := newParserContext ( ctx )
2022-09-13 17:33:37 +01:00
2023-06-13 02:44:47 -04:00
// Preserve original length.
bufWithMetadataLength := len ( buf )
2025-04-03 13:48:24 +08:00
rc := & RenderConfig { Meta : markup . RenderMetaAsDetails }
2022-09-13 17:33:37 +01:00
buf , _ = ExtractMetadataBytes ( buf , rc )
2025-06-18 03:48:09 +02:00
metaLength := max ( bufWithMetadataLength - len ( buf ) , 0 )
2023-06-13 02:44:47 -04:00
rc . metaLength = metaLength
2022-09-13 17:33:37 +01:00
pc . Set ( renderConfigKey , rc )
if err := converter . Convert ( buf , lw , parser . WithContext ( pc ) ) ; err != nil {
2021-11-19 11:46:47 +01:00
log . Error ( "Unable to render: %v" , err )
return err
}
return nil
2021-03-15 23:20:05 +00:00
}
2022-01-20 18:46:10 +01:00
// MarkupName describes markup's name
var MarkupName = "markdown"
2017-04-21 15:01:08 +08:00
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
type Renderer struct { }
2017-04-21 15:01:08 +08:00
2022-06-16 11:33:23 +08:00
var _ markup . PostProcessRenderer = ( * Renderer ) ( nil )
2021-04-20 06:25:08 +08:00
func ( Renderer ) Name ( ) string {
2017-04-21 15:01:08 +08:00
return MarkupName
}
2021-04-20 06:25:08 +08:00
func ( Renderer ) NeedPostProcess ( ) bool { return true }
2021-04-13 15:06:31 +08:00
2026-01-26 10:34:38 +08:00
func ( Renderer ) FileNamePatterns ( ) [ ] string {
return setting . Markdown . FileNamePatterns
2017-04-21 15:01:08 +08:00
}
2021-06-23 23:09:51 +02:00
func ( Renderer ) SanitizerRules ( ) [ ] setting . MarkupSanitizerRule {
return [ ] setting . MarkupSanitizerRule { }
}
2021-04-20 06:25:08 +08:00
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 {
2024-11-22 13:48:09 +08:00
ctx . RenderOptions . MarkupType = MarkupName
2021-04-20 06:25:08 +08:00
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
2024-03-01 15:11:51 +08:00
func RenderString ( ctx * markup . RenderContext , content string ) ( template . HTML , error ) {
2021-04-20 06:25:08 +08:00
var buf strings . Builder
if err := Render ( ctx , strings . NewReader ( content ) , & buf ) ; err != nil {
2026-05-20 02:08:11 +08:00
log . Warn ( "Unable to RenderString: %v, content: %s" , err , giteautil . TruncateRunes ( content , 200 ) )
err = nil
return template . HTML ( template . HTMLEscapeString ( content ) ) , err
2021-04-20 06:25:08 +08:00
}
2024-03-01 15:11:51 +08:00
return template . HTML ( 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 {
2021-11-19 11:46:47 +01: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-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
}