Skip to content

Commit c7d2645

Browse files
Copilotneongreen
andcommitted
lion: Add marker-at-end format for cleaner multi-line documentation
Co-authored-by: neongreen <1523306+neongreen@users.noreply.github.com>
1 parent 1624d87 commit c7d2645

3 files changed

Lines changed: 253 additions & 9 deletions

File tree

lion/README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,20 @@ The tool generates:
5555

5656
## Comment Format
5757

58-
lion supports two comment formats:
58+
lion supports three comment formats:
5959

60-
### Single-line format
60+
### 1. Marker at end (cleanest for multi-line)
61+
62+
```go
63+
// Regular Go comment describing functionality.
64+
// Multiple lines without any lion prefix.
65+
// The marker at the end pulls in all preceding lines.
66+
//lion:topic-name
67+
```
68+
69+
This is the **recommended format** for longer documentation blocks. It keeps your comments clean and readable - just regular Go comments with a single lion marker at the end.
70+
71+
### 2. Single-line format
6172

6273
```
6374
//lion:topic-name Optional content describing this code element
@@ -67,7 +78,7 @@ lion supports two comment formats:
6778
- **Content**: Optional markdown-formatted text
6879
- Multiple consecutive `//lion:topic` lines are combined into one entry
6980

70-
### Block comment format
81+
### 3. Block comment format
7182

7283
```go
7384
/*lion:topic-name
@@ -81,10 +92,27 @@ This makes documentation cleaner.
8192
- **Content**: All subsequent lines in the block comment
8293
- Cleaner for longer documentation blocks
8394

84-
Both formats can be attached to functions, types, constants, variables, and package declarations.
95+
All formats can be attached to functions, types, constants, variables, and package declarations.
8596

8697
## Example
8798

99+
Marker at end format (recommended):
100+
101+
```go
102+
// lion is a documentation extraction tool.
103+
// Add lion comments to generate markdown docs.
104+
// This is the cleanest syntax for multi-line docs.
105+
//lion:getting-started
106+
package main
107+
108+
// The main function initializes the app.
109+
// It handles setup and configuration.
110+
//lion:architecture
111+
func main() {
112+
// ...
113+
}
114+
```
115+
88116
Single-line format:
89117

90118
```go

lion/internal/extractor/extractor.go

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,82 @@ func getEntityName(decl *ast.GenDecl) string {
101101
return ""
102102
}
103103

104-
//lion:implementation extractFromCommentGroup processes a comment group and extracts lion comments.
105-
//lion:implementation It groups consecutive lion comments by topic, combines their content, and creates DocEntry records.
106-
//lion:implementation This allows multiple lion comments on the same entity to be merged into a single documentation entry per topic.
107-
//lion:implementation Supports both single-line (//lion:topic) and block comment (/*lion:topic ... */) formats.
104+
/*lion:implementation
105+
extractFromCommentGroup processes a comment group and extracts lion comments.
106+
107+
It supports three formats:
108+
1. Single-line: //lion:topic Content (repeated on each line)
109+
2. Block: slash-star lion:topic Multi-line content star-slash
110+
3. Marker at end: Regular // comments followed by //lion:topic (pulls entire comment block)
111+
112+
The marker-at-end format is cleanest for longer documentation blocks.
113+
*/
108114
func extractFromCommentGroup(fset *token.FileSet, cg *ast.CommentGroup, filepath, entityName string, docs map[string][]DocEntry) {
109-
// Group consecutive lion comments by topic
115+
// First, check if the last comment is a lion marker (pulls entire comment group)
116+
// This only applies if there are non-lion comments before it
117+
if len(cg.List) > 1 {
118+
lastComment := cg.List[len(cg.List)-1]
119+
lastText := lastComment.Text
120+
if strings.HasPrefix(lastText, "//") {
121+
lastText = strings.TrimPrefix(lastText, "//")
122+
lastText = strings.TrimSpace(lastText)
123+
if strings.HasPrefix(lastText, "lion:") {
124+
// Check if all preceding comments are NON-lion comments
125+
hasOnlyRegularComments := true
126+
for i := 0; i < len(cg.List)-1; i++ {
127+
text := cg.List[i].Text
128+
if strings.HasPrefix(text, "//") {
129+
text = strings.TrimPrefix(text, "//")
130+
text = strings.TrimSpace(text)
131+
if strings.HasPrefix(text, "lion:") {
132+
hasOnlyRegularComments = false
133+
break
134+
}
135+
}
136+
}
137+
138+
// Only use marker-at-end if there are regular comments before the marker
139+
if hasOnlyRegularComments {
140+
// Extract topic from the last line
141+
topic, markerContent := parseLionCommentLine(lastText)
142+
if topic != "" {
143+
// Collect all preceding comments as content
144+
var contentLines []string
145+
for i := 0; i < len(cg.List)-1; i++ {
146+
text := cg.List[i].Text
147+
if strings.HasPrefix(text, "//") {
148+
text = strings.TrimPrefix(text, "//")
149+
text = strings.TrimSpace(text)
150+
if text != "" {
151+
contentLines = append(contentLines, text)
152+
}
153+
}
154+
}
155+
156+
// Add marker content if provided
157+
if markerContent != "" {
158+
contentLines = append(contentLines, markerContent)
159+
}
160+
161+
if len(contentLines) > 0 {
162+
pos := fset.Position(cg.List[0].Pos())
163+
entry := DocEntry{
164+
Topic: topic,
165+
Content: strings.Join(contentLines, "\n"),
166+
File: filepath,
167+
Line: pos.Line,
168+
Entity: entityName,
169+
}
170+
docs[topic] = append(docs[topic], entry)
171+
}
172+
return // Don't process individual comments
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
// Otherwise, process comments individually (legacy behavior)
110180
topicGroups := make(map[string][]string)
111181
topicOrder := []string{}
112182
var firstLine int

lion/internal/extractor/extractor_test.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,3 +357,149 @@ type Config struct {
357357
t.Errorf("expected 1 api entry, got %d", len(apiEntries))
358358
}
359359
}
360+
361+
func TestExtractMarkerAtEnd(t *testing.T) {
362+
tmpDir := t.TempDir()
363+
364+
// Create test Go file with marker-at-end format
365+
testFile := filepath.Join(tmpDir, "test.go")
366+
content := `// This is a multi-line comment.
367+
// It describes the functionality.
368+
// The lion marker is at the end.
369+
//lion:overview
370+
package test
371+
372+
373+
// The Config struct holds settings.
374+
// Fields can be configured via files or environment.
375+
// This demonstrates the marker-at-end format.
376+
//lion:api
377+
type Config struct {
378+
Port int
379+
}
380+
381+
// Initialize creates a new instance.
382+
// It sets up default values.
383+
//lion:api Additional info can go on the marker line
384+
func Initialize() *Config {
385+
return &Config{Port: 8080}
386+
}
387+
`
388+
389+
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
390+
t.Fatalf("failed to create test file: %v", err)
391+
}
392+
393+
// Extract documentation
394+
docs, err := Extract(tmpDir)
395+
if err != nil {
396+
t.Fatalf("Extract failed: %v", err)
397+
}
398+
399+
// Verify topics
400+
if len(docs) != 2 {
401+
t.Errorf("expected 2 topics, got %d", len(docs))
402+
}
403+
404+
// Verify overview content
405+
overviewEntries := docs["overview"]
406+
if len(overviewEntries) != 1 {
407+
t.Errorf("expected 1 overview entry, got %d", len(overviewEntries))
408+
}
409+
if len(overviewEntries) > 0 {
410+
content := overviewEntries[0].Content
411+
if !strings.Contains(content, "multi-line comment") {
412+
t.Errorf("overview content missing expected text, got: %q", content)
413+
}
414+
if !strings.Contains(content, "lion marker is at the end") {
415+
t.Errorf("overview content missing expected text, got: %q", content)
416+
}
417+
// Should NOT contain the lion marker itself
418+
if strings.Contains(content, "lion:overview") {
419+
t.Errorf("overview content should not contain lion marker, got: %q", content)
420+
}
421+
}
422+
423+
// Verify api content (should have 2 entries)
424+
apiEntries := docs["api"]
425+
if len(apiEntries) != 2 {
426+
t.Errorf("expected 2 api entries, got %d", len(apiEntries))
427+
}
428+
429+
// First api entry (Config struct)
430+
if len(apiEntries) > 0 {
431+
content := apiEntries[0].Content
432+
if !strings.Contains(content, "Config struct holds settings") {
433+
t.Errorf("api content missing expected text, got: %q", content)
434+
}
435+
if !strings.Contains(content, "marker-at-end format") {
436+
t.Errorf("api content missing expected text, got: %q", content)
437+
}
438+
}
439+
440+
// Second api entry (Initialize function) - should include marker line content
441+
if len(apiEntries) > 1 {
442+
content := apiEntries[1].Content
443+
if !strings.Contains(content, "Initialize creates a new instance") {
444+
t.Errorf("api content missing expected text, got: %q", content)
445+
}
446+
if !strings.Contains(content, "Additional info can go on the marker line") {
447+
t.Errorf("api content should include marker line content, got: %q", content)
448+
}
449+
}
450+
}
451+
452+
func TestMixedCommentFormats(t *testing.T) {
453+
tmpDir := t.TempDir()
454+
455+
// Test file that mixes all three formats
456+
testFile := filepath.Join(tmpDir, "test.go")
457+
content := `// Format 1: Marker at end
458+
// This uses the cleanest syntax.
459+
//lion:format1
460+
package test
461+
462+
//lion:format2 Format 2: Single-line with content on same line
463+
func format2example() {}
464+
465+
/*lion:format3
466+
Format 3: Block comment
467+
with multiple lines
468+
*/
469+
func format3example() {}
470+
471+
// Format 1 again: Another marker-at-end example
472+
// With multiple lines of documentation.
473+
//lion:format1
474+
func example() {}
475+
`
476+
477+
if err := os.WriteFile(testFile, []byte(content), 0644); err != nil {
478+
t.Fatalf("failed to create test file: %v", err)
479+
}
480+
481+
docs, err := Extract(tmpDir)
482+
if err != nil {
483+
t.Fatalf("Extract failed: %v", err)
484+
}
485+
486+
// Should have 3 topics
487+
if len(docs) != 3 {
488+
t.Errorf("expected 3 topics, got %d", len(docs))
489+
}
490+
491+
// Verify format1 has 2 entries
492+
if len(docs["format1"]) != 2 {
493+
t.Errorf("expected 2 format1 entries, got %d", len(docs["format1"]))
494+
}
495+
496+
// Verify format2 has 1 entry
497+
if len(docs["format2"]) != 1 {
498+
t.Errorf("expected 1 format2 entry, got %d", len(docs["format2"]))
499+
}
500+
501+
// Verify format3 has 1 entry
502+
if len(docs["format3"]) != 1 {
503+
t.Errorf("expected 1 format3 entry, got %d", len(docs["format3"]))
504+
}
505+
}

0 commit comments

Comments
 (0)