fckeuspy-go/vendor/fyne.io/fyne/v2/widget/markdown.go

192 lines
5.8 KiB
Go

package widget
import (
"io"
"net/url"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"fyne.io/fyne/v2"
)
// NewRichTextFromMarkdown configures a RichText widget by parsing the provided markdown content.
//
// Since: 2.1
func NewRichTextFromMarkdown(content string) *RichText {
return NewRichText(parseMarkdown(content)...)
}
// ParseMarkdown allows setting the content of this RichText widget from a markdown string.
// It will replace the content of this widget similarly to SetText, but with the appropriate formatting.
func (t *RichText) ParseMarkdown(content string) {
t.Segments = parseMarkdown(content)
t.Refresh()
}
// AppendMarkdown parses the given markdown string and appends the
// content to the widget, with the appropriate formatting.
// This API is intended for appending complete markdown documents or
// standalone fragments, and should not be used to parse a single
// markdown document piecewise.
//
// Since: 2.5
func (t *RichText) AppendMarkdown(content string) {
t.Segments = append(t.Segments, parseMarkdown(content)...)
t.Refresh()
}
type markdownRenderer []RichTextSegment
func (m *markdownRenderer) AddOptions(...renderer.Option) {}
func (m *markdownRenderer) Render(_ io.Writer, source []byte, n ast.Node) error {
segs, err := renderNode(source, n, false)
*m = segs
return err
}
func renderNode(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, error) {
switch t := n.(type) {
case *ast.Document:
return renderChildren(source, n, blockquote)
case *ast.Paragraph:
children, err := renderChildren(source, n, blockquote)
if !blockquote {
linebreak := &TextSegment{Style: RichTextStyleParagraph}
children = append(children, linebreak)
}
return children, err
case *ast.List:
items, err := renderChildren(source, n, blockquote)
return []RichTextSegment{
&ListSegment{Items: items, Ordered: t.Marker != '*' && t.Marker != '-' && t.Marker != '+'},
}, err
case *ast.ListItem:
texts, err := renderChildren(source, n, blockquote)
return []RichTextSegment{&ParagraphSegment{Texts: texts}}, err
case *ast.TextBlock:
return renderChildren(source, n, blockquote)
case *ast.Heading:
text := forceIntoHeadingText(source, n)
switch t.Level {
case 1:
return []RichTextSegment{&TextSegment{Style: RichTextStyleHeading, Text: text}}, nil
case 2:
return []RichTextSegment{&TextSegment{Style: RichTextStyleSubHeading, Text: text}}, nil
default:
textSegment := TextSegment{Style: RichTextStyleParagraph, Text: text}
textSegment.Style.TextStyle.Bold = true
return []RichTextSegment{&textSegment}, nil
}
case *ast.ThematicBreak:
return []RichTextSegment{&SeparatorSegment{}}, nil
case *ast.Link:
link, _ := url.Parse(string(t.Destination))
text := forceIntoText(source, n)
return []RichTextSegment{&HyperlinkSegment{Alignment: fyne.TextAlignLeading, Text: text, URL: link}}, nil
case *ast.CodeSpan:
text := forceIntoText(source, n)
return []RichTextSegment{&TextSegment{Style: RichTextStyleCodeInline, Text: text}}, nil
case *ast.CodeBlock, *ast.FencedCodeBlock:
var data []byte
lines := n.Lines()
for i := 0; i < lines.Len(); i++ {
line := lines.At(i)
data = append(data, line.Value(source)...)
}
if len(data) == 0 {
return nil, nil
}
if data[len(data)-1] == '\n' {
data = data[:len(data)-1]
}
return []RichTextSegment{&TextSegment{Style: RichTextStyleCodeBlock, Text: string(data)}}, nil
case *ast.Emphasis:
text := string(forceIntoText(source, n))
switch t.Level {
case 2:
return []RichTextSegment{&TextSegment{Style: RichTextStyleStrong, Text: text}}, nil
default:
return []RichTextSegment{&TextSegment{Style: RichTextStyleEmphasis, Text: text}}, nil
}
case *ast.Text:
text := string(t.Value(source))
if text == "" {
// These empty text elements indicate single line breaks after non-text elements in goldmark.
return []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: " "}}, nil
}
text = suffixSpaceIfAppropriate(text, n)
if blockquote {
return []RichTextSegment{&TextSegment{Style: RichTextStyleBlockquote, Text: text}}, nil
}
return []RichTextSegment{&TextSegment{Style: RichTextStyleInline, Text: text}}, nil
case *ast.Blockquote:
return renderChildren(source, n, true)
case *ast.Image:
return parseMarkdownImage(t), nil
}
return nil, nil
}
func suffixSpaceIfAppropriate(text string, n ast.Node) string {
next := n.NextSibling()
if next != nil && next.Type() == ast.TypeInline && !strings.HasSuffix(text, " ") {
return text + " "
}
return text
}
func renderChildren(source []byte, n ast.Node, blockquote bool) ([]RichTextSegment, error) {
children := make([]RichTextSegment, 0, n.ChildCount())
for childCount, child := n.ChildCount(), n.FirstChild(); childCount > 0; childCount-- {
segs, err := renderNode(source, child, blockquote)
if err != nil {
return children, err
}
children = append(children, segs...)
child = child.NextSibling()
}
return children, nil
}
func forceIntoText(source []byte, n ast.Node) string {
texts := make([]string, 0)
ast.Walk(n, func(n2 ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch t := n2.(type) {
case *ast.Text:
texts = append(texts, string(t.Value(source)))
}
}
return ast.WalkContinue, nil
})
return strings.Join(texts, " ")
}
func forceIntoHeadingText(source []byte, n ast.Node) string {
var text strings.Builder
ast.Walk(n, func(n2 ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
switch t := n2.(type) {
case *ast.Text:
text.Write(t.Value(source))
}
}
return ast.WalkContinue, nil
})
return text.String()
}
func parseMarkdown(content string) []RichTextSegment {
r := markdownRenderer{}
md := goldmark.New(goldmark.WithRenderer(&r))
err := md.Convert([]byte(content), nil)
if err != nil {
fyne.LogError("Failed to parse markdown", err)
}
return r
}