- Add complete domain layer: Note, Vault, WikiLink, Tag, Frontmatter, Graph entities - Implement repository interfaces for data access abstraction - Create comprehensive configuration system with YAML and env support - Add CLI entry point with signal handling and graceful shutdown - Fix mermaid diagram syntax in design.md (array notation) - Add CLAUDE.md for development guidance 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
190 lines
3.2 KiB
Go
190 lines
3.2 KiB
Go
package domain
|
|
|
|
import (
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Note struct {
|
|
path string
|
|
content string
|
|
frontmatter *Frontmatter
|
|
outlinks []*WikiLink
|
|
tags []*Tag
|
|
createdAt time.Time
|
|
modifiedAt time.Time
|
|
}
|
|
|
|
func NewNote(path, content string) (*Note, error) {
|
|
if err := validatePath(path); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
note := &Note{
|
|
path: path,
|
|
content: content,
|
|
frontmatter: NewFrontmatter(),
|
|
outlinks: make([]*WikiLink, 0),
|
|
tags: make([]*Tag, 0),
|
|
createdAt: time.Now(),
|
|
modifiedAt: time.Now(),
|
|
}
|
|
|
|
if err := note.extractLinks(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := note.extractTags(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return note, nil
|
|
}
|
|
|
|
func (n *Note) Path() string {
|
|
return n.path
|
|
}
|
|
|
|
func (n *Note) Content() string {
|
|
return n.content
|
|
}
|
|
|
|
func (n *Note) Frontmatter() *Frontmatter {
|
|
return n.frontmatter
|
|
}
|
|
|
|
func (n *Note) Outlinks() []*WikiLink {
|
|
return n.outlinks
|
|
}
|
|
|
|
func (n *Note) Tags() []*Tag {
|
|
return n.tags
|
|
}
|
|
|
|
func (n *Note) CreatedAt() time.Time {
|
|
return n.createdAt
|
|
}
|
|
|
|
func (n *Note) ModifiedAt() time.Time {
|
|
return n.modifiedAt
|
|
}
|
|
|
|
func (n *Note) UpdateContent(content string) error {
|
|
n.content = content
|
|
n.modifiedAt = time.Now()
|
|
|
|
n.outlinks = make([]*WikiLink, 0)
|
|
n.tags = make([]*Tag, 0)
|
|
|
|
if err := n.extractLinks(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return n.extractTags()
|
|
}
|
|
|
|
func (n *Note) SetFrontmatter(fm *Frontmatter) {
|
|
n.frontmatter = fm
|
|
n.modifiedAt = time.Now()
|
|
}
|
|
|
|
func (n *Note) AddTag(tag *Tag) error {
|
|
for _, existingTag := range n.tags {
|
|
if existingTag.Name() == tag.Name() {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
n.tags = append(n.tags, tag)
|
|
n.modifiedAt = time.Now()
|
|
return nil
|
|
}
|
|
|
|
func (n *Note) RemoveTag(tagName string) error {
|
|
for i, tag := range n.tags {
|
|
if tag.Name() == tagName {
|
|
n.tags = append(n.tags[:i], n.tags[i+1:]...)
|
|
n.modifiedAt = time.Now()
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (n *Note) HasTag(tagName string) bool {
|
|
for _, tag := range n.tags {
|
|
if tag.Name() == tagName {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (n *Note) ExtractLinks() []*WikiLink {
|
|
return n.outlinks
|
|
}
|
|
|
|
func (n *Note) WordCount() int {
|
|
words := strings.Fields(n.content)
|
|
return len(words)
|
|
}
|
|
|
|
func (n *Note) CharacterCount() int {
|
|
return len(n.content)
|
|
}
|
|
|
|
func (n *Note) extractLinks() error {
|
|
linkRegex := regexp.MustCompile(`\[\[([^\]]+)\]\]`)
|
|
matches := linkRegex.FindAllStringSubmatch(n.content, -1)
|
|
|
|
for _, match := range matches {
|
|
if len(match) >= 2 {
|
|
link, err := ParseWikiLink(match[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
n.outlinks = append(n.outlinks, link)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Note) extractTags() error {
|
|
tagRegex := regexp.MustCompile(`#([a-zA-Z0-9_/]+)`)
|
|
matches := tagRegex.FindAllStringSubmatch(n.content, -1)
|
|
|
|
for _, match := range matches {
|
|
if len(match) >= 2 {
|
|
tag, err := NewTag(match[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
n.AddTag(tag)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatePath(path string) error {
|
|
if path == "" {
|
|
return ErrInvalidPath
|
|
}
|
|
|
|
if !strings.HasSuffix(path, ".md") {
|
|
return ErrInvalidPath
|
|
}
|
|
|
|
if strings.Contains(path, "..") {
|
|
return ErrInvalidPath
|
|
}
|
|
|
|
if !filepath.IsAbs(path) && strings.HasPrefix(path, "/") {
|
|
return ErrInvalidPath
|
|
}
|
|
|
|
return nil
|
|
} |