mirror of
https://github.com/traefik/traefik.git
synced 2025-09-20 21:31:14 +02:00
267 lines
5.9 KiB
Go
267 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// detect any existing <a ...> tag in the cell (case-insensitive).
|
|
reExistingAnchor = regexp.MustCompile(`(?i)<\s*a\b`)
|
|
// separator cell like --- or :---: (3+ dashes, optional leading/trailing colon).
|
|
reSepCell = regexp.MustCompile(`^\s*:?-{3,}:?\s*$`)
|
|
// markdown link [text](url) → text (used to strip link wrappers in id).
|
|
reMdLink = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
|
|
// collapse multiple hyphens.
|
|
reMultiHyphens = regexp.MustCompile(`-+`)
|
|
)
|
|
|
|
// splitTableRow splits a markdown table line on pipes, while keeping escaped pipes.
|
|
// parts[1] will be the first data cell for lines that start with '|'.
|
|
func splitTableRow(line string) []string {
|
|
var parts []string
|
|
var b strings.Builder
|
|
escaped := false
|
|
for _, r := range line {
|
|
if escaped {
|
|
b.WriteRune(r)
|
|
escaped = false
|
|
continue
|
|
}
|
|
if r == '\\' {
|
|
escaped = true
|
|
b.WriteRune(r)
|
|
continue
|
|
}
|
|
if r == '|' {
|
|
parts = append(parts, b.String())
|
|
b.Reset()
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
parts = append(parts, b.String())
|
|
return parts
|
|
}
|
|
|
|
func isTableRow(line string) bool {
|
|
s := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(s, "|") {
|
|
return false
|
|
}
|
|
parts := splitTableRow(line)
|
|
return len(parts) >= 3
|
|
}
|
|
|
|
func isSeparatorRow(line string) bool {
|
|
if !isTableRow(line) {
|
|
return false
|
|
}
|
|
parts := splitTableRow(line)
|
|
// check all middle cells (skip first and last which are outside pipes)
|
|
for i := 1; i < len(parts)-1; i++ {
|
|
cell := strings.TrimSpace(parts[i])
|
|
if cell == "" {
|
|
continue
|
|
}
|
|
if !reSepCell.MatchString(cell) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Create ID from cell text, preserving letter case, removing <br /> and markdown decorations.
|
|
func makeID(text string) string {
|
|
id := strings.TrimSpace(text)
|
|
|
|
// remove BR tags (common in table cells)
|
|
id = strings.ReplaceAll(id, "<br />", " ")
|
|
id = strings.ReplaceAll(id, "<br/>", " ")
|
|
id = strings.ReplaceAll(id, "<br>", " ")
|
|
|
|
// remove the dots
|
|
id = strings.ReplaceAll(id, ".", "-")
|
|
|
|
// strip markdown link wrappers [text](url) -> text
|
|
id = reMdLink.ReplaceAllString(id, "$1")
|
|
|
|
// remove inline markdown characters
|
|
id = strings.ReplaceAll(id, "`", "")
|
|
id = strings.ReplaceAll(id, "*", "")
|
|
id = strings.ReplaceAll(id, "~", "")
|
|
|
|
// replace spaces/underscores with hyphen
|
|
id = strings.ReplaceAll(id, " ", "-")
|
|
id = strings.ReplaceAll(id, "_", "-")
|
|
|
|
// keep only letters (both cases), digits and hyphens
|
|
var clean []rune
|
|
for _, r := range id {
|
|
if (r >= 'a' && r <= 'z') ||
|
|
(r >= 'A' && r <= 'Z') ||
|
|
(r >= '0' && r <= '9') ||
|
|
r == '-' || r == '.' {
|
|
// keep dot as you requested (we won't replace it)
|
|
clean = append(clean, r)
|
|
}
|
|
}
|
|
id = string(clean)
|
|
|
|
// collapse multiple hyphens and trim
|
|
id = reMultiHyphens.ReplaceAllString(id, "-")
|
|
id = strings.Trim(id, "-")
|
|
if id == "" {
|
|
id = "row"
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Dedupe ID within a file: if id already seen, append -2, -3...
|
|
func dedupeID(base string, seen map[string]int) string {
|
|
if base == "" {
|
|
base = "row"
|
|
}
|
|
|
|
count, ok := seen[base]
|
|
if !ok {
|
|
seen[base] = 1
|
|
return base
|
|
}
|
|
|
|
seen[base] = count + 1
|
|
return fmt.Sprintf("%s-%d", base, count+1)
|
|
}
|
|
|
|
// Inject clickable link that is also the target (id + href on same element).
|
|
func injectClickableFirstCell(line string, seen map[string]int) string {
|
|
parts := splitTableRow(line)
|
|
// first data cell is index 1
|
|
firstCellRaw := parts[1]
|
|
firstTrimmed := strings.TrimSpace(firstCellRaw)
|
|
|
|
// if there's already an <a ...> tag inside, skip (avoid nesting)
|
|
if reExistingAnchor.MatchString(firstTrimmed) {
|
|
return line
|
|
}
|
|
|
|
id := makeID(firstTrimmed)
|
|
if id == "" {
|
|
return line
|
|
}
|
|
id = dedupeID(id, seen)
|
|
|
|
// wrap the visible cell content in a link that is also the target
|
|
// keep the cell inner HTML/text (firstCellRaw) as-is inside the <a>
|
|
parts[1] = fmt.Sprintf(" <a id=\"%s\" href=\"#%s\" title=\"#%s\">%s</a> ", id, id, id, strings.TrimSpace(firstCellRaw))
|
|
return strings.Join(parts, "|")
|
|
}
|
|
|
|
func processFile(path string) error {
|
|
// read file
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var lines []string
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
lines = append(lines, sc.Text())
|
|
}
|
|
if err := sc.Err(); err != nil {
|
|
_ = f.Close()
|
|
return err
|
|
}
|
|
_ = f.Close()
|
|
|
|
inFence := false
|
|
seen := make(map[string]int)
|
|
out := make([]string, len(lines))
|
|
|
|
for i, line := range lines {
|
|
trim := strings.TrimSpace(line)
|
|
|
|
// toggle code fence (``` or ~~~)
|
|
if strings.HasPrefix(trim, "```") || strings.HasPrefix(trim, "~~~") {
|
|
inFence = !inFence
|
|
out[i] = line
|
|
continue
|
|
}
|
|
if inFence {
|
|
out[i] = line
|
|
continue
|
|
}
|
|
|
|
// not a table row -> copy as-is
|
|
if !isTableRow(line) {
|
|
out[i] = line
|
|
continue
|
|
}
|
|
|
|
// separator row -> copy as-is
|
|
if isSeparatorRow(line) {
|
|
out[i] = line
|
|
continue
|
|
}
|
|
|
|
// detect header row (the row immediately before a separator) and skip it
|
|
isHeader := false
|
|
for j := i + 1; j < len(lines); j++ {
|
|
if strings.TrimSpace(lines[j]) == "" {
|
|
continue
|
|
}
|
|
if isSeparatorRow(lines[j]) {
|
|
isHeader = true
|
|
}
|
|
break
|
|
}
|
|
if isHeader {
|
|
out[i] = line
|
|
continue
|
|
}
|
|
|
|
// otherwise inject clickable link in first cell
|
|
out[i] = injectClickableFirstCell(line, seen)
|
|
}
|
|
|
|
// overwrite file in place
|
|
wf, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bw := bufio.NewWriter(wf)
|
|
for _, l := range out {
|
|
fmt.Fprintln(bw, l)
|
|
}
|
|
if err := bw.Flush(); err != nil {
|
|
_ = wf.Close()
|
|
return err
|
|
}
|
|
return wf.Close()
|
|
}
|
|
|
|
func genAnchors() {
|
|
root := "./docs/content/reference/"
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
|
|
if perr := processFile(path); perr != nil {
|
|
fmt.Printf("⚠️ Error processing %s: %v\n", path, perr)
|
|
} else {
|
|
fmt.Printf("✅ Processed %s\n", path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Fatalf("walk error: %v", err)
|
|
}
|
|
}
|