159 lines
3.8 KiB
Go
159 lines
3.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/charmbracelet/glamour"
|
|
"github.com/charmbracelet/glamour/ansi"
|
|
glamstyles "github.com/charmbracelet/glamour/styles"
|
|
)
|
|
|
|
const minMarkdownWidth = 20
|
|
|
|
func renderMarkdown(content string, width int, th theme) string {
|
|
width = max(minMarkdownWidth, width)
|
|
renderer, err := glamour.NewTermRenderer(
|
|
glamour.WithStyles(markdownStyle(th)),
|
|
glamour.WithWordWrap(width),
|
|
)
|
|
if err != nil {
|
|
return content
|
|
}
|
|
|
|
rendered, err := renderer.Render(content)
|
|
if err != nil {
|
|
return content
|
|
}
|
|
return trimRenderedMarkdown(rendered)
|
|
}
|
|
|
|
func markdownStyle(th theme) ansi.StyleConfig {
|
|
cfg := glamstyles.LightStyleConfig
|
|
if th.Mode == ThemeDark {
|
|
cfg = glamstyles.DarkStyleConfig
|
|
}
|
|
clearMarkdownBackgrounds(&cfg)
|
|
|
|
zero := uint(0)
|
|
cfg.Document.Margin = &zero
|
|
cfg.Document.BlockPrefix = ""
|
|
cfg.Document.BlockSuffix = ""
|
|
cfg.Document.Color = stringPtr(th.Text)
|
|
|
|
cfg.Heading.Color = stringPtr(th.Assistant)
|
|
cfg.H1.Prefix = ""
|
|
cfg.H1.Suffix = ""
|
|
cfg.H1.Color = stringPtr(th.Assistant)
|
|
cfg.H1.BackgroundColor = nil
|
|
cfg.H1.Bold = boolPtr(true)
|
|
|
|
cfg.BlockQuote.Color = stringPtr(th.Muted)
|
|
cfg.Link.Color = stringPtr(th.User)
|
|
cfg.LinkText.Color = stringPtr(th.User)
|
|
|
|
cfg.Code.Color = stringPtr(th.Tool)
|
|
cfg.Code.BackgroundColor = nil
|
|
cfg.CodeBlock.Color = stringPtr(th.Text)
|
|
cfg.CodeBlock.BackgroundColor = nil
|
|
cfg.CodeBlock.Margin = &zero
|
|
|
|
return cfg
|
|
}
|
|
|
|
func clearMarkdownBackgrounds(cfg *ansi.StyleConfig) {
|
|
cfg.Document.BackgroundColor = nil
|
|
cfg.BlockQuote.BackgroundColor = nil
|
|
cfg.Paragraph.BackgroundColor = nil
|
|
cfg.Heading.BackgroundColor = nil
|
|
cfg.H1.BackgroundColor = nil
|
|
cfg.H2.BackgroundColor = nil
|
|
cfg.H3.BackgroundColor = nil
|
|
cfg.H4.BackgroundColor = nil
|
|
cfg.H5.BackgroundColor = nil
|
|
cfg.H6.BackgroundColor = nil
|
|
cfg.Text.BackgroundColor = nil
|
|
cfg.Strikethrough.BackgroundColor = nil
|
|
cfg.Emph.BackgroundColor = nil
|
|
cfg.Strong.BackgroundColor = nil
|
|
cfg.HorizontalRule.BackgroundColor = nil
|
|
cfg.Item.BackgroundColor = nil
|
|
cfg.Enumeration.BackgroundColor = nil
|
|
cfg.Task.BackgroundColor = nil
|
|
cfg.Link.BackgroundColor = nil
|
|
cfg.LinkText.BackgroundColor = nil
|
|
cfg.Image.BackgroundColor = nil
|
|
cfg.ImageText.BackgroundColor = nil
|
|
cfg.Code.BackgroundColor = nil
|
|
cfg.CodeBlock.BackgroundColor = nil
|
|
cfg.Table.BackgroundColor = nil
|
|
cfg.DefinitionList.BackgroundColor = nil
|
|
cfg.DefinitionTerm.BackgroundColor = nil
|
|
cfg.DefinitionDescription.BackgroundColor = nil
|
|
cfg.HTMLBlock.BackgroundColor = nil
|
|
cfg.HTMLSpan.BackgroundColor = nil
|
|
}
|
|
|
|
func trimRenderedMarkdown(value string) string {
|
|
lines := strings.Split(strings.TrimRight(value, "\n"), "\n")
|
|
for i, line := range lines {
|
|
lines[i] = trimStyledTrailingSpaces(line)
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func trimStyledTrailingSpaces(line string) string {
|
|
cut := 0
|
|
seenNonSpace := false
|
|
seenWhitespaceAfterLastNonSpace := false
|
|
for i := 0; i < len(line); {
|
|
if end, ok := ansiSequenceEnd(line, i); ok {
|
|
if seenNonSpace && !seenWhitespaceAfterLastNonSpace {
|
|
cut = end
|
|
}
|
|
i = end
|
|
continue
|
|
}
|
|
|
|
r, size := utf8.DecodeRuneInString(line[i:])
|
|
if r == utf8.RuneError && size == 0 {
|
|
break
|
|
}
|
|
end := i + size
|
|
if unicode.IsSpace(r) {
|
|
if seenNonSpace {
|
|
seenWhitespaceAfterLastNonSpace = true
|
|
}
|
|
} else {
|
|
seenNonSpace = true
|
|
seenWhitespaceAfterLastNonSpace = false
|
|
cut = end
|
|
}
|
|
i = end
|
|
}
|
|
if cut == 0 {
|
|
return ""
|
|
}
|
|
return line[:cut]
|
|
}
|
|
|
|
func ansiSequenceEnd(value string, start int) (int, bool) {
|
|
if start+2 >= len(value) || value[start] != '\x1b' || value[start+1] != '[' {
|
|
return 0, false
|
|
}
|
|
for i := start + 2; i < len(value); i++ {
|
|
if value[i] >= 0x40 && value[i] <= 0x7e {
|
|
return i + 1, true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
func stringPtr(value string) *string {
|
|
return &value
|
|
}
|
|
|
|
func boolPtr(value bool) *bool {
|
|
return &value
|
|
}
|