2019-12-31 04:53:28 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2019-12-31 04:53:28 +03:00
package markdown
import (
"bytes"
"fmt"
2020-04-24 16:22:36 +03:00
"regexp"
2019-12-31 04:53:28 +03:00
"strings"
2022-10-12 08:18:26 +03:00
"code.gitea.io/gitea/modules/container"
2019-12-31 04:53:28 +03:00
"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"
2022-11-09 03:11:26 +03:00
"code.gitea.io/gitea/modules/svg"
2019-12-31 04:53:28 +03:00
giteautil "code.gitea.io/gitea/modules/util"
2022-10-21 15:00:53 +03:00
"github.com/microcosm-cc/bluemonday/css"
2019-12-31 04:53:28 +03:00
"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"
)
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 ( )
2023-04-17 22:05:19 +03:00
tocMode := ""
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 )
2023-04-17 22:05:19 +03:00
tocList := make ( [ ] markup . Header , 0 , 20 )
2022-09-13 19:33:37 +03:00
if rc . yamlNode != nil {
metaNode := rc . toMetaNode ( )
2020-04-24 16:22:36 +03:00
if metaNode != nil {
node . InsertBefore ( node , firstChild , metaNode )
}
2023-04-17 22:05:19 +03:00
tocMode = rc . TOC
2020-04-24 16:22:36 +03:00
}
2023-05-21 00:02:52 +03:00
applyElementDir := func ( n ast . Node ) {
if markup . DefaultProcessorHelper . ElementDir != "" {
n . SetAttributeString ( "dir" , [ ] byte ( markup . DefaultProcessorHelper . ElementDir ) )
}
}
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
}
2023-04-17 22:05:19 +03:00
txt := n . Text ( reader . Source ( ) )
2022-06-08 11:59:16 +03:00
header := markup . Header {
2023-04-17 22:05:19 +03:00
Text : util . BytesToReadOnlyString ( txt ) ,
2022-06-08 11:59:16 +03:00
Level : v . Level ,
}
if id , found := v . AttributeString ( "id" ) ; found {
header . ID = util . BytesToReadOnlyString ( id . ( [ ] byte ) )
}
2023-04-17 22:05:19 +03:00
tocList = append ( tocList , header )
2023-05-21 00:02:52 +03:00
applyElementDir ( v )
case * ast . Paragraph :
applyElementDir ( v )
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
2024-02-21 13:08:08 +03:00
if len ( v . Destination ) > 0 && ! markup . IsFullURLBytes ( v . Destination ) {
2024-01-27 06:36:01 +03:00
v . Destination = [ ] byte ( giteautil . URLJoin (
ctx . Links . ResolveMediaLink ( ctx . IsWiki ) ,
strings . TrimLeft ( string ( v . Destination ) , "/" ) ,
) )
2019-12-31 04:53:28 +03:00
}
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 ( )
2024-01-15 11:49:24 +03:00
wrap . Destination = v . Destination
2019-12-31 04:53:28 +03:00
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 ( ) )
2024-01-27 06:36:01 +03:00
image . Destination = v . Destination
2021-03-14 19:36:51 +03:00
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
2024-02-21 13:08:08 +03:00
isAnchorFragment := len ( link ) > 0 && link [ 0 ] == '#'
if ! isAnchorFragment && ! markup . IsFullURLBytes ( link ) {
base := ctx . Links . Base
2024-01-15 11:49:24 +03:00
if ctx . IsWiki {
base = ctx . Links . WikiLink ( )
2024-01-25 11:54:44 +03:00
} else if ctx . Links . HasBranchInfo ( ) {
base = ctx . Links . SrcLink ( )
2019-12-31 04:53:28 +03:00
}
2024-01-15 11:49:24 +03:00
link = [ ] byte ( giteautil . URLJoin ( base , string ( link ) ) )
2019-12-31 04:53:28 +03:00
}
2024-02-21 13:08:08 +03:00
if isAnchorFragment {
2020-01-16 14:23:48 +03:00
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" ) )
2023-06-13 09:44:47 +03:00
segments := newChild . FirstChild ( ) . Lines ( )
if segments . Len ( ) > 0 {
segment := segments . At ( 0 )
newChild . SourcePosition = rc . metaLength + segment . Start
}
2020-05-11 02:14:49 +03:00
v . AppendChild ( v , newChild )
2020-03-23 01:25:38 +03:00
}
}
2023-05-21 00:02:52 +03:00
applyElementDir ( v )
2020-05-24 11:14:26 +03:00
case * ast . Text :
if v . SoftLineBreak ( ) && ! v . HardLineBreak ( ) {
2024-01-15 11:49:24 +03:00
if ctx . Metas [ "mode" ] != "document" {
2020-05-24 11:14:26 +03:00
v . SetHardLineBreak ( setting . Markdown . EnableHardLineBreakInComments )
} else {
v . SetHardLineBreak ( setting . Markdown . EnableHardLineBreakInDocuments )
}
}
2022-10-21 15:00:53 +03:00
case * ast . CodeSpan :
colorContent := n . Text ( reader . Source ( ) )
if css . ColorHandler ( strings . ToLower ( string ( colorContent ) ) ) {
v . AppendChild ( v , NewColorPreview ( colorContent ) )
}
2024-02-10 21:43:09 +03:00
case * ast . Blockquote :
// We only want attention blockquotes when the AST looks like:
// Text: "["
// Text: "!TYPE"
// Text(SoftLineBreak): "]"
// grab these nodes and make sure we adhere to the attention blockquote structure
firstParagraph := v . FirstChild ( )
if firstParagraph . ChildCount ( ) < 3 {
return ast . WalkContinue , nil
}
firstTextNode , ok := firstParagraph . FirstChild ( ) . ( * ast . Text )
if ! ok || string ( firstTextNode . Segment . Value ( reader . Source ( ) ) ) != "[" {
return ast . WalkContinue , nil
}
secondTextNode , ok := firstTextNode . NextSibling ( ) . ( * ast . Text )
if ! ok || ! attentionTypeRE . MatchString ( string ( secondTextNode . Segment . Value ( reader . Source ( ) ) ) ) {
return ast . WalkContinue , nil
2022-11-09 03:11:26 +03:00
}
2024-02-10 21:43:09 +03:00
thirdTextNode , ok := secondTextNode . NextSibling ( ) . ( * ast . Text )
if ! ok || string ( thirdTextNode . Segment . Value ( reader . Source ( ) ) ) != "]" {
return ast . WalkContinue , nil
}
// grab attention type from markdown source
attentionType := strings . ToLower ( strings . TrimPrefix ( string ( secondTextNode . Segment . Value ( reader . Source ( ) ) ) , "!" ) )
// color the blockquote
v . SetAttributeString ( "class" , [ ] byte ( "gt-py-3 attention attention-" + attentionType ) )
// create an emphasis to make it bold
2024-03-08 12:30:41 +03:00
attentionParagraph := ast . NewParagraph ( )
2024-02-10 21:43:09 +03:00
emphasis := ast . NewEmphasis ( 2 )
emphasis . SetAttributeString ( "class" , [ ] byte ( "attention-" + attentionType ) )
// capitalize first letter
attentionText := ast . NewString ( [ ] byte ( strings . ToUpper ( string ( attentionType [ 0 ] ) ) + attentionType [ 1 : ] ) )
2024-03-08 12:30:41 +03:00
// replace the ![TYPE] with a dedicated paragraph of icon+Type
2024-02-10 21:43:09 +03:00
emphasis . AppendChild ( emphasis , attentionText )
2024-03-08 12:30:41 +03:00
attentionParagraph . AppendChild ( attentionParagraph , NewAttention ( attentionType ) )
attentionParagraph . AppendChild ( attentionParagraph , emphasis )
firstParagraph . Parent ( ) . InsertBefore ( firstParagraph . Parent ( ) , firstParagraph , attentionParagraph )
2024-02-10 21:43:09 +03:00
firstParagraph . RemoveChild ( firstParagraph , firstTextNode )
firstParagraph . RemoveChild ( firstParagraph , secondTextNode )
firstParagraph . RemoveChild ( firstParagraph , thirdTextNode )
2019-12-31 04:53:28 +03:00
}
return ast . WalkContinue , nil
} )
2020-04-24 16:22:36 +03:00
2023-04-17 22:05:19 +03:00
showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main"
showTocInSidebar := ! showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar
if len ( tocList ) > 0 && ( showTocInMain || showTocInSidebar ) {
if showTocInMain {
tocNode := createTOCNode ( tocList , rc . Lang , nil )
2020-04-24 16:22:36 +03:00
node . InsertBefore ( node , firstChild , tocNode )
2023-04-17 22:05:19 +03:00
} else {
tocNode := createTOCNode ( tocList , rc . Lang , map [ string ] string { "open" : "open" } )
ctx . SidebarTocNode = tocNode
2020-04-24 16:22:36 +03:00
}
}
if len ( rc . Lang ) > 0 {
node . SetAttributeString ( "lang" , [ ] byte ( rc . Lang ) )
}
2019-12-31 04:53:28 +03:00
}
type prefixedIDs struct {
2022-10-12 08:18:26 +03:00
values container . Set [ string ]
2019-12-31 04:53:28 +03:00
}
// 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 ... )
}
2022-10-12 08:18:26 +03:00
if p . values . Add ( util . BytesToReadOnlyString ( result ) ) {
2019-12-31 04:53:28 +03:00
return result
}
for i := 1 ; ; i ++ {
newResult := fmt . Sprintf ( "%s-%d" , result , i )
2022-10-12 08:18:26 +03:00
if p . values . Add ( newResult ) {
2019-12-31 04:53:28 +03:00
return [ ] byte ( newResult )
}
}
}
// Put puts a given element id to the used ids table.
func ( p * prefixedIDs ) Put ( value [ ] byte ) {
2022-10-12 08:18:26 +03:00
p . values . Add ( util . BytesToReadOnlyString ( value ) )
2019-12-31 04:53:28 +03:00
}
func newPrefixedIDs ( ) * prefixedIDs {
return & prefixedIDs {
2022-10-12 08:18:26 +03:00
values : make ( container . Set [ string ] ) ,
2019-12-31 04:53:28 +03:00
}
}
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 )
2022-10-21 15:00:53 +03:00
reg . Register ( ast . KindCodeSpan , r . renderCodeSpan )
2022-11-09 03:11:26 +03:00
reg . Register ( KindAttention , r . renderAttention )
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 )
}
2022-10-21 15:00:53 +03:00
// renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
// See #21474 for reference
func ( r * HTMLRenderer ) renderCodeSpan ( w util . BufWriter , source [ ] byte , n ast . Node , entering bool ) ( ast . WalkStatus , error ) {
if entering {
if n . Attributes ( ) != nil {
_ , _ = w . WriteString ( "<code" )
html . RenderAttributes ( w , n , html . CodeAttributeFilter )
_ = w . WriteByte ( '>' )
} else {
_ , _ = w . WriteString ( "<code>" )
}
for c := n . FirstChild ( ) ; c != nil ; c = c . NextSibling ( ) {
switch v := c . ( type ) {
case * ast . Text :
segment := v . Segment
value := segment . Value ( source )
if bytes . HasSuffix ( value , [ ] byte ( "\n" ) ) {
r . Writer . RawWrite ( w , value [ : len ( value ) - 1 ] )
r . Writer . RawWrite ( w , [ ] byte ( " " ) )
} else {
r . Writer . RawWrite ( w , value )
}
case * ColorPreview :
_ , _ = w . WriteString ( fmt . Sprintf ( ` <span class="color-preview" style="background-color: %v"></span> ` , string ( v . Color ) ) )
}
}
return ast . WalkSkipChildren , nil
}
_ , _ = w . WriteString ( "</code>" )
return ast . WalkContinue , nil
}
2022-11-09 03:11:26 +03:00
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
func ( r * HTMLRenderer ) renderAttention ( w util . BufWriter , source [ ] byte , node ast . Node , entering bool ) ( ast . WalkStatus , error ) {
if entering {
2024-02-10 21:43:09 +03:00
_ , _ = w . WriteString ( ` <span class="gt-mr-2 gt-vm attention- ` )
2022-11-09 03:11:26 +03:00
n := node . ( * Attention )
_ , _ = w . WriteString ( strings . ToLower ( n . AttentionType ) )
_ , _ = w . WriteString ( ` "> ` )
var octiconType string
switch n . AttentionType {
2024-02-10 21:43:09 +03:00
case "note" :
2022-11-09 03:11:26 +03:00
octiconType = "info"
2024-02-10 21:43:09 +03:00
case "tip" :
octiconType = "light-bulb"
case "important" :
octiconType = "report"
case "warning" :
2022-11-09 03:11:26 +03:00
octiconType = "alert"
2024-02-10 21:43:09 +03:00
case "caution" :
octiconType = "stop"
2022-11-09 03:11:26 +03:00
}
_ , _ = w . WriteString ( string ( svg . RenderHTML ( "octicon-" + octiconType ) ) )
} else {
_ , _ = w . WriteString ( "</span>\n" )
}
return ast . WalkContinue , nil
}
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 {
2023-04-17 22:05:19 +03:00
if _ , err = w . WriteString ( "<details" ) ; err != nil {
return ast . WalkStop , err
}
html . RenderAttributes ( w , node , nil )
_ , err = w . WriteString ( ">" )
2020-04-24 16:22:36 +03:00
} 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
}
2024-02-10 21:43:09 +03:00
var (
validNameRE = regexp . MustCompile ( "^[a-z ]+$" )
attentionTypeRE = regexp . MustCompile ( "^!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)$" )
)
2020-04-24 16:22:36 +03:00
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>" )
}
2023-06-13 09:44:47 +03:00
fmt . Fprintf ( w , ` <input type="checkbox" disabled="" data-source-position="%d" ` , n . SourcePosition )
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
}