@@ -27,196 +27,159 @@ func (t *RichText) ParseMarkdown(content string) {
27
27
t .Refresh ()
28
28
}
29
29
30
- type markdownRenderer struct {
31
- blockquote bool
32
- heading bool
33
- nextSeg RichTextSegment
34
- parentStack [][]RichTextSegment
35
- segs []RichTextSegment
36
- }
30
+ type markdownRenderer []RichTextSegment
37
31
38
32
func (m * markdownRenderer ) AddOptions (... renderer.Option ) {}
39
33
40
34
func (m * markdownRenderer ) Render (_ io.Writer , source []byte , n ast.Node ) error {
41
- m .nextSeg = & TextSegment {}
42
- err := ast .Walk (n , func (n ast.Node , entering bool ) (ast.WalkStatus , error ) {
43
- if ! entering {
44
- if n .Kind ().String () == "Heading" {
45
- m .segs = append (m .segs , m .nextSeg )
46
- m .heading = false
47
- }
48
- return ast .WalkContinue , m .handleExitNode (n )
49
- }
50
-
51
- switch n .Kind ().String () {
52
- case "List" :
53
- // prepare a new child level
54
- m .parentStack = append (m .parentStack , m .segs )
55
- m .segs = nil
56
- case "ListItem" :
57
- // prepare a new item level
58
- m .parentStack = append (m .parentStack , m .segs )
59
- m .segs = nil
60
- case "Heading" :
61
- m .heading = true
62
- switch n .(* ast.Heading ).Level {
63
- case 1 :
64
- m .nextSeg = & TextSegment {
65
- Style : RichTextStyleHeading ,
66
- }
67
- case 2 :
68
- m .nextSeg = & TextSegment {
69
- Style : RichTextStyleSubHeading ,
70
- }
71
- default :
72
- m .nextSeg = & TextSegment {
73
- Style : RichTextStyleParagraph ,
74
- }
75
- m .nextSeg .(* TextSegment ).Style .TextStyle .Bold = true
76
- }
77
- case "HorizontalRule" , "ThematicBreak" :
78
- m .segs = append (m .segs , & SeparatorSegment {})
79
- case "Link" :
80
- m .nextSeg = makeLink (n .(* ast.Link ))
81
- case "Paragraph" :
82
- m .nextSeg = & TextSegment {
83
- Style : RichTextStyleInline , // we make it a paragraph at the end if there are no more elements
84
- }
85
- if m .blockquote {
86
- m .nextSeg .(* TextSegment ).Style = RichTextStyleBlockquote
87
- }
88
- case "CodeSpan" :
89
- m .nextSeg = & TextSegment {
90
- Style : RichTextStyleCodeInline ,
91
- }
92
- case "CodeBlock" , "FencedCodeBlock" :
93
- var data []byte
94
- lines := n .Lines ()
95
- for i := 0 ; i < lines .Len (); i ++ {
96
- line := lines .At (i )
97
- data = append (data , line .Value (source )... )
98
- }
99
- if len (data ) == 0 {
100
- return ast .WalkContinue , nil
101
- }
102
- if data [len (data )- 1 ] == '\n' {
103
- data = data [:len (data )- 1 ]
104
- }
105
- m .segs = append (m .segs , & TextSegment {
106
- Style : RichTextStyleCodeBlock ,
107
- Text : string (data ),
108
- })
109
- case "Emph" , "Emphasis" :
110
- switch n .(* ast.Emphasis ).Level {
111
- case 2 :
112
- m .nextSeg = & TextSegment {
113
- Style : RichTextStyleStrong ,
114
- }
115
- default :
116
- m .nextSeg = & TextSegment {
117
- Style : RichTextStyleEmphasis ,
118
- }
119
- }
120
- case "Strong" :
121
- m .nextSeg = & TextSegment {
122
- Style : RichTextStyleStrong ,
123
- }
124
- case "Text" :
125
- ret := addTextToSegment (string (n .Text (source )), m .nextSeg , n )
126
- if ret != 0 {
127
- return ret , nil
128
- }
129
-
130
- _ , isImage := m .nextSeg .(* ImageSegment )
131
- if ! m .heading && ! isImage {
132
- m .segs = append (m .segs , m .nextSeg )
133
- }
134
- case "Blockquote" :
135
- m .blockquote = true
136
- case "Image" :
137
- m .nextSeg = makeImage (n .(* ast.Image )) // remember this for applying title
138
- m .segs = append (m .segs , m .nextSeg )
139
- }
140
-
141
- return ast .WalkContinue , nil
142
- })
35
+ segs , err := renderNode (source , n , false )
36
+ * m = segs
143
37
return err
144
38
}
145
39
146
- func (m * markdownRenderer ) handleExitNode (n ast.Node ) error {
147
- if n .Kind ().String () == "Blockquote" {
148
- m .blockquote = false
149
- } else if n .Kind ().String () == "List" {
150
- listSegs := m .segs
151
- m .segs = m .parentStack [len (m .parentStack )- 1 ]
152
- m .parentStack = m .parentStack [:len (m .parentStack )- 1 ]
153
- marker := n .(* ast.List ).Marker
154
- m .segs = append (m .segs , & ListSegment {Items : listSegs , Ordered : marker != '*' && marker != '-' && marker != '+' })
155
- } else if n .Kind ().String () == "ListItem" {
156
- itemSegs := m .segs
157
- m .segs = m .parentStack [len (m .parentStack )- 1 ]
158
- m .parentStack = m .parentStack [:len (m .parentStack )- 1 ]
159
- m .segs = append (m .segs , & ParagraphSegment {Texts : itemSegs })
160
- } else if ! m .blockquote && ! m .heading {
161
- if len (m .segs ) > 0 {
162
- if text , ok := m .segs [len (m .segs )- 1 ].(* TextSegment ); ok && n .Kind ().String () == "Paragraph" {
163
- text .Style .Inline = false
164
- }
40
+ func renderNode (source []byte , n ast.Node , blockquote bool ) ([]RichTextSegment , error ) {
41
+ switch t := n .(type ) {
42
+ case * ast.Document :
43
+ return renderChildren (source , n , blockquote )
44
+ case * ast.Paragraph :
45
+ children , err := renderChildren (source , n , blockquote )
46
+ if ! blockquote {
47
+ linebreak := & TextSegment {Style : RichTextStyleParagraph }
48
+ children = append (children , linebreak )
49
+ }
50
+ return children , err
51
+ case * ast.List :
52
+ items , err := renderChildren (source , n , blockquote )
53
+ return []RichTextSegment {
54
+ & ListSegment {Items : items , Ordered : t .Marker != '*' && t .Marker != '-' && t .Marker != '+' },
55
+ }, err
56
+ case * ast.ListItem :
57
+ texts , err := renderChildren (source , n , blockquote )
58
+ return []RichTextSegment {& ParagraphSegment {Texts : texts }}, err
59
+ case * ast.TextBlock :
60
+ return renderChildren (source , n , blockquote )
61
+ case * ast.Heading :
62
+ text := forceIntoHeadingText (source , n )
63
+ switch t .Level {
64
+ case 1 :
65
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleHeading , Text : text }}, nil
66
+ case 2 :
67
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleSubHeading , Text : text }}, nil
68
+ default :
69
+ textSegment := TextSegment {Style : RichTextStyleParagraph , Text : text }
70
+ textSegment .Style .TextStyle .Bold = true
71
+ return []RichTextSegment {& textSegment }, nil
72
+ }
73
+ case * ast.ThematicBreak :
74
+ return []RichTextSegment {& SeparatorSegment {}}, nil
75
+ case * ast.Link :
76
+ link , _ := url .Parse (string (t .Destination ))
77
+ text := forceIntoText (source , n )
78
+ return []RichTextSegment {& HyperlinkSegment {Alignment : fyne .TextAlignLeading , Text : text , URL : link }}, nil
79
+ case * ast.CodeSpan :
80
+ text := forceIntoText (source , n )
81
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleCodeInline , Text : text }}, nil
82
+ case * ast.CodeBlock , * ast.FencedCodeBlock :
83
+ var data []byte
84
+ lines := n .Lines ()
85
+ for i := 0 ; i < lines .Len (); i ++ {
86
+ line := lines .At (i )
87
+ data = append (data , line .Value (source )... )
165
88
}
166
- m . nextSeg = & TextSegment {
167
- Style : RichTextStyleInline ,
89
+ if len ( data ) == 0 {
90
+ return nil , nil
168
91
}
92
+ if data [len (data )- 1 ] == '\n' {
93
+ data = data [:len (data )- 1 ]
94
+ }
95
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleCodeBlock , Text : string (data )}}, nil
96
+ case * ast.Emphasis :
97
+ text := string (forceIntoText (source , n ))
98
+ switch t .Level {
99
+ case 2 :
100
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleStrong , Text : text }}, nil
101
+ default :
102
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleEmphasis , Text : text }}, nil
103
+ }
104
+ case * ast.Text :
105
+ text := string (t .Text (source ))
106
+ if text == "" {
107
+ // These empty text elements indicate single line breaks after non-text elements in goldmark.
108
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleInline , Text : " " }}, nil
109
+ }
110
+ text = suffixSpaceIfAppropriate (text , source , n )
111
+ if blockquote {
112
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleBlockquote , Text : text }}, nil
113
+ }
114
+ return []RichTextSegment {& TextSegment {Style : RichTextStyleInline , Text : text }}, nil
115
+ case * ast.Blockquote :
116
+ return renderChildren (source , n , true )
117
+ case * ast.Image :
118
+ dest := string (t .Destination )
119
+ u , err := storage .ParseURI (dest )
120
+ if err != nil {
121
+ u = storage .NewFileURI (dest )
122
+ }
123
+ return []RichTextSegment {& ImageSegment {Source : u , Title : string (t .Title ), Alignment : fyne .TextAlignCenter }}, nil
169
124
}
170
- return nil
125
+ return nil , nil
171
126
}
172
127
173
- func addTextToSegment (text string , s RichTextSegment , node ast.Node ) ast. WalkStatus {
174
- trimmed := strings . ReplaceAll ( text , " \n " , " " ) // newline inside paragraph is not newline
175
- if trimmed == "" {
176
- return ast . WalkContinue
128
+ func suffixSpaceIfAppropriate (text string , source [] byte , n ast.Node ) string {
129
+ next := n . NextSibling ()
130
+ if next != nil && next . Type () == ast . TypeInline && ! strings . HasSuffix ( text , " " ) {
131
+ return text + " "
177
132
}
178
- if t , ok := s .(* TextSegment ); ok {
179
- next := node .(* ast.Text ).NextSibling ()
180
- if next != nil {
181
- if nextText , ok := next .(* ast.Text ); ok {
182
- if nextText .Segment .Start > node .(* ast.Text ).Segment .Stop { // detect presence of a trailing newline
183
- trimmed = trimmed + " "
184
- }
185
- }
186
- }
133
+ return text
134
+ }
187
135
188
- t .Text = t .Text + trimmed
189
- }
190
- if link , ok := s .(* HyperlinkSegment ); ok {
191
- link .Text = link .Text + trimmed
136
+ func renderChildren (source []byte , n ast.Node , blockquote bool ) ([]RichTextSegment , error ) {
137
+ children := make ([]RichTextSegment , 0 , n .ChildCount ())
138
+ for childCount , child := n .ChildCount (), n .FirstChild (); childCount > 0 ; childCount -- {
139
+ segs , err := renderNode (source , child , blockquote )
140
+ if err != nil {
141
+ return children , err
142
+ }
143
+ children = append (children , segs ... )
144
+ child = child .NextSibling ()
192
145
}
193
- return 0
146
+ return children , nil
194
147
}
195
148
196
- func makeImage (n * ast.Image ) * ImageSegment {
197
- dest := string (n .Destination )
198
- u , err := storage .ParseURI (dest )
199
- if err != nil {
200
- u = storage .NewFileURI (dest )
201
- }
202
- return & ImageSegment {Source : u , Title : string (n .Title ), Alignment : fyne .TextAlignCenter }
149
+ func forceIntoText (source []byte , n ast.Node ) string {
150
+ texts := make ([]string , 0 )
151
+ ast .Walk (n , func (n2 ast.Node , entering bool ) (ast.WalkStatus , error ) {
152
+ if entering {
153
+ switch t := n2 .(type ) {
154
+ case * ast.Text :
155
+ texts = append (texts , string (t .Text (source )))
156
+ }
157
+ }
158
+ return ast .WalkContinue , nil
159
+ })
160
+ return strings .Join (texts , " " )
203
161
}
204
162
205
- func makeLink (n * ast.Link ) * HyperlinkSegment {
206
- link , _ := url .Parse (string (n .Destination ))
207
- return & HyperlinkSegment {fyne .TextAlignLeading , "" , link , nil }
163
+ func forceIntoHeadingText (source []byte , n ast.Node ) string {
164
+ var text strings.Builder
165
+ ast .Walk (n , func (n2 ast.Node , entering bool ) (ast.WalkStatus , error ) {
166
+ if entering {
167
+ switch t := n2 .(type ) {
168
+ case * ast.Text :
169
+ text .Write (t .Text (source ))
170
+ }
171
+ }
172
+ return ast .WalkContinue , nil
173
+ })
174
+ return text .String ()
208
175
}
209
176
210
177
func parseMarkdown (content string ) []RichTextSegment {
211
- r := & markdownRenderer {}
212
- if content == "" {
213
- return r .segs
214
- }
215
-
216
- md := goldmark .New (goldmark .WithRenderer (r ))
178
+ r := markdownRenderer {}
179
+ md := goldmark .New (goldmark .WithRenderer (& r ))
217
180
err := md .Convert ([]byte (content ), nil )
218
181
if err != nil {
219
182
fyne .LogError ("Failed to parse markdown" , err )
220
183
}
221
- return r . segs
184
+ return r
222
185
}
0 commit comments