github.com/jacekolszak/noteo@v0.5.0/note/note.go (about) 1 package note 2 3 import ( 4 "io/ioutil" 5 "os" 6 "path/filepath" 7 "regexp" 8 "strings" 9 "sync" 10 "time" 11 12 "github.com/jacekolszak/noteo/tag" 13 ) 14 15 type Note struct { 16 path string 17 modified func() (time.Time, error) 18 originalContent *originalContent 19 frontMatter *frontMatter 20 body *body 21 } 22 23 func New(path string) *Note { 24 return newWithModifiedFunc(path, readModifiedFunc(path)) 25 } 26 27 func readModifiedFunc(path string) func() (time.Time, error) { 28 return func() (modTime time.Time, err error) { 29 var stat os.FileInfo 30 stat, err = os.Stat(path) 31 if err != nil { 32 return 33 } 34 modTime = stat.ModTime() 35 return 36 } 37 } 38 39 func newWithModifiedFunc(path string, modified func() (time.Time, error)) *Note { 40 original := &originalContent{path: path} 41 return &Note{ 42 path: path, 43 modified: modified, 44 originalContent: original, 45 frontMatter: &frontMatter{ 46 path: path, 47 original: original.FrontMatter, 48 mapSlice: mapSlice{}, 49 }, 50 body: &body{ 51 original: original.Body, 52 }, 53 } 54 } 55 56 func NewWithModified(path string, modified time.Time) *Note { 57 return newWithModifiedFunc(path, func() (time.Time, error) { 58 return modified, nil 59 }) 60 } 61 62 func (n *Note) Path() string { 63 return n.path 64 } 65 66 func (n *Note) Modified() (time.Time, error) { 67 return n.modified() 68 } 69 70 func (n *Note) Created() (time.Time, error) { 71 return n.frontMatter.Created() 72 } 73 74 func (n *Note) Tags() ([]tag.Tag, error) { 75 return n.frontMatter.Tags() 76 } 77 78 func (n *Note) Body() (string, error) { 79 return n.body.text() 80 } 81 82 func (n *Note) SetTag(newTag tag.Tag) error { 83 return n.frontMatter.setTag(newTag) 84 } 85 86 func (n *Note) RemoveTag(newTag tag.Tag) error { 87 return n.frontMatter.removeTag(newTag) 88 } 89 90 func (n *Note) RemoveTagRegex(regex *regexp.Regexp) error { 91 return n.frontMatter.removeTagRegex(regex) 92 } 93 94 func (n *Note) UpdateLink(from, to string) error { 95 body, err := n.body.text() 96 if err != nil { 97 return err 98 } 99 newBody, err := replaceLinks{notePath: n.path, from: from, to: to}.run(body) 100 if err != nil { 101 return err 102 } 103 n.body.setText(newBody) 104 return nil 105 } 106 107 type replaceLinks struct { 108 notePath string 109 from, to string 110 } 111 112 func (u replaceLinks) run(body string) (newBody string, returnedError error) { 113 relativeFrom, err := u.relativePath(u.from) 114 if err != nil { 115 return "", err 116 } 117 markdownLinkRegexp := regexp.MustCompile(`(\[[^][]+])\(([^()]+)\)`) // TODO does not take into account code fences 118 newBody = markdownLinkRegexp.ReplaceAllStringFunc(body, func(s string) string { 119 linkPath := markdownLinkRegexp.FindStringSubmatch(s)[2] 120 relativeLinkPath, err := u.relativePath(linkPath) 121 if err != nil { 122 returnedError = err 123 return s 124 } 125 if relativeFrom == relativeLinkPath { 126 return markdownLinkRegexp.ReplaceAllString(s, `$1(`+u.to+`)`) 127 } 128 if isAncestorPath(relativeFrom, relativeLinkPath) { 129 newTo := u.to + strings.TrimPrefix(relativeLinkPath, relativeFrom) 130 newTo = filepath.ToSlash(newTo) // always use slashes, even on Windows 131 return markdownLinkRegexp.ReplaceAllString(s, `$1(`+newTo+`)`) 132 } 133 return s 134 }) 135 return 136 } 137 138 // Returns relative path to note path 139 func (u replaceLinks) relativePath(p string) (string, error) { 140 dir := filepath.Dir(u.notePath) 141 relativePath := p 142 if !filepath.IsAbs(relativePath) { 143 relativePath = filepath.Join(dir, p) 144 } 145 return filepath.Rel(dir, relativePath) 146 } 147 148 func isAncestorPath(relativeAncestorPath string, relativeDescendantPath string) bool { 149 rel, err := filepath.Rel(relativeAncestorPath, relativeDescendantPath) 150 if err != nil { 151 return false 152 } 153 return filepath.Dir(rel) == "." 154 } 155 156 // Save returns true if file was modified. 157 func (n *Note) Save() (bool, error) { 158 frontMatter, err := n.frontMatter.marshal() 159 if err != nil { 160 return false, err 161 } 162 text, err := n.Body() 163 if err != nil { 164 return false, err 165 } 166 newContent := frontMatter + text 167 original, err := n.originalContent.Full() 168 if err != nil { 169 return false, err 170 } 171 if original == newContent { 172 return false, nil 173 } 174 newBytes := []byte(newContent) 175 if err := ioutil.WriteFile(n.path, newBytes, 0664); err != nil { 176 return false, err 177 } 178 return true, nil 179 } 180 181 type body struct { 182 body *string 183 original func() (string, error) 184 mutex sync.Mutex 185 } 186 187 func (t *body) text() (string, error) { 188 t.mutex.Lock() 189 defer t.mutex.Unlock() 190 191 if t.body == nil { 192 body, err := t.original() 193 if err != nil { 194 return "", err 195 } 196 t.body = &body 197 } 198 return *t.body, nil 199 } 200 201 func (t *body) setText(body string) { 202 t.mutex.Lock() 203 defer t.mutex.Unlock() 204 205 t.body = &body 206 }