Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214package tui
import ( "regexp" "strings"
"github.com/charmbracelet/x/ansi")
var ( headerRegex = regexp.MustCompile(`^(#{1,6})\s+(.*)`) ulRegex = regexp.MustCompile(`^(\s*)[*\-+]\s+(.*)`) olRegex = regexp.MustCompile(`^(\s*)(\d+)\.\s+(.*)`) hrRegex = regexp.MustCompile(`^(\*\*\*|---|___)$`))
// hashComments are the languages whose line comments start with "#".var hashComments = map[string]bool{ "python": true, "py": true, "sh": true, "bash": true, "zsh": true, "shell": true, "nu": true, "nix": true, "toml": true, "yaml": true, "yml": true, "ruby": true, "rb": true, "elixir": true,}
// HighlightCode marks only comments. Every other token keeps the code's own// ink: highlighting has shown little measured benefit for comprehension, and// a hue per token class drowns the few hues that carry meaning here.// Comments stay readable because they are the notes in the code.func HighlightCode(code string, lang string) string { l := strings.ToLower(lang) if l == "" || l == "text" || l == "plain" || l == "txt" { return code } hash := hashComments[l] comment := faintInk() + ansiItalic var b strings.Builder chars := []rune(code) for i := 0; i < len(chars); { ch := chars[i] if (hash && ch == '#') || (!hash && ch == '/' && i+1 < len(chars) && chars[i+1] == '/') { j := i for j < len(chars) && chars[j] != '\n' { j++ } b.WriteString(comment + string(chars[i:j]) + ansiReset + codeInk()) i = j continue } if ch == '"' || ch == '\'' || ch == '`' { j := i + 1 for j < len(chars) && chars[j] != ch && chars[j] != '\n' { if chars[j] == '\\' { j++ } j++ } j = min(j+1, len(chars)) b.WriteString(string(chars[i:j])) i = j continue } b.WriteRune(ch) i++ } return b.String()}
// A bold lead before a colon or dash labels a list item, and a wholly bold// item is a label too. Models bold nearly every item, and a signal on every// line stops signalling, so these render plain.var ( boldLeadThenMark = regexp.MustCompile(`^\*\*([^*]+)\*\*(\s*(?::|—|–|\s-\s))`) boldLeadWithMark = regexp.MustCompile(`^\*\*([^*]+(?::|—|–))\*\*`) boldWhole = regexp.MustCompile(`^\*\*([^*]+)\*\*\s*$`))
func plainLead(item string) string { switch { case boldWhole.MatchString(item): return boldWhole.ReplaceAllString(item, "$1") case boldLeadThenMark.MatchString(item): return boldLeadThenMark.ReplaceAllString(item, "$1$2") } return boldLeadWithMark.ReplaceAllString(item, "$1")}
type emphasis struct{ strong, em, code bool }
// sgr restates the whole style after a full reset, so a closing span never// strips an enclosing one and wrapOrChunkLine can reopen it on a later row.func (e emphasis) sgr() string { s := ansiReset if e.code { s += inlineCodeInk() } if e.strong { s += ansiBold } if e.em { s += ansiItalic } return s}
func inline(text string, base emphasis) string { var b strings.Builder strong, em := false, false style := func() emphasis { return emphasis{strong: base.strong || strong, em: base.em || em} } b.WriteString(base.sgr()) for i := 0; i < len(text); { rest := text[i:] switch { case rest[0] == '`': if j := strings.IndexByte(rest[1:], '`'); j >= 0 { code := style() code.code = true // dim ticks mark the edges and pad the tint, and copied text keeps its markdown tick := decorInk() + "`" b.WriteString(code.sgr() + tick + code.sgr() + rest[1:1+j] + tick + style().sgr()) i += j + 2 continue } case strings.HasPrefix(rest, "**"): if strong || strings.Contains(rest[2:], "**") { strong = !strong b.WriteString(style().sgr()) i += 2 continue } case rest[0] == '*': opens := !em && len(rest) > 1 && rest[1] != ' ' && strings.Contains(rest[2:], "*") closes := em && text[i-1] != ' ' if opens || closes { em = opens b.WriteString(style().sgr()) i++ continue } } b.WriteByte(rest[0]) i++ } return b.String() + ansiReset}
func RenderInlineMarkdown(text string) string { return inline(text, emphasis{})}
func RenderMarkdownAnsi(text string, width int) string { if width <= 0 { width = 80 } mark := func(s string) string { return decorInk() + s + ansiReset } // hang wraps body under its lead so continuation rows keep the list, // quote, or code gutter instead of falling back to column zero. hang := func(lead, cont, body string) []string { rows := wrapOrChunkLine(body, max(1, width-ansi.StringWidth(lead))) for i := range rows { if i == 0 { rows[i] = lead + rows[i] } else { rows[i] = cont + rows[i] } } return rows } lines := strings.Split(text, "\n") var out []string for i := 0; i < len(lines); i++ { line := lines[i] if strings.HasPrefix(line, "```") { lang := strings.TrimSpace(strings.TrimPrefix(line, "```")) start := i + 1 for i = start; i < len(lines) && !strings.HasPrefix(lines[i], "```"); i++ { } code := strings.Join(lines[start:min(i, len(lines))], "\n") if lang != "" { out = append(out, mark("┌─ "+lang)) } for _, hl := range strings.Split(HighlightCode(code, lang), "\n") { out = append(out, hang(mark("│")+" ", mark("│")+" ", codeInk()+hl+ansiReset)...) } out = append(out, mark("└")) continue } if h := headerRegex.FindStringSubmatch(line); h != nil { out = append(out, inline(h[2], emphasis{strong: true})) continue } if hrRegex.MatchString(strings.TrimSpace(line)) { out = append(out, mark(strings.Repeat("─", width))) continue } if ul := ulRegex.FindStringSubmatch(line); ul != nil { out = append(out, hang(ul[1]+mark("•")+" ", ul[1]+" ", inline(plainLead(ul[2]), emphasis{}))...) continue } if ol := olRegex.FindStringSubmatch(line); ol != nil { lead := ol[1] + ol[2] + ". " out = append(out, hang(ol[1]+mark(ol[2]+".")+" ", strings.Repeat(" ", len(lead)), inline(plainLead(ol[3]), emphasis{}))...) continue } if quote, ok := strings.CutPrefix(line, "> "); ok { out = append(out, hang(mark("│")+" ", mark("│")+" ", inline(quote, emphasis{}))...) continue } if line == "" { out = append(out, "") continue } out = append(out, inline(line, emphasis{})) } return strings.Join(out, "\n")}