diff --git a/gofilereader.go b/gofilereader.go new file mode 100644 index 0000000..8fa668c --- /dev/null +++ b/gofilereader.go @@ -0,0 +1,108 @@ +package progdoc + +import ( + "bufio" + "io" + "strings" +) + +type goFileMeta struct { + Name string + Value string +} + +type goFilePart struct { + Meta []goFileMeta + Body string +} + +type goFileDocs struct { + Parts []goFilePart +} + +type allGoFileDocs struct { + Files []goFileDocs +} + +func parseGoFileDocs(r io.Reader) (goFileDocs, error) { + scanner := bufio.NewScanner(r) + var docs goFileDocs + var currentPart *goFilePart + var inDocBlock bool + + for scanner.Scan() { + line := scanner.Text() + trimmed := strings.TrimSpace(line) + + // Check if this is the start of a doc block (///) + if strings.HasPrefix(trimmed, "///") { + // Finalize previous part if exists + if currentPart != nil { + currentPart.Body = strings.TrimSpace(currentPart.Body) + docs.Parts = append(docs.Parts, *currentPart) + } + // Start a new part + currentPart = &goFilePart{} + inDocBlock = true + + // Process the first line content after /// + content := strings.TrimPrefix(trimmed, "///") + content = strings.TrimPrefix(content, " ") + if content != "" { + processPart(currentPart, content) + } + continue + } + + // Check if this continues a doc block (//) + if inDocBlock && strings.HasPrefix(trimmed, "//") && !strings.HasPrefix(trimmed, "///") { + // Extract content after // + content := strings.TrimPrefix(trimmed, "//") + content = strings.TrimPrefix(content, " ") + processPart(currentPart, content) + continue + } + + // If we hit a non-comment line, end the doc block + if inDocBlock && !strings.HasPrefix(trimmed, "//") { + inDocBlock = false + if currentPart != nil { + currentPart.Body = strings.TrimSpace(currentPart.Body) + docs.Parts = append(docs.Parts, *currentPart) + currentPart = nil + } + } + } + + // Finalize any remaining part + if currentPart != nil { + currentPart.Body = strings.TrimSpace(currentPart.Body) + docs.Parts = append(docs.Parts, *currentPart) + } + + if err := scanner.Err(); err != nil { + return goFileDocs{}, err + } + + return docs, nil +} + +func processPart(part *goFilePart, line string) { + // Check if this is a meta line (starts with :) + if strings.HasPrefix(line, ":") { + // Parse meta value + rest := strings.TrimPrefix(line, ":") + fields := strings.Fields(rest) + if len(fields) > 0 { + name := fields[0] + value := strings.Join(fields[1:], " ") + part.Meta = append(part.Meta, goFileMeta{Name: name, Value: value}) + } + } else { + // Add to body + if part.Body != "" { + part.Body += "\n" + } + part.Body += line + } +} diff --git a/gofilereader_test.go b/gofilereader_test.go new file mode 100644 index 0000000..84d239f --- /dev/null +++ b/gofilereader_test.go @@ -0,0 +1,60 @@ +package progdoc + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseGoFile(t *testing.T) { + sampleGoFile := `package main + +/// :meta this is some meta +// +// This is some content + +import "fmt" + +// Ignore me + +/// :more meta +// +// :arg stuff +// :arg more stuff +// :foo bar +// +// This is some more content +// +// this is indented +// +// More content goes here and there + +func main() { + fmt.Println("Hello, World!") +}` + + want := goFileDocs{ + Parts: []goFilePart{ + { + Meta: []goFileMeta{ + {Name: "meta", Value: "this is some meta"}, + }, + Body: "This is some content", + }, + { + Meta: []goFileMeta{ + {Name: "more", Value: "meta"}, + {Name: "arg", Value: "stuff"}, + {Name: "arg", Value: "more stuff"}, + {Name: "foo", Value: "bar"}, + }, + Body: "This is some more content\n\n this is indented\n\nMore content goes here and there", + }, + }, + } + + got, err := parseGoFileDocs(strings.NewReader(sampleGoFile)) + assert.NoError(t, err) + assert.Equal(t, want, got) +} diff --git a/pathbuilder.go b/pathbuilder.go new file mode 100644 index 0000000..f86792b --- /dev/null +++ b/pathbuilder.go @@ -0,0 +1,123 @@ +package progdoc + +import ( + "bytes" + "html/template" + "log" + "os" + "path/filepath" + "strings" +) + +func Path(path string) PathBuilder { + return PathBuilder{path: path} +} + +func (pb PathBuilder) File(file string) Option { + return Option{ + configSitemap: func(sm *siteMap) error { + _, err := os.Stat(file) + if err != nil { + return err + } + + // TODO: support things other than markdown + src, err := inferSourceFromFilename(file) + if err != nil { + return err + } + + log.Printf("Page '%s' -> %s", pb.path, file) + sm.Pages = append(sm.Pages, sitePage{ + Path: pb.path, + Source: stdLayoutSource{MainSource: src}, + }) + + return nil + }, + } +} + +func (pb PathBuilder) GoFiles(dir, templateFile string) Option { + return Option{ + configSitemap: func(sm *siteMap) error { + var allDocs allGoFileDocs + + tmplBytes, err := os.ReadFile(templateFile) + if err != nil { + return err + } + tmpl, err := template.New("").Parse(string(tmplBytes)) + if err != nil { + return err + } + + if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && info.Mode().IsRegular() { + absName, err := filepath.Abs(path) + if err != nil { + return err + } else if !strings.HasSuffix(absName, ".go") { + return nil + } + + goBts, err := os.ReadFile(path) + if err != nil { + return err + } + goFileDoc, err := parseGoFileDocs(bytes.NewReader(goBts)) + if err != nil { + return err + } + allDocs.Files = append(allDocs.Files, goFileDoc) + } + return nil + }); err != nil { + return err + } + + sm.Pages = append(sm.Pages, sitePage{ + Path: pb.path, + Source: mdTemplateSource{ + Template: tmpl, + Data: allDocs, + }, + }) + return nil + }, + } +} + +func (pb PathBuilder) Dir(dir string) Option { + return Option{ + configSitemap: func(sm *siteMap) error { + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && info.Mode().IsRegular() { + src, err := inferSourceFromFilename(path) + if err != nil { + return nil + } + + relPath, err := filepath.Rel(dir, strings.TrimSuffix(path, filepath.Ext(path))) + if err != nil { + return nil + } + targetPath := filepath.Join(pb.path, relPath) + + log.Printf("Page '%s' -> %s", targetPath, path) + sm.Pages = append(sm.Pages, sitePage{ + Path: relPath, + Source: stdLayoutSource{MainSource: src}, + }) + } + return nil + }) + }, + } +}