prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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 }