package progdoc import ( "bufio" "io" "strings" ) type goFileMeta struct { Name string Value string } type goFilePart struct { Meta []goFileMeta Body string } func (g goFilePart) FirstMeta(name string) string { for _, meta := range g.Meta { if meta.Name == name { return meta.Value } } return "" } type goFileDocs struct { Parts []goFilePart } func (g goFileDocs) PartsWithMeta(name string) []goFilePart { var parts []goFilePart for _, part := range g.Parts { if part.FirstMeta(name) != "" { parts = append(parts, part) } } return parts } func (g goFileDocs) FirstPartWithMeta(name string) goFilePart { parts := g.PartsWithMeta(name) if len(parts) > 0 { return parts[0] } return goFilePart{} } func (g goFileDocs) HasDocs() bool { return len(g.Parts) > 0 } 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 } }