2019-12-31 04:53:28 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package markdown
import (
"bytes"
"fmt"
2020-04-24 16:22:36 +03:00
"regexp"
2019-12-31 04:53:28 +03:00
"strings"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/common"
2020-04-24 16:22:36 +03:00
"code.gitea.io/gitea/modules/setting"
2019-12-31 04:53:28 +03:00
giteautil "code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast"
east "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
var byteMailto = [ ] byte ( "mailto:" )
2020-04-24 16:22:36 +03:00
// ASTTransformer is a default transformer of the goldmark tree.
type ASTTransformer struct { }
2019-12-31 04:53:28 +03:00
// Transform transforms the given AST tree.
2020-04-24 16:22:36 +03:00
func ( g * ASTTransformer ) Transform ( node * ast . Document , reader text . Reader , pc parser . Context ) {
firstChild := node . FirstChild ( )
createTOC := false
2022-06-08 11:59:16 +03:00
ctx := pc . Get ( renderContextKey ) . ( * markup . RenderContext )
2022-09-13 19:33:37 +03:00
rc := pc . Get ( renderConfigKey ) . ( * RenderConfig )
if rc . yamlNode != nil {
metaNode := rc . toMetaNode ( )
2020-04-24 16:22:36 +03:00
if metaNode != nil {
node . InsertBefore ( node , firstChild , metaNode )
}
createTOC = rc . TOC
2022-06-08 11:59:16 +03:00
ctx . TableOfContents = make ( [ ] markup . Header , 0 , 100 )
2020-04-24 16:22:36 +03:00
}
2019-12-31 04:53:28 +03:00
_ = ast . Walk ( node , func ( n ast . Node , entering bool ) ( ast . WalkStatus , error ) {
if ! entering {
return ast . WalkContinue , nil
}
switch v := n . ( type ) {
2020-04-24 16:22:36 +03:00
case * ast . Heading :
2022-06-08 11:59:16 +03:00
for _ , attr := range v . Attributes ( ) {
if _ , ok := attr . Value . ( [ ] byte ) ; ! ok {
v . SetAttribute ( attr . Name , [ ] byte ( fmt . Sprintf ( "%v" , attr . Value ) ) )
2021-03-16 02:20:05 +03:00
}
2020-04-24 16:22:36 +03:00
}
2022-06-08 11:59:16 +03:00
text := n . Text ( reader . Source ( ) )
header := markup . Header {
Text : util . BytesToReadOnlyString ( text ) ,
Level : v . Level ,
}
if id , found := v . AttributeString ( "id" ) ; found {
header . ID = util . BytesToReadOnlyString ( id . ( [ ] byte ) )
}
ctx . TableOfContents = append ( ctx . TableOfContents , header )
2019-12-31 04:53:28 +03:00
case * ast . Image :
// Images need two things:
//
// 1. Their src needs to munged to be a real value
// 2. If they're not wrapped with a link they need a link wrapper
// Check if the destination is a real link
link := v . Destination
if len ( link ) > 0 && ! markup . IsLink ( link ) {
prefix := pc . Get ( urlPrefixKey ) . ( string )
if pc . Get ( isWikiKey ) . ( bool ) {
prefix = giteautil . URLJoin ( prefix , "wiki" , "raw" )
}
prefix = strings . Replace ( prefix , "/src/" , "/media/" , 1 )
2021-04-10 19:26:28 +03:00
lnk := strings . TrimLeft ( string ( link ) , "/" )
2019-12-31 04:53:28 +03:00
lnk = giteautil . URLJoin ( prefix , lnk )
link = [ ] byte ( lnk )
}
v . Destination = link
parent := n . Parent ( )
// Create a link around image only if parent is not already a link
if _ , ok := parent . ( * ast . Link ) ; ! ok && parent != nil {
2021-03-14 19:36:51 +03:00
next := n . NextSibling ( )
// Create a link wrapper
2019-12-31 04:53:28 +03:00
wrap := ast . NewLink ( )
wrap . Destination = link
wrap . Title = v . Title
2021-10-11 15:12:06 +03:00
wrap . SetAttributeString ( "target" , [ ] byte ( "_blank" ) )
2021-03-14 19:36:51 +03:00
// Duplicate the current image node
image := ast . NewImage ( ast . NewLink ( ) )
image . Destination = link
image . Title = v . Title
for _ , attr := range v . Attributes ( ) {
image . SetAttribute ( attr . Name , attr . Value )
}
for child := v . FirstChild ( ) ; child != nil ; {
next := child . NextSibling ( )
image . AppendChild ( image , child )
child = next
}
// Append our duplicate image to the wrapper link
wrap . AppendChild ( wrap , image )
// Wire in the next sibling
wrap . SetNextSibling ( next )
// Replace the current node with the wrapper link
2019-12-31 04:53:28 +03:00
parent . ReplaceChild ( parent , n , wrap )
2021-03-14 19:36:51 +03:00
// But most importantly ensure the next sibling is still on the old image too
v . SetNextSibling ( next )
2019-12-31 04:53:28 +03:00
}
case * ast . Link :
// Links need their href to munged to be a real value
link := v . Destination
if len ( link ) > 0 && ! markup . IsLink ( link ) &&
link [ 0 ] != '#' && ! bytes . HasPrefix ( link , byteMailto ) {
// special case: this is not a link, a hash link or a mailto:, so it's a
// relative URL
lnk := string ( link )
if pc . Get ( isWikiKey ) . ( bool ) {
lnk = giteautil . URLJoin ( "wiki" , lnk )
}
link = [ ] byte ( giteautil . URLJoin ( pc . Get ( urlPrefixKey ) . ( string ) , lnk ) )
}
2020-01-16 14:23:48 +03:00
if len ( link ) > 0 && link [ 0 ] == '#' {
link = [ ] byte ( "#user-content-" + string ( link ) [ 1 : ] )
}
2019-12-31 04:53:28 +03:00
v . Destination = link
2020-03-23 01:25:38 +03:00
case * ast . List :
2020-05-11 02:14:49 +03:00
if v . HasChildren ( ) {
children := make ( [ ] ast . Node , 0 , v . ChildCount ( ) )
child := v . FirstChild ( )
for child != nil {
children = append ( children , child )
child = child . NextSibling ( )
}
v . RemoveChildren ( v )
for _ , child := range children {
listItem := child . ( * ast . ListItem )
if ! child . HasChildren ( ) || ! child . FirstChild ( ) . HasChildren ( ) {
v . AppendChild ( v , child )
continue
2020-04-26 08:09:08 +03:00
}
2020-05-11 02:14:49 +03:00
taskCheckBox , ok := child . FirstChild ( ) . FirstChild ( ) . ( * east . TaskCheckBox )
if ! ok {
v . AppendChild ( v , child )
continue
2020-04-26 08:09:08 +03:00
}
2020-05-11 02:14:49 +03:00
newChild := NewTaskCheckBoxListItem ( listItem )
newChild . IsChecked = taskCheckBox . IsChecked
newChild . SetAttributeString ( "class" , [ ] byte ( "task-list-item" ) )
v . AppendChild ( v , newChild )
2020-03-23 01:25:38 +03:00
}
}
2020-05-24 11:14:26 +03:00
case * ast . Text :
if v . SoftLineBreak ( ) && ! v . HardLineBreak ( ) {
renderMetas := pc . Get ( renderMetasKey ) . ( map [ string ] string )
mode := renderMetas [ "mode" ]
if mode != "document" {
v . SetHardLineBreak ( setting . Markdown . EnableHardLineBreakInComments )
} else {
v . SetHardLineBreak ( setting . Markdown . EnableHardLineBreakInDocuments )
}
}
2019-12-31 04:53:28 +03:00
}
return ast . WalkContinue , nil
} )
2020-04-24 16:22:36 +03:00
2022-06-08 11:59:16 +03:00
if createTOC && len ( ctx . TableOfContents ) > 0 {
2020-04-24 16:22:36 +03:00
lang := rc . Lang
if len ( lang ) == 0 {
lang = setting . Langs [ 0 ]
}
2022-06-08 11:59:16 +03:00
tocNode := createTOCNode ( ctx . TableOfContents , lang )
2020-04-24 16:22:36 +03:00
if tocNode != nil {
node . InsertBefore ( node , firstChild , tocNode )
}
}
if len ( rc . Lang ) > 0 {
node . SetAttributeString ( "lang" , [ ] byte ( rc . Lang ) )
}
2019-12-31 04:53:28 +03:00
}
type prefixedIDs struct {
values map [ string ] bool
}
// Generate generates a new element id.
func ( p * prefixedIDs ) Generate ( value [ ] byte , kind ast . NodeKind ) [ ] byte {
dft := [ ] byte ( "id" )
if kind == ast . KindHeading {
dft = [ ] byte ( "heading" )
}
return p . GenerateWithDefault ( value , dft )
}
// Generate generates a new element id.
2021-12-20 07:41:31 +03:00
func ( p * prefixedIDs ) GenerateWithDefault ( value , dft [ ] byte ) [ ] byte {
2019-12-31 04:53:28 +03:00
result := common . CleanValue ( value )
if len ( result ) == 0 {
result = dft
}
if ! bytes . HasPrefix ( result , [ ] byte ( "user-content-" ) ) {
result = append ( [ ] byte ( "user-content-" ) , result ... )
}
if _ , ok := p . values [ util . BytesToReadOnlyString ( result ) ] ; ! ok {
p . values [ util . BytesToReadOnlyString ( result ) ] = true
return result
}
for i := 1 ; ; i ++ {
newResult := fmt . Sprintf ( "%s-%d" , result , i )
if _ , ok := p . values [ newResult ] ; ! ok {
p . values [ newResult ] = true
return [ ] byte ( newResult )
}
}
}
// Put puts a given element id to the used ids table.
func ( p * prefixedIDs ) Put ( value [ ] byte ) {
p . values [ util . BytesToReadOnlyString ( value ) ] = true
}
func newPrefixedIDs ( ) * prefixedIDs {
return & prefixedIDs {
values : map [ string ] bool { } ,
}
}
2020-04-24 16:22:36 +03:00
// NewHTMLRenderer creates a HTMLRenderer to render
2019-12-31 04:53:28 +03:00
// in the gitea form.
2020-04-24 16:22:36 +03:00
func NewHTMLRenderer ( opts ... html . Option ) renderer . NodeRenderer {
r := & HTMLRenderer {
2019-12-31 04:53:28 +03:00
Config : html . NewConfig ( ) ,
}
for _ , opt := range opts {
opt . SetHTMLOption ( & r . Config )
}
return r
}
2020-04-24 16:22:36 +03:00
// HTMLRenderer is a renderer.NodeRenderer implementation that
// renders gitea specific features.
type HTMLRenderer struct {
2019-12-31 04:53:28 +03:00
html . Config
}
// RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
2020-04-24 16:22:36 +03:00
func ( r * HTMLRenderer ) RegisterFuncs ( reg renderer . NodeRendererFuncRegisterer ) {
reg . Register ( ast . KindDocument , r . renderDocument )
reg . Register ( KindDetails , r . renderDetails )
reg . Register ( KindSummary , r . renderSummary )
reg . Register ( KindIcon , r . renderIcon )
2020-04-26 08:09:08 +03:00
reg . Register ( KindTaskCheckBoxListItem , r . renderTaskCheckBoxListItem )
2019-12-31 04:53:28 +03:00
reg . Register ( east . KindTaskCheckBox , r . renderTaskCheckBox )
}
2020-04-24 16:22:36 +03:00
func ( r * HTMLRenderer ) renderDocument ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
n := node . ( * ast . Document )
if val , has := n . AttributeString ( "lang" ) ; has {
var err error
if entering {
_ , err = w . WriteString ( "<div" )
if err == nil {
_ , err = w . WriteString ( fmt . Sprintf ( ` lang=%q ` , val ) )
}
if err == nil {
_ , err = w . WriteRune ( '>' )
}
} else {
_ , err = w . WriteString ( "</div>" )
}
if err != nil {
return ast . WalkStop , err
}
}
return ast . WalkContinue , nil
}
func ( r * HTMLRenderer ) renderDetails ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
var err error
if entering {
_ , err = w . WriteString ( "<details>" )
} else {
_ , err = w . WriteString ( "</details>" )
}
if err != nil {
return ast . WalkStop , err
}
return ast . WalkContinue , nil
}
func ( r * HTMLRenderer ) renderSummary ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
var err error
if entering {
_ , err = w . WriteString ( "<summary>" )
} else {
_ , err = w . WriteString ( "</summary>" )
}
if err != nil {
return ast . WalkStop , err
}
return ast . WalkContinue , nil
}
var validNameRE = regexp . MustCompile ( "^[a-z ]+$" )
func ( r * HTMLRenderer ) renderIcon ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
if ! entering {
return ast . WalkContinue , nil
}
n := node . ( * Icon )
name := strings . TrimSpace ( strings . ToLower ( string ( n . Name ) ) )
if len ( name ) == 0 {
// skip this
return ast . WalkContinue , nil
}
if ! validNameRE . MatchString ( name ) {
// skip this
return ast . WalkContinue , nil
}
var err error
_ , err = w . WriteString ( fmt . Sprintf ( ` <i class="icon %s"></i> ` , name ) )
if err != nil {
return ast . WalkStop , err
}
return ast . WalkContinue , nil
}
2020-04-26 08:09:08 +03:00
func ( r * HTMLRenderer ) renderTaskCheckBoxListItem ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
n := node . ( * TaskCheckBoxListItem )
if entering {
if n . Attributes ( ) != nil {
_ , _ = w . WriteString ( "<li" )
html . RenderAttributes ( w , n , html . ListItemAttributeFilter )
_ = w . WriteByte ( '>' )
} else {
_ , _ = w . WriteString ( "<li>" )
}
2021-05-23 17:14:03 +03:00
_ , _ = w . WriteString ( ` <input type="checkbox" disabled="" ` )
segments := node . FirstChild ( ) . Lines ( )
if segments . Len ( ) > 0 {
segment := segments . At ( 0 )
_ , _ = w . WriteString ( fmt . Sprintf ( ` data-source-position="%d" ` , segment . Start ) )
2020-04-26 08:09:08 +03:00
}
if n . IsChecked {
2021-05-23 17:14:03 +03:00
_ , _ = w . WriteString ( ` checked="" ` )
2020-04-26 08:09:08 +03:00
}
2021-05-23 17:14:03 +03:00
if r . XHTML {
_ , _ = w . WriteString ( ` /> ` )
} else {
_ = w . WriteByte ( '>' )
2020-04-26 08:09:08 +03:00
}
fc := n . FirstChild ( )
if fc != nil {
if _ , ok := fc . ( * ast . TextBlock ) ; ! ok {
_ = w . WriteByte ( '\n' )
}
}
2019-12-31 04:53:28 +03:00
} else {
2020-12-13 04:05:50 +03:00
_ , _ = w . WriteString ( "</li>\n" )
2019-12-31 04:53:28 +03:00
}
return ast . WalkContinue , nil
}
2020-04-26 08:09:08 +03:00
func ( r * HTMLRenderer ) renderTaskCheckBox ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
return ast . WalkContinue , nil
}