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 }