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 }