markdown_wide.go (3974B)
1 package tui 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 8 "github.com/charmbracelet/x/ansi" 9 ) 10 11 // Preformatted blocks (code, ascii diagrams) wider than the pane are pulled 12 // out before markdown rendering and put back verbatim afterwards, so they 13 // overflow (and scroll horizontally) instead of wrapping. Narrow blocks keep 14 // the renderer's own styling and highlighting. 15 16 const wideBlockToken = "PRIME8WIDEBLOCK" 17 18 var ( 19 fenceOpenRE = regexp.MustCompile("^ {0,3}(`{3,}|~{3,})") 20 listItemRE = regexp.MustCompile(`^\s*([-*+]|\d+[.)])\s`) 21 wideTokenRE = regexp.MustCompile(`^\s*` + wideBlockToken + `(\d+)\s*$`) 22 codeGutter = "┃ " 23 codeGutterLen = ansi.StringWidth(codeGutter) 24 ) 25 26 // extractWideBlocks replaces every preformatted block whose longest line does 27 // not fit in width with a placeholder paragraph. 28 func extractWideBlocks(markdown string, width int) (string, [][]string) { 29 lines := strings.Split(markdown, "\n") 30 out := make([]string, 0, len(lines)) 31 var blocks [][]string 32 33 emit := func(original []string, body []string) { 34 if blockWidth(body)+codeGutterLen <= width { 35 out = append(out, original...) 36 return 37 } 38 out = append(out, "", fmt.Sprintf("%s%d", wideBlockToken, len(blocks)), "") 39 blocks = append(blocks, body) 40 } 41 42 for i := 0; i < len(lines); i++ { 43 line := lines[i] 44 45 if m := fenceOpenRE.FindStringSubmatch(line); m != nil { 46 fence := m[1] 47 end := -1 48 for j := i + 1; j < len(lines); j++ { 49 t := strings.TrimSpace(lines[j]) 50 if strings.HasPrefix(t, fence[:3]) && strings.Trim(t, fence[:1]) == "" && len(t) >= len(fence) { 51 end = j 52 break 53 } 54 } 55 if end < 0 { 56 out = append(out, lines[i:]...) 57 break 58 } 59 emit(lines[i:end+1], expandTabs(lines[i+1:end])) 60 i = end 61 continue 62 } 63 64 if isIndentedCode(line) && precededByBlankParagraph(lines, i) { 65 j := i 66 for j < len(lines) && (isIndentedCode(lines[j]) || strings.TrimSpace(lines[j]) == "") { 67 j++ 68 } 69 for j > i && strings.TrimSpace(lines[j-1]) == "" { 70 j-- 71 } 72 body := make([]string, 0, j-i) 73 for _, l := range lines[i:j] { 74 l = expandTabs([]string{l})[0] 75 if len(l) >= 4 { 76 l = l[4:] 77 } else { 78 l = "" 79 } 80 body = append(body, l) 81 } 82 emit(lines[i:j], body) 83 i = j - 1 84 continue 85 } 86 87 out = append(out, line) 88 } 89 90 return strings.Join(out, "\n"), blocks 91 } 92 93 func isIndentedCode(line string) bool { 94 return strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") 95 } 96 97 // precededByBlankParagraph reports whether an indented line starts a code 98 // block rather than continuing a list item. 99 func precededByBlankParagraph(lines []string, i int) bool { 100 if i == 0 { 101 return true 102 } 103 if strings.TrimSpace(lines[i-1]) != "" { 104 return false 105 } 106 for j := i - 1; j >= 0; j-- { 107 if strings.TrimSpace(lines[j]) == "" { 108 continue 109 } 110 return !listItemRE.MatchString(lines[j]) && !isIndentedCode(lines[j]) 111 } 112 return true 113 } 114 115 func expandTabs(lines []string) []string { 116 out := make([]string, len(lines)) 117 for i, l := range lines { 118 out[i] = strings.ReplaceAll(l, "\t", " ") 119 } 120 return out 121 } 122 123 func blockWidth(lines []string) int { 124 w := 0 125 for _, l := range lines { 126 w = max(w, ansi.StringWidth(l)) 127 } 128 return w 129 } 130 131 // restoreWideBlocks swaps the placeholders back for the verbatim block lines. 132 func restoreWideBlocks(rendered string, blocks [][]string) string { 133 if len(blocks) == 0 { 134 return rendered 135 } 136 lines := strings.Split(rendered, "\n") 137 out := make([]string, 0, len(lines)) 138 for _, line := range lines { 139 plain := ansi.Strip(line) 140 m := wideTokenRE.FindStringSubmatch(plain) 141 if m == nil { 142 out = append(out, line) 143 continue 144 } 145 var idx int 146 if _, err := fmt.Sscanf(m[1], "%d", &idx); err != nil || idx >= len(blocks) { 147 out = append(out, line) 148 continue 149 } 150 indent := plain[:len(plain)-len(strings.TrimLeft(plain, " "))] 151 for _, l := range blocks[idx] { 152 out = append(out, indent+dimStyle.Render(codeGutter)+rowStyle.Render(l)) 153 } 154 } 155 return strings.Join(out, "\n") 156 }