Initial fork of github.com/alonswartz/notesium into librenotes: - Source moved to internal/notesium/ (package notesium) - Thin entry point at cmd/librenotes/main.go - Module renamed to git.librete.ch/public/librenotes - main() exposed as notesium.Run() - LICENSE preserved (MIT), NOTICE added with attribution - Web assets and completion.bash co-located with embedding code to satisfy go:embed path constraints Closes #3, #34, #35. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
143 lines
2.9 KiB
Go
143 lines
2.9 KiB
Go
package notesium
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Link struct {
|
|
Filename string
|
|
Title string
|
|
LineNumber int
|
|
}
|
|
|
|
type Note struct {
|
|
Filename string
|
|
Title string
|
|
IsLabel bool
|
|
OutgoingLinks []*Link
|
|
IncomingLinks []*Link
|
|
Ctime time.Time
|
|
Mtime time.Time
|
|
Lines int
|
|
Words int
|
|
Chars int
|
|
}
|
|
|
|
var noteCache map[string]*Note
|
|
var fileRegex = regexp.MustCompile(`^[0-9a-f]{8}\.md$`)
|
|
var linkRegex = regexp.MustCompile(`\]\(([0-9a-f]{8}\.md)\)`)
|
|
|
|
func populateCache(dir string) {
|
|
if noteCache != nil {
|
|
return
|
|
}
|
|
|
|
noteCache = make(map[string]*Note)
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
log.Fatalf("could not read directory: %s\n", err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
filename := file.Name()
|
|
if !file.IsDir() && fileRegex.MatchString(filename) {
|
|
note, err := readNote(dir, filename)
|
|
if err != nil {
|
|
log.Fatalf("could not read note: %s\n", err)
|
|
}
|
|
noteCache[filename] = note
|
|
}
|
|
}
|
|
|
|
for _, note := range noteCache {
|
|
for _, link := range note.OutgoingLinks {
|
|
if targetNote, exists := noteCache[link.Filename]; exists {
|
|
link.Title = targetNote.Title
|
|
targetNote.IncomingLinks = append(targetNote.IncomingLinks, &Link{
|
|
Filename: note.Filename,
|
|
Title: note.Title,
|
|
LineNumber: link.LineNumber,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func readNote(dir string, filename string) (*Note, error) {
|
|
path := filepath.Join(dir, filename)
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not open file: %s", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get file info: %s", err)
|
|
}
|
|
mtime := info.ModTime().Truncate(time.Second)
|
|
|
|
hexTime := strings.TrimSuffix(filename, ".md")
|
|
unixTime, err := strconv.ParseInt(hexTime, 16, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctime := time.Unix(unixTime, 0)
|
|
|
|
var title string
|
|
var isLabel bool
|
|
var outgoingLinks []*Link
|
|
var lines, words, chars int
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
lineNumber := 0
|
|
for scanner.Scan() {
|
|
lineNumber++
|
|
line := scanner.Text()
|
|
if line != "" {
|
|
lines++
|
|
words += len(strings.Fields(line))
|
|
chars += len(line)
|
|
}
|
|
if title == "" {
|
|
title = strings.TrimPrefix(line, "# ")
|
|
isLabel = len(strings.Fields(title)) == 1
|
|
continue
|
|
}
|
|
matches := linkRegex.FindAllStringSubmatch(line, -1)
|
|
for _, match := range matches {
|
|
outgoingLinks = append(outgoingLinks, &Link{LineNumber: lineNumber, Filename: match[1]})
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if title == "" {
|
|
title = "untitled"
|
|
}
|
|
|
|
note := &Note{
|
|
Filename: filename,
|
|
Title: title,
|
|
IsLabel: isLabel,
|
|
OutgoingLinks: outgoingLinks,
|
|
Ctime: ctime,
|
|
Mtime: mtime,
|
|
Lines: lines,
|
|
Words: words,
|
|
Chars: chars,
|
|
}
|
|
|
|
return note, nil
|
|
}
|