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 `
` — // 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]) != `
` { return lines } end := -1 for i := first + 1; i < len(lines); i++ { if strings.TrimSpace(lines[i]) == `
` { 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 . 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()) }