Go 366 lines
package guide
import (
"regexp"
"strings"
"unicode/utf8"
)
// ANSI styling. Kept to two attributes — bold for headings, table
// headers, and **bold** spans; faint for rule lines, code spans, link
// URLs, and fenced bodies. Layout (boxes, indents, rules) is identical
// with or without colour; only these escapes are added or omitted.
const (
ansiBold = "\x1b[1m"
ansiFaint = "\x1b[2m"
ansiReset = "\x1b[0m"
)
var (
reCode = regexp.MustCompile("`([^`]+)`")
reBold = regexp.MustCompile(`\*\*([^*]+)\*\*`)
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
reAutolink = regexp.MustCompile(`<(https?://[^>]+)>`)
reBullet = regexp.MustCompile(`^(\s*)[-*] (.*)$`)
)
// Render transforms the embedded Markdown manual into a terminal-
// friendly form: box-drawing tables, styled headings, and rendered
// inline markup. colour toggles ANSI styling. Layout is identical with
// or without colour, so stripping the escapes from Render(true) yields
// Render(false). Unrecognised lines pass through verbatim, so a future
// docs/USAGE.md edit can never break the guide — at worst a new
// construct renders as plain text. The source embed is untouched;
// Text() still returns the byte-identical mirror.
func Render(colour bool) string {
return renderManual(usage, colour)
}
func renderManual(src string, colour bool) string {
// Normalise line endings so a CRLF checkout (Windows) renders
// identically to an LF one — the golden is LF.
src = strings.ReplaceAll(src, "\r\n", "\n")
lines := strings.Split(src, "\n")
lines = stripTopHTMLBlock(lines)
lines = stripTrailingNavFooter(lines)
out := make([]string, 0, len(lines))
inFence := false
for i := 0; i < len(lines); i++ {
line := lines[i]
trimmed := strings.TrimSpace(line)
// Fence markers toggle verbatim mode and are dropped; bodies
// are emitted as indented blocks with no inline processing.
if strings.HasPrefix(trimmed, "```") {
inFence = !inFence
continue
}
if inFence {
out = append(out, renderFenceLine(line, colour))
continue
}
if h, ok := renderHeading(line, colour); ok {
out = append(out, h...)
continue
}
if isTableHeader(lines, i) {
block, consumed := renderTable(lines, i, colour)
out = append(out, block...)
i += consumed - 1
continue
}
out = append(out, renderProse(line, colour))
}
return strings.Join(out, "\n")
}
// stripTopHTMLBlock drops the GitHub-only header block that every
// cross-repo-fingerprint doc opens with — the centred logo + page
// title + horizontal doc-nav bar inside a `<div align="center">` —
// plus its trailing `---` separator. The block is structural noise in
// a terminal renderer (raw HTML tags would otherwise pass through as
// plain text). Anything before the first non-empty line is kept
// untouched; if the first non-empty line is not the marker, the input
// is returned unchanged.
func stripTopHTMLBlock(lines []string) []string {
first := -1
for i, l := range lines {
if strings.TrimSpace(l) != "" {
first = i
break
}
}
if first < 0 {
return lines
}
if strings.TrimSpace(lines[first]) != `<div align="center">` {
return lines
}
end := -1
for i := first + 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == `</div>` {
end = i
break
}
}
if end < 0 {
return lines
}
tail := end + 1
for tail < len(lines) && strings.TrimSpace(lines[tail]) == "" {
tail++
}
if tail < len(lines) && strings.TrimSpace(lines[tail]) == "---" {
tail++
}
return append(lines[:first:first], lines[tail:]...)
}
// stripTrailingNavFooter drops the GitHub-only `---` + `[← Prev: X] ·
// [Next: Y →]` footer the cross-repo-fingerprint doc set closes with.
// It is structural noise in the terminal renderer (the same `eeco
// guide` user has nothing to click). The check looks at the last
// non-empty content line: if it matches the Prev/Next pattern, drop
// it together with the preceding `---` rule and any blank lines.
func stripTrailingNavFooter(lines []string) []string {
last := len(lines) - 1
for last >= 0 && strings.TrimSpace(lines[last]) == "" {
last--
}
if last < 0 {
return lines
}
footer := strings.TrimSpace(lines[last])
if !strings.Contains(footer, "Prev:") && !strings.Contains(footer, "Next:") && !strings.Contains(footer, "Back to start") {
return lines
}
cut := last
for cut-1 >= 0 && strings.TrimSpace(lines[cut-1]) == "" {
cut--
}
if cut-1 >= 0 && strings.TrimSpace(lines[cut-1]) == "---" {
cut--
}
return lines[:cut]
}
// renderHeading styles an ATX heading. Level 1 and 2 gain a rule line
// underneath sized to the title; level 3+ is bold only. Returns false
// for any line that is not a `# ` heading.
func renderHeading(line string, colour bool) ([]string, bool) {
level := 0
for level < len(line) && line[level] == '#' {
level++
}
if level == 0 || level >= len(line) || line[level] != ' ' {
return nil, false
}
text := visible(strings.TrimSpace(line[level+1:]))
styled := text
if colour {
styled = ansiBold + text + ansiReset
}
switch level {
case 1:
return []string{styled, dim(strings.Repeat("═", utf8.RuneCountInString(text)), colour)}, true
case 2:
return []string{styled, dim(strings.Repeat("─", utf8.RuneCountInString(text)), colour)}, true
default:
return []string{styled}, true
}
}
// renderProse renders a non-heading, non-table, non-fenced line:
// bullet markers become a `•` glyph (indent preserved) and inline
// markup is applied. Blank and unrecognised lines pass through.
func renderProse(line string, colour bool) string {
if m := reBullet.FindStringSubmatch(line); m != nil {
return m[1] + "• " + renderInline(m[2], colour)
}
return renderInline(line, colour)
}
// renderFenceLine emits a fenced-block line verbatim, indented four
// spaces and faint when colour is on. A blank line stays blank.
func renderFenceLine(line string, colour bool) string {
if strings.TrimSpace(line) == "" {
return ""
}
indented := " " + line
if colour {
return ansiFaint + indented + ansiReset
}
return indented
}
// renderInline applies inline Markdown: `code`, **bold**, [text](url),
// and <autolink>. Code is rendered first so markup inside a code span
// is left literal.
func renderInline(s string, colour bool) string {
s = reCode.ReplaceAllStringFunc(s, func(m string) string {
inner := m[1 : len(m)-1]
if colour {
return ansiFaint + inner + ansiReset
}
return inner
})
s = reBold.ReplaceAllStringFunc(s, func(m string) string {
inner := m[2 : len(m)-2]
if colour {
return ansiBold + inner + ansiReset
}
return inner
})
s = reLink.ReplaceAllStringFunc(s, func(m string) string {
sub := reLink.FindStringSubmatch(m)
text, url := sub[1], sub[2]
if colour {
return text + " " + ansiFaint + "(" + url + ")" + ansiReset
}
return text + " (" + url + ")"
})
return reAutolink.ReplaceAllString(s, "$1")
}
// visible returns the plain visible text of an inline-markup string —
// renderInline with colour off — used for width measurement.
func visible(s string) string {
return renderInline(s, false)
}
func dim(s string, colour bool) string {
if colour {
return ansiFaint + s + ansiReset
}
return s
}
// isTableHeader reports whether line i begins a GitHub-style table: a
// pipe row immediately followed by a `| --- |` separator.
func isTableHeader(lines []string, i int) bool {
return i+1 < len(lines) && isTableRow(lines[i]) && isTableSep(lines[i+1])
}
func isTableRow(s string) bool {
t := strings.TrimSpace(s)
return len(t) >= 2 && strings.HasPrefix(t, "|") && strings.HasSuffix(t, "|")
}
func isTableSep(s string) bool {
t := strings.TrimSpace(s)
if !strings.HasPrefix(t, "|") || !strings.ContainsRune(t, '-') {
return false
}
for _, r := range t {
switch r {
case '|', '-', ':', ' ', '\t':
default:
return false
}
}
return true
}
// renderTable redraws a table block starting at lines[start] as a
// box-drawing grid. Returns the rendered lines and how many source
// lines it consumed (header + separator + body rows).
func renderTable(lines []string, start int, colour bool) ([]string, int) {
header := parseRow(lines[start])
rows := [][]string{header}
consumed := 2 // header + separator
for j := start + 2; j < len(lines) && isTableRow(lines[j]); j++ {
rows = append(rows, parseRow(lines[j]))
consumed++
}
ncols := 0
for _, r := range rows {
if len(r) > ncols {
ncols = len(r)
}
}
widths := make([]int, ncols)
for _, r := range rows {
for c := 0; c < ncols; c++ {
if c < len(r) {
if w := utf8.RuneCountInString(visible(r[c])); w > widths[c] {
widths[c] = w
}
}
}
}
border := func(left, mid, right string) string {
var b strings.Builder
b.WriteString(left)
for c := 0; c < ncols; c++ {
b.WriteString(strings.Repeat("─", widths[c]+2))
if c < ncols-1 {
b.WriteString(mid)
}
}
b.WriteString(right)
return b.String()
}
row := func(cells []string, bold bool) string {
var b strings.Builder
b.WriteString("│")
for c := 0; c < ncols; c++ {
cell := ""
if c < len(cells) {
cell = cells[c]
}
disp := renderInline(cell, colour)
if bold && colour {
disp = ansiBold + disp + ansiReset
}
padding := widths[c] - utf8.RuneCountInString(visible(cell))
b.WriteString(" " + disp + strings.Repeat(" ", padding) + " │")
}
return b.String()
}
out := []string{border("┌", "┬", "┐"), row(rows[0], true), border("├", "┼", "┤")}
for _, r := range rows[1:] {
out = append(out, row(r, false))
}
out = append(out, border("└", "┴", "┘"))
return out, consumed
}
// parseRow splits a table row into trimmed cells, honouring `\|` as a
// literal pipe rather than a column separator.
func parseRow(line string) []string {
t := strings.TrimSpace(line)
t = strings.TrimPrefix(t, "|")
t = strings.TrimSuffix(t, "|")
cells := splitUnescapedPipe(t)
for i := range cells {
cells[i] = strings.ReplaceAll(strings.TrimSpace(cells[i]), `\|`, "|")
}
return cells
}
func splitUnescapedPipe(s string) []string {
var cells []string
var b strings.Builder
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) && s[i+1] == '|' {
b.WriteByte('\\')
b.WriteByte('|')
i++
continue
}
if s[i] == '|' {
cells = append(cells, b.String())
b.Reset()
continue
}
b.WriteByte(s[i])
}
return append(cells, b.String())
}