52160345bf
Complete implementation of note management CLI with all core features: Commands: - add: Create new notes in $EDITOR with auto-generated filenames - list: Display all notes with titles, paths, and tags - search: Full-text search via ripgrep, tag-based filtering - tags: List all tags with occurrence counts - edit: Fuzzy search and edit notes by title - rm: Move notes to trash with confirmation prompt Features: - Automatic depository structure initialization (.jade/trash/) - Configurable tag prefix (default '+') - Parse title from first # heading (filename fallback) - Extract tags anywhere in content - Parse both [[wiki-links]] and [markdown](links) - Trash system with timestamps to prevent conflicts Technical: - Global config at ~/.config/jade/config.yml - Per-depository settings support - Ripgrep integration for fast search - $EDITOR integration for note editing - Comprehensive README with usage examples
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Note struct {
|
|
Path string
|
|
Title string
|
|
Tags []string
|
|
Links []string
|
|
Created time.Time
|
|
Modified time.Time
|
|
}
|
|
|
|
// LoadNote reads a note from the filesystem and parses its content
|
|
func LoadNote(depoPath, notePath string, tagPrefix string) (*Note, error) {
|
|
fullPath := filepath.Join(depoPath, notePath)
|
|
|
|
// Get file info
|
|
info, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to stat note: %w", err)
|
|
}
|
|
|
|
// Read file content
|
|
content, err := os.ReadFile(fullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read note: %w", err)
|
|
}
|
|
|
|
note := &Note{
|
|
Path: notePath,
|
|
Modified: info.ModTime(),
|
|
Created: info.ModTime(), // We'll use modified time as created for now
|
|
}
|
|
|
|
// Parse title (first # heading)
|
|
note.Title = parseTitle(string(content))
|
|
if note.Title == "" {
|
|
// Use filename without extension as fallback
|
|
note.Title = strings.TrimSuffix(filepath.Base(notePath), filepath.Ext(notePath))
|
|
}
|
|
|
|
// Parse tags
|
|
note.Tags = parseTags(string(content), tagPrefix)
|
|
|
|
// Parse links
|
|
note.Links = parseLinks(string(content))
|
|
|
|
return note, nil
|
|
}
|
|
|
|
// parseTitle extracts the first # heading from markdown content
|
|
func parseTitle(content string) string {
|
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if strings.HasPrefix(line, "# ") {
|
|
return strings.TrimSpace(strings.TrimPrefix(line, "#"))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// parseTags extracts tags from markdown content based on the tag prefix
|
|
func parseTags(content string, tagPrefix string) []string {
|
|
// Escape special regex characters in tagPrefix
|
|
escapedPrefix := regexp.QuoteMeta(tagPrefix)
|
|
|
|
// Match tagPrefix followed by word characters
|
|
pattern := escapedPrefix + `\w+`
|
|
re := regexp.MustCompile(pattern)
|
|
|
|
matches := re.FindAllString(content, -1)
|
|
|
|
// Remove duplicates
|
|
tagSet := make(map[string]bool)
|
|
var tags []string
|
|
for _, tag := range matches {
|
|
if !tagSet[tag] {
|
|
tagSet[tag] = true
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
// parseLinks extracts both [[wiki-style]] and [markdown](links) from content
|
|
func parseLinks(content string) []string {
|
|
var links []string
|
|
linkSet := make(map[string]bool)
|
|
|
|
// Match [[wiki-style links]]
|
|
wikiRe := regexp.MustCompile(`\[\[([^\]]+)\]\]`)
|
|
wikiMatches := wikiRe.FindAllStringSubmatch(content, -1)
|
|
for _, match := range wikiMatches {
|
|
if len(match) > 1 {
|
|
link := match[1]
|
|
if !linkSet[link] {
|
|
linkSet[link] = true
|
|
links = append(links, link)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Match [text](markdown-links)
|
|
mdRe := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
|
mdMatches := mdRe.FindAllStringSubmatch(content, -1)
|
|
for _, match := range mdMatches {
|
|
if len(match) > 2 {
|
|
link := match[2]
|
|
// Only include .md files (note links, not external URLs)
|
|
if strings.HasSuffix(link, ".md") && !strings.HasPrefix(link, "http") {
|
|
if !linkSet[link] {
|
|
linkSet[link] = true
|
|
links = append(links, link)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return links
|
|
}
|
|
|
|
// TitleToFilename converts a note title to a valid filename
|
|
func TitleToFilename(title string) string {
|
|
// Convert to lowercase
|
|
filename := strings.ToLower(title)
|
|
|
|
// Replace spaces with hyphens
|
|
filename = strings.ReplaceAll(filename, " ", "-")
|
|
|
|
// Remove or replace special characters
|
|
re := regexp.MustCompile(`[^a-z0-9\-_]`)
|
|
filename = re.ReplaceAllString(filename, "")
|
|
|
|
// Add .md extension
|
|
return filename + ".md"
|
|
}
|