Skip to content

Commit

Permalink
feat: collect backlinks
Browse files Browse the repository at this point in the history
Adds support for collecting backlinks. For simplification we now assume
that all notes have unique base file names.
  • Loading branch information
luissimas committed Jul 2, 2024
1 parent 110931f commit 9b1c91f
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 30 deletions.
38 changes: 28 additions & 10 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ func (c *Collector) CollectMetrics(root fs.FS, collectionTime time.Time) error {

// collectMetrics collects all metrics from a Zettelkasten rooted in `root`.
func (c *Collector) collectMetrics(root fs.FS) (metrics.Metrics, error) {
var noteCount uint
var linkCount uint
var wordCount uint
notes := make(map[string]metrics.NoteMetrics)
noteMetrics := make(map[string]metrics.NoteMetrics)

err := fs.WalkDir(root, ".", func(path string, dir fs.DirEntry, err error) error {
if err != nil {
Expand Down Expand Up @@ -86,11 +83,7 @@ func (c *Collector) collectMetrics(root fs.FS) (metrics.Metrics, error) {
slog.Error("Error reading file", slog.Any("error", err), slog.String("path", path))
return nil
}
metrics := CollectNoteMetrics(content)
notes[path] = metrics
linkCount += metrics.LinkCount
wordCount += metrics.WordCount
noteCount += 1
noteMetrics[nameFromFilename(path)] = CollectNoteMetrics(content)

slog.Debug("collected metrics from file", slog.String("path", path), slog.Any("d", dir), slog.Any("err", err))

Expand All @@ -102,5 +95,30 @@ func (c *Collector) collectMetrics(root fs.FS) (metrics.Metrics, error) {
return metrics.Metrics{}, err
}

return metrics.Metrics{NoteCount: noteCount, LinkCount: linkCount, WordCount: wordCount, Notes: notes}, nil
zettelkastenMetrics := aggregateMetrics(noteMetrics)
return zettelkastenMetrics, nil
}

// aggregateMetrics aggregates all individual note metrics into metrics in the context of a full Zettelkasten.
func aggregateMetrics(noteMetrics map[string]metrics.NoteMetrics) metrics.Metrics {
zettelkastenMetrics := metrics.Metrics{
NoteCount: 0,
LinkCount: 0,
WordCount: 0,
Notes: make(map[string]metrics.NoteMetrics),
}

for name, metric := range noteMetrics {
// Aggregate totals
zettelkastenMetrics.NoteCount += 1
zettelkastenMetrics.LinkCount += metric.LinkCount
zettelkastenMetrics.WordCount += metric.WordCount
// Collect backlinks
for _, n := range noteMetrics {
metric.BacklinkCount += n.Links[name]
}
zettelkastenMetrics.Notes[name] = metric
}

return zettelkastenMetrics
}
22 changes: 11 additions & 11 deletions internal/collector/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ created-at: "2024-05-29"
Testing a note with no links. But there's a [markdown link](./dir1/two.md)
[[./dir1/two.md]]
[[two]]
![[./image.png]]
`)},
Expand All @@ -42,7 +42,7 @@ Links to [[one]] but also to [[two|two with an alias]]
---
created-at: "2024-05-29"
---
Link to [one](./one.md) and also a full link [[./dir1/dir2/three]] and a [[./dir1/two.md|full link with .md]]
Link to [one](one.md) and also a full link [[./dir1/dir2/three]] and a [[dir1/two.md|full link with .md]]
`)},
"ignoredir/foo": {Data: []byte("Foo contents")},
"ignoredir/bar": {Data: []byte("Bar contents")},
Expand All @@ -55,26 +55,26 @@ Link to [one](./one.md) and also a full link [[./dir1/dir2/three]] and a [[./dir
LinkCount: 8,
WordCount: 43,
Notes: map[string]metrics.NoteMetrics{
"zettel/one.md": {
Links: map[string]uint{"./dir1/two.md": 2},
"one": {
Links: map[string]uint{"two": 2},
LinkCount: 2,
WordCount: 13,
BacklinkCount: 0,
BacklinkCount: 3,
},
"zettel/dir1/two.md": {
"two": {
Links: map[string]uint{"one": 1},
LinkCount: 1,
WordCount: 5,
BacklinkCount: 0,
BacklinkCount: 4,
},
"zettel/dir1/dir2/three.md": {
"three": {
Links: map[string]uint{"one": 1, "two": 1},
LinkCount: 2,
WordCount: 10,
BacklinkCount: 0,
BacklinkCount: 1,
},
"zettel/four.md": {
Links: map[string]uint{"./one.md": 1, "./dir1/dir2/three": 1, "./dir1/two.md": 1},
"four": {
Links: map[string]uint{"one": 1, "three": 1, "two": 1},
LinkCount: 3,
WordCount: 15,
BacklinkCount: 0,
Expand Down
12 changes: 9 additions & 3 deletions internal/collector/note.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ func CollectNoteMetrics(content []byte) metrics.NoteMetrics {
return ast.WalkContinue, nil
}

v, ok := noteMetrics.Links[linkTarget]
targetName := nameFromFilename(linkTarget)
v, ok := noteMetrics.Links[targetName]
if !ok {
noteMetrics.Links[linkTarget] = 0
noteMetrics.Links[targetName] = 0
}
noteMetrics.Links[linkTarget] = v + 1
noteMetrics.Links[targetName] = v + 1
return ast.WalkContinue, nil
})
if err != nil {
Expand Down Expand Up @@ -89,3 +90,8 @@ func isNoteTarget(target string) bool {
isNoteTarget := extension == "" || extension == ".md"
return isNoteTarget
}

// nameFromFilename extracts the base note name from a full path.
func nameFromFilename(filename string) string {
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
}
12 changes: 6 additions & 6 deletions internal/collector/note_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,17 @@ another [[link]]`,
name: "markdown link",
content: "[Link](target.md)",
expected: metrics.NoteMetrics{
Links: map[string]uint{"target.md": 1},
Links: map[string]uint{"target": 1},
LinkCount: 1,
WordCount: 1,
BacklinkCount: 0,
},
},
{
name: "repeated links",
content: "[[target.md|link]] [link](target.md) [[link]]",
content: "[[target|link]] [link](target.md) [[link]]",
expected: metrics.NoteMetrics{
Links: map[string]uint{"target.md": 2, "link": 1},
Links: map[string]uint{"target": 2, "link": 1},
LinkCount: 3,
WordCount: 3,
BacklinkCount: 0,
Expand All @@ -60,7 +60,7 @@ another [[link]]`,
name: "ignore links to non markdown files",
content: "![[note.md]] [[test.pdf]] ![[target.png]] ![](another.jpeg) [[link]] [](link)",
expected: metrics.NoteMetrics{
Links: map[string]uint{"link": 2, "note.md": 1},
Links: map[string]uint{"link": 2, "note": 1},
LinkCount: 3,
WordCount: 4,
BacklinkCount: 0,
Expand All @@ -70,7 +70,7 @@ another [[link]]`,
name: "ignore http links",
content: "[[one]] [this is an http link](https://go.dev/) [[not/an/http/link]]",
expected: metrics.NoteMetrics{
Links: map[string]uint{"one": 1, "not/an/http/link": 1},
Links: map[string]uint{"one": 1, "link": 1},
LinkCount: 2,
WordCount: 7,
BacklinkCount: 0,
Expand All @@ -95,7 +95,7 @@ Another list:
1. First
2. Second [link](link-ordered.md)`,
expected: metrics.NoteMetrics{
Links: map[string]uint{"target.md": 1, "linked": 1, "another": 1, "yet-another.md": 1, "link-unordered.md": 1, "link-ordered.md": 1},
Links: map[string]uint{"target": 1, "linked": 1, "another": 1, "yet-another": 1, "link-unordered": 1, "link-ordered": 1},
LinkCount: 6,
WordCount: 23,
BacklinkCount: 0,
Expand Down

0 comments on commit 9b1c91f

Please sign in to comment.