Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOCS: TOC builder (for functions and providers) #2197

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
142 changes: 142 additions & 0 deletions build/generate/docuLocalLinkRectifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"bufio"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
)

func walkFolderForFilesToReplaceAllFileLinks(root, ext string) error {
var files []string
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

if filepath.Ext(path) == ext {
files = append(files, path)
}

return nil
})

if err != nil {
return err
}

for _, file := range files {
// fmt.Printf("==%s==\n", file)
err := processFileForLinks(root, file, files)
if err != nil {
return err
}
}

return err
}

// processFileForLinks searches and replaces markdown links that refer to a
// file of identical name also in the folder heirarchy somewhere.
func processFileForLinks(root, path string, files []string) error {
dirtyFlag := false
file, err := os.Open(path)
if err != nil {
return err
}
fileinfo, err := file.Stat()
if fileinfo.Size() == 0 {
file.Close()
return nil
}
if err != nil {
return err
}

defer file.Close()

currentFileFolder := filepath.Dir(path)
scanner := bufio.NewScanner(file)
newLines := []string{}

for scanner.Scan() {
line := scanner.Text()
// look for markdown link [...](...) pattern:
linkRegex := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)

linkMatchPositions := linkRegex.FindAllStringSubmatchIndex(line, -1)
if linkMatchPositions != nil {
dirtyFlag = true

var replacementline string
matchStart := 0

for x, match := range linkMatchPositions {
replacementline += line[matchStart:linkMatchPositions[x][0]]
linkText := line[match[2]:match[3]] //link ref
linkPath := line[match[4]:match[5]] //link to .md

linkFilename := filepath.Base(linkPath)
linkFilepath := getLinkFilepathFromFileList(files, linkFilename)
if linkFilepath != "" {
//if the linked file is found within the list of files from of our folder walk, rewrite the link.

// build a path to the linked file from the currently open file
linkPathRelToCurrentFile, err := filepath.Rel(currentFileFolder, linkFilepath)
if err != nil {
return err
}
if filepath.Dir(linkFilepath) == currentFileFolder {
//[current file] and [file target of the current link] are in the same folder: link with no path prefix.
// fmt.Printf("Test result same folder:[%s](%s)\n", linkText, linkFilename)
replacementline += "[" + linkText + "](" + linkFilename + ")"
} else {
//[current file] and [file target of the current link] are in different folders: link with relative path prefix.
// fmt.Printf("Test result different folder:[%s](%s)\n", linkText, linkPathRelToCurrentFile) //, linkFilename)
replacementline += "[" + linkText + "](" + linkPathRelToCurrentFile + ")"
}
} else {
replacementline += "[" + linkText + "](" + linkPath + ")"
}

// Update the start index for the next match (could be multiple links per line)
matchStart = match[5] + 1
}
if len(line)-matchStart > 0 {
replacementline += line[matchStart:]
}
// fmt.Printf("replacementline: %s\n", replacementline)
line = replacementline
}
newLines = append(newLines, line)
}

if err := scanner.Err(); err != nil {
return err
}

newContent := strings.Join(newLines, "\n") + "\n"
if dirtyFlag {
err = ioutil.WriteFile(path, []byte(newContent), 0644)
if err != nil {
return err
}
}

return nil
}

func getLinkFilepathFromFileList(files []string, filename string) string {
for _, file := range files {
if filepath.Base(file) == filepath.Base(filename) {
return file
}
}
return ""
}
178 changes: 178 additions & 0 deletions build/generate/docuTOC.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"os"
"path/filepath"
"sort"
"strings"
)

func generateDocuTOC(folderPath string, targetFile string, onlyThisPath string, startMarker string, endMarker string) error {
if folderPath == "" {
return fmt.Errorf("empty docutoc path")
}

var exclusivePath string
if onlyThisPath != "" {
// if onlyThisPath is provided, build a list of files exclusively in this (sub)folder
exclusivePath = string(filepath.Separator) + onlyThisPath
}
// Find all the markdown files in the specified folder and its subfolders.
markdownFiles, err := findMarkdownFiles(folderPath + exclusivePath)
if err != nil {
return err
}

//First sort by folders, then by filename.
sort.SliceStable(markdownFiles, func(i, j int) bool {
if filepath.Dir(markdownFiles[i]) == filepath.Dir(markdownFiles[j]) {
return strings.ToLower(markdownFiles[i]) < strings.ToLower(markdownFiles[j])
} else {
return filepath.Dir(strings.ToLower(markdownFiles[i])) < filepath.Dir(strings.ToLower(markdownFiles[j]))
}
})

// Create the table of contents.
toc := generateTableOfContents(folderPath, markdownFiles)

err = replaceTextBetweenMarkers(filepath.Join(folderPath, targetFile), startMarker, endMarker, toc)
if err != nil {
return err
}

return nil
}

// func stringInSlice(a string, list []string) bool {
// for _, b := range list {
// if b == a {
// return true
// }
// }
// return false
// }

func generateTableOfContents(folderPath string, markdownFiles []string) string {
var toc strings.Builder
currentFolder := ""

// skip over these root entries (dont print these "#" headings)
rootFolderExceptions := []string{
"documentation",
}

// dont print these folder names as bullets (which lack a link)
folderExceptions := []string{
"providers",
}
// dont print these file names as bullets
fileExceptions := []string{
"index.md",
"summary.md",
"ignore-me.md",
}

caser := cases.Title(language.Und, cases.NoLower)

for _, file := range markdownFiles {
//depthCount is folder depth for toc indentation, minus one for docu folder
depthCount := strings.Count(file, string(filepath.Separator)) - 1
filename := filepath.Base(file)

fileFolder := filepath.Dir(file)
if fileFolder != currentFolder {
// we are in a new folder

// hop over these entries altogether
if stringInSlice(strings.ToLower(fileFolder), rootFolderExceptions) {
continue
}
currentFolder = fileFolder
folderName := filepath.Base(currentFolder)

// if we're in an "exception" folder, deeper than a heading "#", skip printing it
// this has the effect of putting subentries under an entry that is already a link,
// without printing an entry to represent the folder name that is not a link
// e.g. provider md files are all links, under the provider.md file which is a link
if stringInSlice(strings.ToLower(folderName), folderExceptions) && depthCount > 1 {
continue
} else {
if depthCount > 1 {
// if we're deeper in heirarchy, print an indented bullet "*" to add to bullet heirarchy
toc.WriteString(strings.Repeat(" ", depthCount-1) + "* ")
} else {
// If we're in folder root, just print an unindented heading "#"
toc.WriteString("\n## ")
}
// Captalize folder names, replace underscores with spaces for # headings
toc.WriteString(strings.TrimSpace(caser.String(strings.ReplaceAll(folderName, "_", " "))) + "\n")
}
}
//if the file is an exception listed above, skip it.
if stringInSlice(strings.ToLower(filename), fileExceptions) {
continue
}

// naming exceptions - function names shall retain "_"
displayfilename := strings.TrimSuffix(filename, filepath.Ext(filename))
if !strings.Contains(file, "functions") {
displayfilename = strings.TrimSpace(caser.String(strings.ReplaceAll(displayfilename, "_", " ")))
}

// print the filename as a bullet, and as a [link](hyperlink)
toc.WriteString(strings.Repeat(" ", depthCount))
toc.WriteString("* [" + displayfilename + "](" + filepath.Join(".", strings.ReplaceAll(file, folderPath, "")) + ")\n")
}
return toc.String()
}

// replaceTextBetweenMarkers inserts the generated table of contents between the two markers in the specified markdown file.
func replaceTextBetweenMarkers(targetFile, startMarker, endMarker, newcontent string) error {
// Read the contents of the markdown file into memory.
input, err := os.ReadFile(targetFile)
if err != nil {
return err
}

// Find the starting and ending positions of the table of contents markers.
startPos := strings.Index(string(input), startMarker)
if startPos == -1 {
return fmt.Errorf("could not find start marker %q in file %q", startMarker, targetFile)
}
endPos := strings.Index(string(input), endMarker)
if endPos == -1 {
return fmt.Errorf("could not find end marker %q in file %q", endMarker, targetFile)
}

// Construct the new contents of the markdown file with the updated table of contents.
output := string(input[:startPos+len(startMarker)]) + newcontent + string(input[endPos:])

// Write the updated contents to the markdown file.
err = os.WriteFile(targetFile, []byte(output), 0644)
if err != nil {
return err
}

return nil
}

// findMarkdownFiles returns a list of all the markdown files in the specified folder and its subfolders.
func findMarkdownFiles(folderPath string) ([]string, error) {
markdownFiles := make([]string, 0)
err := filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".md" {
markdownFiles = append(markdownFiles, path)
}
return nil
})
if err != nil {
return nil, err
}
return markdownFiles, err
}