prime8dev

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

project_markdown_render.go (4334B)


      1 package tui
      2 
      3 import (
      4 	"strconv"
      5 	"strings"
      6 
      7 	tea "github.com/charmbracelet/bubbletea"
      8 )
      9 
     10 type projectMarkdownRenderedMsg struct {
     11 	projectID string
     12 	width     int
     13 	rendered  string
     14 }
     15 
     16 func (m portfolioModel) cachedProjectMarkdown(projectID string, width int) (string, bool) {
     17 	byWidth, ok := m.projectReadmeRender[projectID]
     18 	if !ok {
     19 		return "", false
     20 	}
     21 
     22 	rendered, ok := byWidth[width]
     23 	return rendered, ok
     24 }
     25 
     26 func (m *portfolioModel) setCachedProjectMarkdown(projectID string, width int, rendered string) {
     27 	byWidth := m.projectReadmeRender[projectID]
     28 	if byWidth == nil {
     29 		byWidth = make(map[int]string)
     30 		m.projectReadmeRender[projectID] = byWidth
     31 	}
     32 
     33 	byWidth[width] = rendered
     34 }
     35 
     36 func projectMarkdownLoadKey(projectID string, width int) string {
     37 	return projectID + ":" + strconv.Itoa(width)
     38 }
     39 
     40 func (m *portfolioModel) maybeRenderSelectedProjectMarkdownCmd() tea.Cmd {
     41 	if m.activeTab != tabProjects {
     42 		return nil
     43 	}
     44 
     45 	project := m.selectedProject()
     46 	if project == nil {
     47 		return nil
     48 	}
     49 
     50 	readme, ok := m.projectReadmes[project.ID]
     51 	if !ok || strings.TrimSpace(readme) == "" {
     52 		return nil
     53 	}
     54 
     55 	width := m.projectMarkdownWidth()
     56 	if _, ok := m.cachedProjectMarkdown(project.ID, width); ok {
     57 		return nil
     58 	}
     59 
     60 	loadKey := projectMarkdownLoadKey(project.ID, width)
     61 	if m.projectMarkdownLoads[loadKey] {
     62 		return nil
     63 	}
     64 
     65 	tagRow := renderTagList(project.Tech)
     66 	patched := replaceFirstH1WithSourceLink(readme, project.Name, project.URL)
     67 
     68 	m.projectMarkdownLoads[loadKey] = true
     69 	return renderProjectMarkdownCmd(project.ID, patched, tagRow, width)
     70 }
     71 
     72 func renderProjectMarkdownCmd(projectID string, markdown string, techRow string, width int) tea.Cmd {
     73 	return func() tea.Msg {
     74 		rendered := renderMarkdown(markdown, width)
     75 		if rendered == "" {
     76 			rendered = markdown
     77 		}
     78 		rendered = outdentRenderedBody(rendered)
     79 		if strings.TrimSpace(techRow) != "" {
     80 			rendered = insertTechRowBelowRenderedH1(rendered, techRow)
     81 		}
     82 
     83 		return projectMarkdownRenderedMsg{
     84 			projectID: projectID,
     85 			width:     width,
     86 			rendered:  rendered,
     87 		}
     88 	}
     89 }
     90 
     91 func insertTechRowBelowRenderedH1(rendered string, techRow string) string {
     92 	if strings.TrimSpace(techRow) == "" {
     93 		return rendered
     94 	}
     95 
     96 	lines := strings.Split(rendered, "\n")
     97 	headIdx := -1
     98 	for i, line := range lines {
     99 		plain := strings.TrimSpace(ansiEscapeRE.ReplaceAllString(line, ""))
    100 		if plain != "" {
    101 			headIdx = i
    102 			break
    103 		}
    104 	}
    105 	if headIdx == -1 {
    106 		if strings.TrimSpace(rendered) == "" {
    107 			return techRow
    108 		}
    109 		return techRow + "\n\n" + rendered
    110 	}
    111 
    112 	insertAt := headingBlockEndIndex(lines, headIdx)
    113 
    114 	bodyStart := insertAt
    115 	for bodyStart < len(lines) && strings.TrimSpace(ansiEscapeRE.ReplaceAllString(lines[bodyStart], "")) == "" {
    116 		bodyStart++
    117 	}
    118 
    119 	header := strings.Join(lines[:insertAt], "\n")
    120 	if bodyStart >= len(lines) {
    121 		if strings.TrimSpace(header) == "" {
    122 			return techRow
    123 		}
    124 		return header + "\n" + techRow
    125 	}
    126 
    127 	body := strings.Join(lines[bodyStart:], "\n")
    128 	return header + "\n" + techRow + "\n\n" + body
    129 }
    130 
    131 func headingBlockEndIndex(lines []string, headIdx int) int {
    132 	for i := headIdx + 1; i < len(lines); i++ {
    133 		plain := strings.TrimSpace(ansiEscapeRE.ReplaceAllString(lines[i], ""))
    134 		if horizontalRuleLineRE.MatchString(plain) {
    135 			return i + 1
    136 		}
    137 		if plain == "" {
    138 			return i
    139 		}
    140 	}
    141 	return len(lines)
    142 }
    143 
    144 func outdentRenderedBody(rendered string) string {
    145 	lines := strings.Split(rendered, "\n")
    146 	if len(lines) < 2 {
    147 		return rendered
    148 	}
    149 
    150 	headIdx := -1
    151 	for i, line := range lines {
    152 		if strings.TrimSpace(line) != "" {
    153 			headIdx = i
    154 			break
    155 		}
    156 	}
    157 	if headIdx == -1 || headIdx == len(lines)-1 {
    158 		return rendered
    159 	}
    160 
    161 	bodyStart := headIdx + 1
    162 	for bodyStart < len(lines) && strings.TrimSpace(lines[bodyStart]) == "" {
    163 		bodyStart++
    164 	}
    165 	if bodyStart >= len(lines) {
    166 		return rendered
    167 	}
    168 
    169 	outdent := 0
    170 	for _, line := range lines[bodyStart:] {
    171 		if strings.TrimSpace(line) == "" || !strings.HasPrefix(line, " ") {
    172 			continue
    173 		}
    174 		indent := 0
    175 		for indent < len(line) && line[indent] == ' ' {
    176 			indent++
    177 		}
    178 		if outdent == 0 || indent < outdent {
    179 			outdent = indent
    180 		}
    181 	}
    182 	if outdent == 0 {
    183 		return rendered
    184 	}
    185 
    186 	for i := bodyStart; i < len(lines); i++ {
    187 		indent := 0
    188 		for indent < len(lines[i]) && indent < outdent && lines[i][indent] == ' ' {
    189 			indent++
    190 		}
    191 		lines[i] = lines[i][indent:]
    192 	}
    193 
    194 	return strings.Join(lines, "\n")
    195 }