feat: Implement foundation layer with domain entities and repository interfaces
- 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>
This commit is contained in:
190
internal/domain/note.go
Normal file
190
internal/domain/note.go
Normal file
@@ -0,0 +1,190 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user