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" }