package main import ( "regexp" "strings" "sync" "github.com/charmbracelet/glamour" glamouransi "github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/glamour/styles" "github.com/charmbracelet/lipgloss" xansi "github.com/charmbracelet/x/ansi" ) var commentMarkdownRenderers sync.Map var quoteRailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")) var markdownStyleName = "dark" var renderedMentionPattern = regexp.MustCompile( `(^|[^A-Za-z0-9_-])(@[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?)([^A-Za-z0-9-]|$)`, ) var sgrPattern = regexp.MustCompile(`\x1b\[[0-9:;]*m`) func renderCommentMarkdown(markdown string, width int) []string { if strings.TrimSpace(markdown) == "" { return nil } width = max(10, width) markdown = normalizeGitHubAlerts(markdown) var ( result []string block []string blockQuote bool haveBlock bool ) flush := func() { if len(block) == 0 { return } blockWidth := width if blockQuote { blockWidth = max(8, width-2) } lines := renderMarkdownFragment(strings.Join(block, "\n"), blockWidth) if haveBlock && len(lines) > 0 { result = append(result, "") } if blockQuote { rail := quoteRailStyle.Render("│ ") for i := range lines { lines[i] = rail + lines[i] } } result = append(result, lines...) haveBlock = haveBlock || len(lines) > 0 block = nil } for _, line := range strings.Split(markdown, "\n") { content, quoted := stripQuoteMarker(line) if len(block) > 0 && quoted != blockQuote { flush() } blockQuote = quoted block = append(block, content) } flush() return trimMarkdownLines(result) } func renderMarkdownFragment(markdown string, width int) []string { if strings.TrimSpace(markdown) == "" { return nil } renderer, err := commentMarkdownRenderer(width) if err != nil { return fallbackCommentLines(markdown, width) } rendered, err := renderer.Render(markdown) if err != nil { return fallbackCommentLines(markdown, width) } lines := strings.Split(strings.Trim(rendered, "\n"), "\n") for index := range lines { lines[index] = highlightRenderedMentions(lines[index]) } return trimMarkdownLines(lines) } func commentMarkdownRenderer(width int) (*glamour.TermRenderer, error) { if cached, ok := commentMarkdownRenderers.Load(width); ok { return cached.(*glamour.TermRenderer), nil } style := markdownStyleForTheme() zero := uint(0) style.Document.Margin = &zero style.Code.Prefix = "" style.Code.Suffix = "" renderer, err := glamour.NewTermRenderer( glamour.WithStyles(style), glamour.WithChromaFormatter("terminal16m"), glamour.WithWordWrap(width), glamour.WithTableWrap(true), glamour.WithPreservedNewLines(), glamour.WithEmoji(), ) if err != nil { return nil, err } actual, _ := commentMarkdownRenderers.LoadOrStore(width, renderer) return actual.(*glamour.TermRenderer), nil } func markdownStyleForTheme() glamouransi.StyleConfig { if markdownStyleName == "notty" { return styles.NoTTYStyleConfig } style := styles.DarkStyleConfig if themeIsLight { style = styles.LightStyleConfig } palette := editorMarkdownTheme color := func(value string) *string { return &value } truth := func(value bool) *bool { return &value } style.Document.Color = color(palette.Text) // Leave Text unset so inline text inherits the surrounding heading, link, // quote, or paragraph color instead of flattening every Markdown token to // the document foreground. style.Text.Color = nil style.Paragraph.Color = color(palette.Text) style.Heading.Color = color(palette.Title) style.H1.Color = color(palette.ActiveForeground) style.H1.BackgroundColor = color(palette.ActiveBackground) style.H2.Color = color(palette.Title) style.H3.Color = color(palette.Title) style.H4.Color = color(palette.Title) style.H5.Color = color(palette.Title) style.H6.Color = color(palette.Title) style.HorizontalRule.Color = color(palette.Dim) style.Item.Color = color(palette.Title) style.Enumeration.Color = color(palette.Title) style.Task.Color = color(palette.Text) style.BlockQuote.Color = color(palette.Quote) style.Strong.Color = color(palette.Text) style.Strong.Bold = truth(true) style.Emph.Color = color(palette.Text) style.Link.Color = color(themeAuthorColor(palette, 0)) style.LinkText.Color = color(themeAuthorColor(palette, 1)) style.Image.Color = color(themeAuthorColor(palette, 0)) style.ImageText.Color = color(palette.Dim) style.Code.Color = color(palette.Success) style.Code.BackgroundColor = color(palette.EditorBackground) style.CodeBlock.Color = color(palette.EditorForeground) style.CodeBlock.BackgroundColor = color(palette.EditorBackground) style.CodeBlock.Theme = palette.SyntaxTheme style.CodeBlock.Chroma = nil style.Table.Color = color(palette.Text) style.Table.CenterSeparator = stringPointer("─") style.Table.ColumnSeparator = stringPointer("│") style.Table.RowSeparator = stringPointer("─") style.DefinitionTerm.Color = color(palette.Title) style.DefinitionDescription.Color = color(palette.Text) return style } func themeAuthorColor(palette themePalette, index int) string { if len(palette.AuthorPalette) == 0 { return palette.Text } return palette.AuthorPalette[index%len(palette.AuthorPalette)] } func stringPointer(value string) *string { return &value } func stripQuoteMarker(line string) (string, bool) { trimmed := strings.TrimLeft(line, " \t") if !strings.HasPrefix(trimmed, ">") { return line, false } content := strings.TrimPrefix(trimmed, ">") content = strings.TrimPrefix(content, " ") return content, true } func normalizeGitHubAlerts(markdown string) string { alerts := map[string]string{ "[!NOTE]": "ℹ **Note**", "[!TIP]": "◆ **Tip**", "[!IMPORTANT]": "❗ **Important**", "[!WARNING]": "⚠ **Warning**", "[!CAUTION]": "⛔ **Caution**", } lines := strings.Split(markdown, "\n") for i, line := range lines { trimmed := strings.TrimSpace(line) if !strings.HasPrefix(trimmed, ">") { continue } label := strings.TrimSpace(strings.TrimPrefix(trimmed, ">")) if replacement, ok := alerts[strings.ToUpper(label)]; ok { prefix := line[:strings.Index(line, ">")+1] lines[i] = prefix + " " + replacement } } return strings.Join(lines, "\n") } func fallbackCommentLines(markdown string, width int) []string { lines := strings.Split(wrap(markdown, max(10, width)), "\n") for index := range lines { lines[index] = highlightRenderedMentions(lines[index]) } return lines } func highlightRenderedMentions(line string) string { visible, offsets := visibleTextOffsets(line) var result strings.Builder activeStyle := "" cursor := 0 searchFrom := 0 for searchFrom < len(visible) { match := renderedMentionPattern.FindStringSubmatchIndex(visible[searchFrom:]) if match == nil { break } visibleStart := searchFrom + match[4] visibleEnd := searchFrom + match[5] mentionStart := offsets[visibleStart] mentionEnd := offsets[visibleEnd-1] + 1 prefix := line[cursor:mentionStart] result.WriteString(prefix) activeStyle = activeSGR(activeStyle, prefix) mention := visible[visibleStart:visibleEnd] result.WriteString(authorStyle(strings.TrimPrefix(mention, "@")).Render(mention)) result.WriteString(activeStyle) cursor = mentionEnd searchFrom = visibleEnd } result.WriteString(line[cursor:]) return result.String() } func visibleTextOffsets(line string) (string, []int) { var visible strings.Builder offsets := make([]int, 0, len(line)) var state byte for offset := 0; offset < len(line); { sequence, _, length, nextState := xansi.GraphemeWidth.DecodeSequenceInString( line[offset:], state, nil, ) if length == 0 { break } plain := xansi.Strip(sequence) if plain != "" { relative := strings.Index(sequence, plain) if relative < 0 { relative = 0 } visible.WriteString(plain) for index := range len(plain) { offsets = append(offsets, offset+relative+index) } } offset += length state = nextState } return visible.String(), offsets } func activeSGR(active, text string) string { for _, sequence := range sgrPattern.FindAllString(text, -1) { parameters := strings.TrimSuffix(strings.TrimPrefix(sequence, "\x1b["), "m") if parameters == "" || parameters == "0" || strings.HasPrefix(parameters, "0;") || strings.HasPrefix(parameters, "0:") { active = "" } if parameters != "" && parameters != "0" { active += sequence } } return active } func trimMarkdownLines(lines []string) []string { for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" { lines = lines[1:] } for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" { lines = lines[:len(lines)-1] } return lines }