Skip to content

Commit c81be97

Browse files
JordanCoinclaude
andcommitted
Add --refs flag to show markdown cross-references
Like codemap's --deps, this shows how docs link to each other: - Finds all [text](file.md) links between markdown files - Shows HUBS (most referenced docs) - Displays reference flow graph 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b7b2665 commit c81be97

4 files changed

Lines changed: 177 additions & 2 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ docmap README.md # Single file deep dive
6666
docmap docs/ # Specific folder
6767
docmap README.md --section "API" # Filter to section
6868
docmap README.md --expand "API" # Show section content
69+
docmap . --refs # Show cross-references between docs
6970
```
7071

7172
## Output
@@ -122,6 +123,33 @@ See the actual content:
122123
docmap docs/API.md --expand "Authentication"
123124
```
124125

126+
### References Mode
127+
128+
See how docs link to each other (like `codemap --deps`):
129+
130+
```bash
131+
docmap . --refs
132+
```
133+
134+
```
135+
╭─────────────────────── project/ ───────────────────────╮
136+
│ References: 53 links between docs │
137+
╰────────────────────────────────────────────────────────╯
138+
139+
HUBS: docs/architecture.md (5←), docs/api.md (3←)
140+
141+
Reference Flow:
142+
143+
README.md
144+
├──▶ docs/architecture.md
145+
├──▶ docs/api.md
146+
└──▶ CHANGELOG.md
147+
148+
docs/architecture.md
149+
├──▶ docs/components.md
150+
└──▶ docs/data-flow.md
151+
```
152+
125153
## Why docmap?
126154

127155
| Before | After |

main.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func main() {
1414
if len(os.Args) < 2 {
15-
fmt.Println("Usage: docmap <file.md|dir> [--section <name>] [--expand <name>]")
15+
fmt.Println("Usage: docmap <file.md|dir> [--section <name>] [--expand <name>] [--refs]")
1616
os.Exit(1)
1717
}
1818

@@ -21,6 +21,7 @@ func main() {
2121
// Parse flags
2222
var sectionFilter string
2323
var expandSection string
24+
var showRefs bool
2425
for i := 2; i < len(os.Args); i++ {
2526
switch os.Args[i] {
2627
case "--section", "-s":
@@ -33,6 +34,8 @@ func main() {
3334
expandSection = os.Args[i+1]
3435
i++
3536
}
37+
case "--refs", "-r":
38+
showRefs = true
3639
}
3740
}
3841

@@ -50,7 +53,11 @@ func main() {
5053
fmt.Println("No markdown files found")
5154
os.Exit(1)
5255
}
53-
render.MultiTree(docs, target)
56+
if showRefs {
57+
render.RefsTree(docs, target)
58+
} else {
59+
render.MultiTree(docs, target)
60+
}
5461
} else {
5562
// Single file mode
5663
content, err := os.ReadFile(target)

parser/markdown.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ type Document struct {
1010
Filename string
1111
TotalTokens int
1212
Sections []*Section
13+
References []Reference // Links to other .md files
14+
}
15+
16+
// Reference represents a link to another markdown file
17+
type Reference struct {
18+
Text string // Link text
19+
Target string // Target file path
20+
Line int // Line number where reference appears
1321
}
1422

1523
// Section represents a heading and its content
@@ -36,12 +44,28 @@ func Parse(content string) *Document {
3644
doc := &Document{}
3745

3846
headingRe := regexp.MustCompile(`^(#{1,6})\s+(.+)$`)
47+
// Match markdown links to .md files: [text](path.md) or [text](path.md#anchor)
48+
linkRe := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+\.md(?:#[^)]*)?)\)`)
3949

4050
var allSections []*Section
4151
var currentSection *Section
4252
var contentBuilder strings.Builder
4353

4454
for i, line := range lines {
55+
// Extract markdown links to .md files
56+
for _, match := range linkRe.FindAllStringSubmatch(line, -1) {
57+
target := match[2]
58+
// Remove anchor if present
59+
if idx := strings.Index(target, "#"); idx != -1 {
60+
target = target[:idx]
61+
}
62+
doc.References = append(doc.References, Reference{
63+
Text: match[1],
64+
Target: target,
65+
Line: i + 1,
66+
})
67+
}
68+
4569
if matches := headingRe.FindStringSubmatch(line); matches != nil {
4670
// Save previous section's content
4771
if currentSection != nil {

render/tree.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,3 +261,119 @@ func getTopSections(sections []*parser.Section, maxDepth int) []*parser.Section
261261
}
262262
return result
263263
}
264+
265+
// RefsTree renders document references (links to other .md files)
266+
func RefsTree(docs []*parser.Document, dirName string) {
267+
// Build reference graph
268+
type RefInfo struct {
269+
From string
270+
To string
271+
Text string
272+
Line int
273+
}
274+
275+
var allRefs []RefInfo
276+
fileRefs := make(map[string][]string) // file -> files it references
277+
fileRefBy := make(map[string][]string) // file -> files that reference it
278+
279+
for _, doc := range docs {
280+
for _, ref := range doc.References {
281+
allRefs = append(allRefs, RefInfo{
282+
From: doc.Filename,
283+
To: ref.Target,
284+
Text: ref.Text,
285+
Line: ref.Line,
286+
})
287+
fileRefs[doc.Filename] = append(fileRefs[doc.Filename], ref.Target)
288+
fileRefBy[ref.Target] = append(fileRefBy[ref.Target], doc.Filename)
289+
}
290+
}
291+
292+
if len(allRefs) == 0 {
293+
fmt.Println("No markdown cross-references found")
294+
return
295+
}
296+
297+
// Header
298+
innerWidth := 60
299+
titleLine := fmt.Sprintf(" %s/ ", dirName)
300+
padding := innerWidth - len(titleLine)
301+
leftPad := padding / 2
302+
rightPad := padding - leftPad
303+
fmt.Printf("╭%s%s%s╮\n", strings.Repeat("─", leftPad), titleLine, strings.Repeat("─", rightPad))
304+
305+
info := fmt.Sprintf("References: %d links between docs", len(allRefs))
306+
fmt.Printf("│ %-*s │\n", innerWidth-2, centerText(info, innerWidth-2))
307+
fmt.Printf("╰%s╯\n", strings.Repeat("─", innerWidth))
308+
fmt.Println()
309+
310+
// Find hubs (most referenced files)
311+
type hub struct {
312+
file string
313+
count int
314+
}
315+
var hubs []hub
316+
for file, refs := range fileRefBy {
317+
if len(refs) >= 2 {
318+
hubs = append(hubs, hub{file, len(refs)})
319+
}
320+
}
321+
322+
if len(hubs) > 0 {
323+
// Sort by count descending
324+
for i := 0; i < len(hubs); i++ {
325+
for j := i + 1; j < len(hubs); j++ {
326+
if hubs[j].count > hubs[i].count {
327+
hubs[i], hubs[j] = hubs[j], hubs[i]
328+
}
329+
}
330+
}
331+
332+
fmt.Printf("%sHUBS:%s ", bold, reset)
333+
var hubStrs []string
334+
for _, h := range hubs {
335+
if len(hubStrs) >= 5 {
336+
break
337+
}
338+
hubStrs = append(hubStrs, fmt.Sprintf("%s%s%s (%d←)", green, h.file, reset, h.count))
339+
}
340+
fmt.Println(strings.Join(hubStrs, ", "))
341+
fmt.Println()
342+
}
343+
344+
// Show reference flow by file
345+
fmt.Printf("%sReference Flow:%s\n", bold+cyan, reset)
346+
fmt.Println()
347+
348+
// Group by source file
349+
printed := make(map[string]bool)
350+
for _, doc := range docs {
351+
if len(doc.References) == 0 {
352+
continue
353+
}
354+
if printed[doc.Filename] {
355+
continue
356+
}
357+
printed[doc.Filename] = true
358+
359+
// Dedupe targets
360+
seen := make(map[string]bool)
361+
var targets []string
362+
for _, ref := range doc.References {
363+
if !seen[ref.Target] {
364+
targets = append(targets, ref.Target)
365+
seen[ref.Target] = true
366+
}
367+
}
368+
369+
fmt.Printf(" %s%s%s\n", bold, doc.Filename, reset)
370+
for i, target := range targets {
371+
connector := "├──▶ "
372+
if i == len(targets)-1 {
373+
connector = "└──▶ "
374+
}
375+
fmt.Printf(" %s%s%s%s\n", dim, connector, reset, target)
376+
}
377+
fmt.Println()
378+
}
379+
}

0 commit comments

Comments
 (0)