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  }