Skip to content

Commit 9662dd4

Browse files
committed
feat: improve refparser
1 parent 3a9e6d6 commit 9662dd4

File tree

3 files changed

+258
-743
lines changed

3 files changed

+258
-743
lines changed

refparser.go

Lines changed: 89 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,45 @@ import (
88
"go/parser"
99
"go/printer"
1010
"go/token"
11+
"html"
1112
"log"
1213
"os"
1314
"path/filepath"
1415
"regexp"
1516
"strings"
1617
)
1718

18-
// generateReferenceMarkdown creates a Markdown documentation file from Go source code comments.
19-
// It parses the package found in inputDir and writes the formatted Markdown to outputFile.
19+
// generateReferenceMarkdown generates a Markdown reference file for the package
20+
// found in inputDir and writes it to outputFile.
2021
func generateReferenceMarkdown(inputDir, outputFile string) error {
2122
if _, err := os.Stat(inputDir); os.IsNotExist(err) {
2223
return fmt.Errorf("input directory does not exist: %s", inputDir)
2324
}
2425

2526
outputDirPath := filepath.Dir(outputFile)
2627
if err := os.MkdirAll(outputDirPath, 0755); err != nil {
27-
// Log warning but continue, os.Create might still work
2828
log.Printf("Warning: Could not ensure output directory %s exists: %v", outputDirPath, err)
2929
}
3030

3131
log.Printf("Generating reference docs from '%s' to '%s'", inputDir, outputFile)
3232

3333
fset := token.NewFileSet()
3434
pkgs, err := parser.ParseDir(fset, inputDir, func(fi os.FileInfo) bool {
35-
// Basic filter to ignore test files
3635
return !strings.HasSuffix(fi.Name(), "_test.go")
3736
}, parser.ParseComments)
38-
3937
if err != nil {
4038
return fmt.Errorf("failed to parse directory %s: %w", inputDir, err)
4139
}
4240

43-
// Find the primary package in the directory
4441
var pkg *ast.Package
4542
for _, p := range pkgs {
4643
pkg = p
47-
break // Use the first package found
44+
break
4845
}
4946
if pkg == nil {
5047
return fmt.Errorf("no non-test Go package found in directory: %s", inputDir)
5148
}
5249

53-
// Extract documentation (exported symbols only by default)
5450
docPkg := doc.New(pkg, pkg.Name, doc.AllDecls)
5551

5652
out, err := os.Create(outputFile)
@@ -59,217 +55,172 @@ func generateReferenceMarkdown(inputDir, outputFile string) error {
5955
}
6056
defer out.Close()
6157

62-
// Add header
6358
header := "{{ title: Nova - Reference }}\n\n{{ include-block: doc.html markdown=\"true\" }}\n\n"
64-
_, err = out.WriteString(header)
65-
if err != nil {
59+
if _, err := out.WriteString(header); err != nil {
6660
return fmt.Errorf("failed to write header to output file: %w", err)
6761
}
6862

69-
var contentBuf bytes.Buffer
70-
71-
title := "# Reference\n\n"
72-
73-
// Package Documentation (if any)
74-
if docPkg.Doc != "" {
75-
contentBuf.WriteString(fmt.Sprintf("## %s\n\n", "Overview"))
76-
contentBuf.WriteString(formatDocText(docPkg.Doc))
77-
contentBuf.WriteString("\n\n")
78-
}
79-
80-
// Write Title
81-
_, err = out.WriteString(title)
82-
if err != nil {
83-
return fmt.Errorf("failed to write title to output file: %w", err)
84-
}
63+
var content bytes.Buffer
64+
content.WriteString("# Reference\n\n")
8565

86-
// Generate and Write Table of Contents
8766
toc := generateTOC(docPkg)
8867
if toc != "" {
89-
_, err = out.WriteString(toc)
90-
if err != nil {
91-
return fmt.Errorf("failed to write TOC to output file: %w", err)
92-
}
93-
_, err = out.WriteString("\n\n") // Add separation after TOC
94-
if err != nil {
95-
return fmt.Errorf("failed to write separator after TOC: %w", err)
96-
}
68+
content.WriteString(toc)
69+
content.WriteString("\n\n")
70+
}
71+
72+
if docPkg.Doc != "" {
73+
content.WriteString("## Overview\n\n")
74+
content.WriteString(formatDocText(docPkg.Doc))
75+
content.WriteString("\n\n")
9776
}
9877

99-
// Constants
10078
if len(docPkg.Consts) > 0 {
101-
contentBuf.WriteString("## Constants\n\n")
79+
content.WriteString("## Constants\n\n")
10280
for _, c := range docPkg.Consts {
103-
writeDocItem(&contentBuf, fset, c.Doc, c.Names, c.Decl, 3)
81+
writeDocItem(&content, fset, c.Doc, c.Names, c.Decl, 3)
10482
}
10583
}
10684

107-
// Variables
10885
if len(docPkg.Vars) > 0 {
109-
contentBuf.WriteString("## Variables\n\n")
86+
content.WriteString("## Variables\n\n")
11087
for _, v := range docPkg.Vars {
111-
writeDocItem(&contentBuf, fset, v.Doc, v.Names, v.Decl, 3)
88+
writeDocItem(&content, fset, v.Doc, v.Names, v.Decl, 3)
11289
}
11390
}
11491

115-
// Functions
11692
if len(docPkg.Funcs) > 0 {
117-
contentBuf.WriteString("## Functions\n\n")
93+
content.WriteString("## Functions\n\n")
11894
for _, f := range docPkg.Funcs {
119-
writeDocItem(&contentBuf, fset, f.Doc, []string{f.Name}, f.Decl, 3)
95+
writeDocItem(&content, fset, f.Doc, []string{f.Name}, f.Decl, 3)
12096
}
12197
}
12298

123-
// Types
12499
if len(docPkg.Types) > 0 {
125-
contentBuf.WriteString("## Types\n\n")
100+
content.WriteString("## Types\n\n")
126101
for _, t := range docPkg.Types {
127-
writeDocItem(&contentBuf, fset, t.Doc, []string{t.Name}, t.Decl, 3)
102+
fmt.Fprintf(&content, "### `%s`\n\n", t.Name)
103+
printDeclaration(&content, fset, t.Decl, t.Name)
104+
if t.Doc != "" {
105+
content.WriteString(formatDocText(t.Doc))
106+
content.WriteString("\n\n")
107+
}
128108

129109
if len(t.Consts) > 0 {
130-
contentBuf.WriteString("#### Associated Constants\n\n")
110+
content.WriteString("#### Associated Constants\n\n")
131111
for _, c := range t.Consts {
132-
writeDocItem(&contentBuf, fset, c.Doc, c.Names, c.Decl, 4)
112+
writeDocItem(&content, fset, c.Doc, c.Names, c.Decl, 4)
133113
}
134114
}
135115
if len(t.Vars) > 0 {
136-
contentBuf.WriteString("#### Associated Variables\n\n")
116+
content.WriteString("#### Associated Variables\n\n")
137117
for _, v := range t.Vars {
138-
writeDocItem(&contentBuf, fset, v.Doc, v.Names, v.Decl, 4)
118+
writeDocItem(&content, fset, v.Doc, v.Names, v.Decl, 4)
139119
}
140120
}
141121
if len(t.Funcs) > 0 {
142-
contentBuf.WriteString("#### Associated Functions\n\n")
122+
content.WriteString("#### Associated Functions\n\n")
143123
for _, f := range t.Funcs {
144-
writeDocItem(&contentBuf, fset, f.Doc, []string{f.Name}, f.Decl, 4)
124+
writeDocItem(&content, fset, f.Doc, []string{f.Name}, f.Decl, 4)
145125
}
146126
}
147127
if len(t.Methods) > 0 {
148-
contentBuf.WriteString("#### Methods\n\n")
128+
content.WriteString("#### Methods\n\n")
149129
for _, m := range t.Methods {
150-
writeDocItem(&contentBuf, fset, m.Doc, []string{m.Name}, m.Decl, 4)
130+
writeDocItem(&content, fset, m.Doc, []string{m.Name}, m.Decl, 4)
151131
}
152132
}
153133
}
154134
}
155135

156-
// Write Main Content
157-
_, err = contentBuf.WriteTo(out)
158-
if err != nil {
136+
if _, err := content.WriteTo(out); err != nil {
159137
return fmt.Errorf("failed to write content buffer to output file: %w", err)
160138
}
161139

162-
// Add footer
163140
footer := "{{ endinclude }}"
164-
_, err = out.WriteString(footer)
165-
if err != nil {
141+
if _, err := out.WriteString(footer); err != nil {
166142
return fmt.Errorf("failed to write footer to output file: %w", err)
167143
}
168144

169-
log.Printf("Successfully generated reference docs with TOC to %s", outputFile)
145+
log.Printf("Successfully generated reference docs to %s", outputFile)
170146
return nil
171147
}
172148

149+
// generateTOC builds a Table of Contents using GitHub-style heading anchors.
173150
func generateTOC(docPkg *doc.Package) string {
174151
var tocBuf bytes.Buffer
175152
tocBuf.WriteString("## Table of Contents\n\n")
176153

177154
hasContent := false
178155

179156
if docPkg.Doc != "" {
180-
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Overview", "overview"))
157+
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Overview", generateAnchor("Overview")))
181158
hasContent = true
182159
}
183-
184160
if len(docPkg.Consts) > 0 {
185-
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Constants", "constants"))
161+
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Constants", generateAnchor("Constants")))
186162
hasContent = true
187163
}
188-
189164
if len(docPkg.Vars) > 0 {
190-
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Variables", "variables"))
165+
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Variables", generateAnchor("Variables")))
191166
hasContent = true
192167
}
193-
194168
if len(docPkg.Funcs) > 0 {
195-
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Functions", "functions"))
169+
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Functions", generateAnchor("Functions")))
196170
hasContent = true
197171
}
198-
199172
if len(docPkg.Types) > 0 {
200-
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Types", "types"))
173+
tocBuf.WriteString(fmt.Sprintf("- [%s](#%s)\n", "Types", generateAnchor("Types")))
201174
hasContent = true
202175
for _, t := range docPkg.Types {
203176
typeTitle := fmt.Sprintf("`%s`", t.Name)
204177
typeAnchor := generateAnchor(t.Name)
205178
tocBuf.WriteString(fmt.Sprintf(" - [%s](#%s)\n", typeTitle, typeAnchor))
206-
207-
if len(t.Consts) > 0 || len(t.Vars) > 0 || len(t.Funcs) > 0 || len(t.Methods) > 0 {
208-
if len(t.Consts) > 0 {
209-
tocBuf.WriteString(fmt.Sprintf(" - [Associated Constants](#%s-constants)\n", typeAnchor))
210-
}
211-
if len(t.Vars) > 0 {
212-
tocBuf.WriteString(fmt.Sprintf(" - [Associated Variables](#%s-variables)\n", typeAnchor))
213-
}
214-
if len(t.Funcs) > 0 {
215-
tocBuf.WriteString(fmt.Sprintf(" - [Associated Functions](#%s-functions)\n", typeAnchor))
216-
}
217-
if len(t.Methods) > 0 {
218-
tocBuf.WriteString(fmt.Sprintf(" - [Methods](#%s-methods)\n", typeAnchor))
219-
}
220-
}
221179
}
222180
}
223181

224182
if !hasContent {
225183
return ""
226184
}
227-
228185
return tocBuf.String()
229186
}
230187

231-
// writeDocItem formats a single documentation item (const, var, func, type, method)
232-
// and writes it to the content buffer.
188+
// writeDocItem writes a documentation item heading, its declaration, and doc text.
233189
func writeDocItem(contentBuf *bytes.Buffer, fset *token.FileSet, docComment string, names []string, decl ast.Node, level int) {
234-
displayNames := strings.Join(names, ", ")
235-
itemTitle := fmt.Sprintf("`%s`", displayNames)
236-
itemAnchor := generateAnchor(displayNames)
237-
238-
// Create a clean anchor tag that is compatible with most Markdown renderers
239-
fmt.Fprintf(contentBuf, "<a id=\"%s\"></a>\n", itemAnchor)
240-
fmt.Fprintf(contentBuf, "%s %s\n\n", strings.Repeat("#", level), itemTitle)
190+
displayName := strings.Join(names, ", ")
191+
fmt.Fprintf(contentBuf, "%s `%s`\n\n", strings.Repeat("#", level), displayName)
241192

242-
// Print the declaration (signature) using go/printer
243-
var declBuf bytes.Buffer
244-
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
245-
err := cfg.Fprint(&declBuf, fset, decl)
246-
if err != nil {
247-
log.Printf("Warning: Failed to print declaration for %s: %v", displayNames, err)
248-
contentBuf.WriteString("```go\n// Error printing declaration\n```\n\n")
249-
} else {
250-
contentBuf.WriteString("```go\n")
251-
contentBuf.Write(declBuf.Bytes())
252-
contentBuf.WriteString("\n```\n\n")
253-
}
193+
printDeclaration(contentBuf, fset, decl, displayName)
254194

255-
// Write the documentation comment
256195
if docComment != "" {
257196
contentBuf.WriteString(formatDocText(docComment))
258197
contentBuf.WriteString("\n\n")
259198
}
260199
}
261200

201+
// printDeclaration writes an AST declaration into a Go code fence.
202+
func printDeclaration(buf *bytes.Buffer, fset *token.FileSet, decl ast.Node, name string) {
203+
var declBuf bytes.Buffer
204+
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
205+
if err := cfg.Fprint(&declBuf, fset, decl); err != nil {
206+
log.Printf("Warning: Failed to print declaration for %s: %v", name, err)
207+
buf.WriteString("```go\n// Error printing declaration\n```\n\n")
208+
return
209+
}
210+
buf.WriteString("```go\n")
211+
buf.Write(declBuf.Bytes())
212+
buf.WriteString("\n```\n\n")
213+
}
214+
215+
// formatDocText converts Go doc comments to Markdown, unescaping HTML,
216+
// turning "Parameters:" into a small header and cleaning code fences.
262217
func formatDocText(text string) string {
263-
// Trim leading/trailing whitespace
264218
trimmed := strings.TrimSpace(text)
265-
266-
// Use doc.ToHTML to handle formatting, then convert to Markdown
267219
var buf bytes.Buffer
268220
doc.ToHTML(&buf, trimmed, nil)
269-
html := buf.String()
221+
htmlStr := buf.String()
270222

271-
// Basic conversion from HTML to Markdown
272-
md := strings.ReplaceAll(html, "<p>", "")
223+
md := strings.ReplaceAll(htmlStr, "<p>", "")
273224
md = strings.ReplaceAll(md, "</p>", "\n\n")
274225
md = strings.ReplaceAll(md, "<pre>", "```go\n")
275226
md = strings.ReplaceAll(md, "</pre>", "\n```\n\n")
@@ -280,27 +231,32 @@ func formatDocText(text string) string {
280231
md = strings.ReplaceAll(md, "<li>", "- ")
281232
md = strings.ReplaceAll(md, "</li>", "\n")
282233

283-
// Clean up extra newlines
284234
md = strings.TrimSpace(md)
285-
md = regexp.MustCompile(`\n{3,}`).ReplaceAllString(md, "\n\n")
286235

287-
return md
288-
}
236+
// Unescape HTML entities.
237+
md = html.UnescapeString(md)
289238

290-
func generateAnchor(text string) string {
291-
// Remove backticks first
292-
text = strings.ReplaceAll(text, "`", "")
293-
text = strings.ToLower(text)
294-
text = strings.ReplaceAll(text, " ", "-")
239+
// Convert a "Parameters" paragraph into a small header.
240+
md = regexp.MustCompile(`(?m)^\s*Parameters:\s*$`).ReplaceAllString(md, "#### Parameters")
295241

296-
// Remove any characters that are not letters, numbers, or hyphens
297-
reg := regexp.MustCompile("[^a-z0-9-]+")
298-
text = reg.ReplaceAllString(text, "")
242+
// Ensure code fences don't end up with an extra blank line before the closing fence.
243+
reFence := regexp.MustCompile("(?s)```go\\n(.*?)\\n+```")
244+
md = reFence.ReplaceAllString(md, "```go\n$1\n```")
299245

300-
// Replace multiple hyphens with a single hyphen
301-
text = regexp.MustCompile("-+").ReplaceAllString(text, "-")
246+
// Collapse excessive blank lines.
247+
md = regexp.MustCompile(`\n{3,}`).ReplaceAllString(md, "\n\n")
302248

303-
// Ensure it doesn't start or end with a hyphen
304-
text = strings.Trim(text, "-")
305-
return text
249+
return strings.TrimSpace(md)
250+
}
251+
252+
// generateAnchor returns a GitHub-style slug for text.
253+
func generateAnchor(text string) string {
254+
s := strings.TrimSpace(text)
255+
s = strings.ToLower(s)
256+
s = strings.ReplaceAll(s, "`", "")
257+
s = regexp.MustCompile(`[^a-z0-9 -]+`).ReplaceAllString(s, "")
258+
s = strings.ReplaceAll(s, " ", "-")
259+
s = regexp.MustCompile(`-+`).ReplaceAllString(s, "-")
260+
s = strings.Trim(s, "-")
261+
return s
306262
}

0 commit comments

Comments
 (0)