prime8dev

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

project_markdown_render.go (4760B)


      1 package tui
      2 
      3 import "strings"
      4 
      5 // README rendering is synchronous and on demand: whenever the detail pane is
      6 // about to show the selected project (syncViewport: selection, tab switch,
      7 // resize, a README arriving), its README is rendered for the current width if
      8 // that width is not cached yet. Rendering takes a few milliseconds, and doing
      9 // it inline means the pane can never show the raw markdown while an async
     10 // render is in flight, or keep showing it because a render result was
     11 // dropped or landed for a width/README that was no longer current (what the
     12 // old async path did until something happened to re-render the pane).
     13 
     14 func (m portfolioModel) cachedProjectMarkdown(projectID string, width int) (string, bool) {
     15 	byWidth, ok := m.projectReadmeRender[projectID]
     16 	if !ok {
     17 		return "", false
     18 	}
     19 
     20 	rendered, ok := byWidth[width]
     21 	return rendered, ok
     22 }
     23 
     24 // maxCachedReadmeWidths bounds the per-project cache while a window is
     25 // being resized through many widths.
     26 const maxCachedReadmeWidths = 8
     27 
     28 func (m *portfolioModel) setCachedProjectMarkdown(projectID string, width int, rendered string) {
     29 	byWidth := m.projectReadmeRender[projectID]
     30 	if byWidth == nil || len(byWidth) >= maxCachedReadmeWidths {
     31 		byWidth = make(map[int]string)
     32 		m.projectReadmeRender[projectID] = byWidth
     33 	}
     34 
     35 	byWidth[width] = rendered
     36 }
     37 
     38 // ensureSelectedProjectMarkdown renders the selected project's README for the
     39 // current detail width when it is not cached. It reports whether it rendered.
     40 func (m *portfolioModel) ensureSelectedProjectMarkdown() bool {
     41 	if m.activeTab != tabProjects || m.projectReadmeRender == nil {
     42 		return false
     43 	}
     44 
     45 	project := m.selectedProject()
     46 	if project == nil {
     47 		return false
     48 	}
     49 
     50 	readme, ok := m.projectReadmes[project.ID]
     51 	if !ok || strings.TrimSpace(readme) == "" {
     52 		return false
     53 	}
     54 
     55 	width := m.projectMarkdownWidth()
     56 	if _, ok := m.cachedProjectMarkdown(project.ID, width); ok {
     57 		return false
     58 	}
     59 
     60 	patched := replaceFirstH1WithSourceLink(readme, project.Name, project.URL)
     61 	m.setCachedProjectMarkdown(project.ID, width, renderProjectMarkdown(patched, renderTagList(project.Tech), width))
     62 	return true
     63 }
     64 
     65 func renderProjectMarkdown(markdown string, techRow string, width int) string {
     66 	rendered := renderMarkdown(markdown, width)
     67 	if rendered == "" {
     68 		rendered = markdown
     69 	}
     70 	rendered = outdentRenderedBody(rendered)
     71 	if strings.TrimSpace(techRow) != "" {
     72 		rendered = insertTechRowBelowRenderedH1(rendered, techRow)
     73 	}
     74 	return rendered
     75 }
     76 
     77 func insertTechRowBelowRenderedH1(rendered string, techRow string) string {
     78 	if strings.TrimSpace(techRow) == "" {
     79 		return rendered
     80 	}
     81 
     82 	lines := strings.Split(rendered, "\n")
     83 	headIdx := -1
     84 	for i, line := range lines {
     85 		plain := strings.TrimSpace(ansiEscapeRE.ReplaceAllString(line, ""))
     86 		if plain != "" {
     87 			headIdx = i
     88 			break
     89 		}
     90 	}
     91 	if headIdx == -1 {
     92 		if strings.TrimSpace(rendered) == "" {
     93 			return techRow
     94 		}
     95 		return techRow + "\n\n" + rendered
     96 	}
     97 
     98 	insertAt := headingBlockEndIndex(lines, headIdx)
     99 
    100 	bodyStart := insertAt
    101 	for bodyStart < len(lines) && strings.TrimSpace(ansiEscapeRE.ReplaceAllString(lines[bodyStart], "")) == "" {
    102 		bodyStart++
    103 	}
    104 
    105 	header := strings.Join(lines[:insertAt], "\n")
    106 	if bodyStart >= len(lines) {
    107 		if strings.TrimSpace(header) == "" {
    108 			return techRow
    109 		}
    110 		return header + "\n" + techRow
    111 	}
    112 
    113 	body := strings.Join(lines[bodyStart:], "\n")
    114 	return header + "\n" + techRow + "\n\n" + body
    115 }
    116 
    117 func headingBlockEndIndex(lines []string, headIdx int) int {
    118 	for i := headIdx + 1; i < len(lines); i++ {
    119 		plain := strings.TrimSpace(ansiEscapeRE.ReplaceAllString(lines[i], ""))
    120 		if horizontalRuleLineRE.MatchString(plain) {
    121 			return i + 1
    122 		}
    123 		if plain == "" {
    124 			return i
    125 		}
    126 	}
    127 	return len(lines)
    128 }
    129 
    130 func outdentRenderedBody(rendered string) string {
    131 	lines := strings.Split(rendered, "\n")
    132 	if len(lines) < 2 {
    133 		return rendered
    134 	}
    135 
    136 	headIdx := -1
    137 	for i, line := range lines {
    138 		if strings.TrimSpace(line) != "" {
    139 			headIdx = i
    140 			break
    141 		}
    142 	}
    143 	if headIdx == -1 || headIdx == len(lines)-1 {
    144 		return rendered
    145 	}
    146 
    147 	bodyStart := headIdx + 1
    148 	for bodyStart < len(lines) && strings.TrimSpace(lines[bodyStart]) == "" {
    149 		bodyStart++
    150 	}
    151 	if bodyStart >= len(lines) {
    152 		return rendered
    153 	}
    154 
    155 	outdent := 0
    156 	for _, line := range lines[bodyStart:] {
    157 		if strings.TrimSpace(line) == "" || !strings.HasPrefix(line, " ") {
    158 			continue
    159 		}
    160 		indent := 0
    161 		for indent < len(line) && line[indent] == ' ' {
    162 			indent++
    163 		}
    164 		if outdent == 0 || indent < outdent {
    165 			outdent = indent
    166 		}
    167 	}
    168 	if outdent == 0 {
    169 		return rendered
    170 	}
    171 
    172 	for i := bodyStart; i < len(lines); i++ {
    173 		indent := 0
    174 		for indent < len(lines[i]) && indent < outdent && lines[i][indent] == ' ' {
    175 			indent++
    176 		}
    177 		lines[i] = lines[i][indent:]
    178 	}
    179 
    180 	return strings.Join(lines, "\n")
    181 }