Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 115 additions & 3 deletions internal/cmd/docs/generate-cli-documentation.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package docs

import (
"bufio"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
Expand Down Expand Up @@ -49,13 +52,122 @@ title: "%s"
Example: generateDocExample,
Long: generateDocLong,
RunE: func(cmd *cobra.Command, args []string) error {
err := doc.GenMarkdownTreeCustom(root, outputDir, filePrepender, linkHandler)
if err != nil {
if err := doc.GenMarkdownTreeCustom(root, outputDir, filePrepender, linkHandler); err != nil {
return err
}
return nil
return downscaleMarkdownHeadersInDir(outputDir)
},
}
cmd.Flags().StringVar(&outputDir, "output-dir", "/tmp/datumctl-generated-doc", "Directory to use to output the generated documentation")
return cmd
}

func downscaleMarkdownHeadersInDir(root string) error {
return filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || filepath.Ext(path) != ".md" {
return nil
}
return downscaleMarkdownHeadersInFile(path)
})
}

func downscaleMarkdownHeadersInFile(filename string) error {
in, err := os.Open(filename)
if err != nil {
return err
}
defer in.Close()

tmp, err := os.CreateTemp(filepath.Dir(filename), ".md-transform-*.tmp")
if err != nil {
return err
}
defer func() {
_ = tmp.Close()
_ = os.Remove(tmp.Name())
}()

reader := bufio.NewReader(in)
writer := bufio.NewWriter(tmp)
inFence := false
var fenceMarker string

for {
line, err := reader.ReadString('\n')
isEOF := err == io.EOF
if err != nil && !isEOF {
return err
}

trimmed := strings.TrimRight(line, "\r\n")
fence := strings.TrimLeft(trimmed, " \t")
if strings.HasPrefix(fence, "```") || strings.HasPrefix(fence, "~~~") {
marker := fence[:3]
if !inFence {
inFence = true
fenceMarker = marker
} else if fenceMarker == marker {
inFence = false
fenceMarker = ""
}
} else if !inFence {
line = replaceH1WithH3(line)
line = normalizeSeeAlsoLine(line)
}

if _, werr := writer.WriteString(line); werr != nil {
return werr
}

if isEOF {
break
}
}

if err := writer.Flush(); err != nil {
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := in.Close(); err != nil {
return err
}
return os.Rename(tmp.Name(), filename)
}

func replaceH1WithH3(line string) string {
trimmed := strings.TrimLeft(line, " \t")
if len(trimmed) == 0 || trimmed[0] != '#' {
return line
}

hashes := 0
for hashes < len(trimmed) && trimmed[hashes] == '#' {
hashes++
}
if hashes != 1 {
return line
}
if len(trimmed) <= hashes || trimmed[hashes] != ' ' {
return line
}

prefixLen := len(line) - len(trimmed)
return line[:prefixLen] + "###" + trimmed[hashes:]
}

func normalizeSeeAlsoLine(line string) string {
trimmed := strings.TrimLeft(line, " \t")
if strings.HasPrefix(trimmed, "### SEE ALSO") {
prefixLen := len(line) - len(trimmed)
return line[:prefixLen] + "### See also" + trimmed[len("### SEE ALSO"):]
}
if strings.Contains(line, "[SEE ALSO](") {
return strings.ReplaceAll(line, "[SEE ALSO](", "[See also](")
}
return line
}
104 changes: 104 additions & 0 deletions internal/cmd/docs/generate-cli-documentation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package docs

import (
"os"
"path/filepath"
"testing"
)

func TestReplaceH1WithH3(t *testing.T) {
t.Parallel()

tests := []struct {
name string
in string
want string
}{
{name: "h1 to h3", in: "# Title\n", want: "### Title\n"},
{name: "h2 unchanged", in: "## Title\n", want: "## Title\n"},
{name: "h6 unchanged", in: "###### Title\n", want: "###### Title\n"},
{name: "no space unchanged", in: "#Title\n", want: "#Title\n"},
{name: "indent preserved", in: " # Title\n", want: " ### Title\n"},
{name: "not header", in: "Title\n", want: "Title\n"},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
if got := replaceH1WithH3(test.in); got != test.want {
t.Fatalf("replaceH1WithH3(%q) = %q, want %q", test.in, got, test.want)
}
})
}
}

func TestNormalizeSeeAlsoLine(t *testing.T) {
t.Parallel()

tests := []struct {
name string
in string
want string
}{
{name: "heading", in: "### SEE ALSO\n", want: "### See also\n"},
{name: "heading with indent", in: " ### SEE ALSO\n", want: " ### See also\n"},
{name: "link label", in: "* [SEE ALSO](link)\n", want: "* [See also](link)\n"},
{name: "other", in: "### Other\n", want: "### Other\n"},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
if got := normalizeSeeAlsoLine(test.in); got != test.want {
t.Fatalf("normalizeSeeAlsoLine(%q) = %q, want %q", test.in, got, test.want)
}
})
}
}

func TestDownscaleMarkdownHeadersInFile_RespectsFences(t *testing.T) {
t.Parallel()

content := "" +
"# A\n" +
"### SEE ALSO\n" +
"```bash\n" +
"# Not\n" +
"```\n" +
"## B\n" +
"~~~\n" +
"### Not2\n" +
"~~~\n"

dir := t.TempDir()
file := filepath.Join(dir, "test.md")
if err := os.WriteFile(file, []byte(content), 0644); err != nil {
t.Fatalf("write test file: %v", err)
}

if err := downscaleMarkdownHeadersInFile(file); err != nil {
t.Fatalf("downscaleMarkdownHeadersInFile: %v", err)
}

got, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read transformed file: %v", err)
}

want := "" +
"### A\n" +
"### See also\n" +
"```bash\n" +
"# Not\n" +
"```\n" +
"## B\n" +
"~~~\n" +
"### Not2\n" +
"~~~\n"

if string(got) != want {
t.Fatalf("unexpected transformed content:\n%s", string(got))
}
}