Update frontmatter on title/keywords/signature change

This commit is contained in:
Levi Neely 2026-03-21 23:16:36 +01:00
parent 58911d5753
commit 5d6a94ef42
2 changed files with 86 additions and 8 deletions

View File

@ -650,6 +650,9 @@ func (s *server) dispatchWrite(f *fid, tag uint16) *plan9.Fcall {
if s.callbacks.OnRename != nil { if s.callbacks.OnRename != nil {
s.callbacks.OnRename(noteID) s.callbacks.OnRename(noteID)
} }
if err := s.updateFrontMatter(note); err != nil {
return errorFcall(fc, err.Error())
}
if err := s.renameNote(note); err != nil { if err := s.renameNote(note); err != nil {
return errorFcall(fc, err.Error()) return errorFcall(fc, err.Error())
} }
@ -669,6 +672,9 @@ func (s *server) dispatchWrite(f *fid, tag uint16) *plan9.Fcall {
if s.callbacks.OnRename != nil { if s.callbacks.OnRename != nil {
s.callbacks.OnRename(noteID) s.callbacks.OnRename(noteID)
} }
if err := s.updateFrontMatter(note); err != nil {
return errorFcall(fc, err.Error())
}
if err := s.renameNote(note); err != nil { if err := s.renameNote(note); err != nil {
return errorFcall(fc, err.Error()) return errorFcall(fc, err.Error())
} }
@ -680,6 +686,9 @@ func (s *server) dispatchWrite(f *fid, tag uint16) *plan9.Fcall {
if s.callbacks.OnRename != nil { if s.callbacks.OnRename != nil {
s.callbacks.OnRename(noteID) s.callbacks.OnRename(noteID)
} }
if err := s.updateFrontMatter(note); err != nil {
return errorFcall(fc, err.Error())
}
if err := s.renameNote(note); err != nil { if err := s.renameNote(note); err != nil {
return errorFcall(fc, err.Error()) return errorFcall(fc, err.Error())
} }
@ -1008,25 +1017,53 @@ func (s *server) getFileContent(path string) string {
case "backlinks": case "backlinks":
return s.getBacklinks(noteID) return s.getBacklinks(noteID)
case "body": case "body":
return s.readBody(note.Path) return s.readBody(note)
} }
return "" return ""
} }
// readBody reads the file content at path // readBody reads the file content, normalizing frontmatter tags if needed.
func (s *server) readBody(path string) string { // Malformed tags are fixed in place and the file is renamed if necessary.
if path == "" { func (s *server) readBody(note *metadata.Metadata) string {
if note.Path == "" {
return "" return ""
} }
data, err := os.ReadFile(path) data, err := os.ReadFile(note.Path)
if err != nil { if err != nil {
return "" return ""
} }
ext := filepath.Ext(note.Path)
fm, fileType, _ := frontmatter.Unmarshal(data, ext)
if fm != nil && fm.Title != "" {
note.Title = fm.Title
note.Tags = fm.Tags
note.Signature = fm.Signature
normalized := frontmatter.NormalizeTags(data, fm.Tags, fileType)
if string(normalized) != string(data) {
if err := os.WriteFile(note.Path, normalized, 0644); err == nil {
data = normalized
}
}
// Rename if tags changed the filename
s.renameNote(note) //nolint:errcheck
}
return string(data) return string(data)
} }
// renameNote renames the physical file to match current note metadata and updates note.Path. // renameNote renames the physical file to match current note metadata and updates note.Path.
// It is a no-op when the filename is already correct. // It is a no-op when the filename is already correct.
func (s *server) updateFrontMatter(note *metadata.Metadata) error {
if note.Path == "" || !disk.SupportsFrontMatter(note.Path) {
return nil
}
_, fileType, err := disk.ExtractFrontMatter(note.Path)
if err != nil {
return err
}
fm := metadata.NewFrontMatter(note.Title, note.Signature, note.Tags, note.Identifier)
return disk.UpdateFrontMatter(note.Path, fm, fileType)
}
func (s *server) renameNote(note *metadata.Metadata) error { func (s *server) renameNote(note *metadata.Metadata) error {
if note.Path == "" { if note.Path == "" {
return nil return nil
@ -1045,7 +1082,8 @@ func (s *server) renameNote(note *metadata.Metadata) error {
return nil return nil
} }
// writeBody writes content to file and syncs frontmatter to metadata // writeBody writes content to file and syncs frontmatter to metadata.
// If frontmatter tags are malformed, they are normalized and written back.
func (s *server) writeBody(path, body string, note *metadata.Metadata) error { func (s *server) writeBody(path, body string, note *metadata.Metadata) error {
if path == "" { if path == "" {
return fmt.Errorf("no path") return fmt.Errorf("no path")
@ -1055,11 +1093,18 @@ func (s *server) writeBody(path, body string, note *metadata.Metadata) error {
} }
// Parse frontmatter and sync to metadata // Parse frontmatter and sync to metadata
ext := filepath.Ext(path) ext := filepath.Ext(path)
fm, _, _ := frontmatter.Unmarshal([]byte(body), ext) fm, fileType, _ := frontmatter.Unmarshal([]byte(body), ext)
if fm != nil && fm.Title != "" { if fm != nil && fm.Title != "" {
note.Title = fm.Title note.Title = fm.Title
note.Tags = fm.Tags note.Tags = fm.Tags
note.Signature = fm.Signature note.Signature = fm.Signature
// Write normalized tags back into the file
normalized := frontmatter.NormalizeTags([]byte(body), fm.Tags, fileType)
if string(normalized) != body {
if err := os.WriteFile(path, normalized, 0644); err != nil {
return err
}
}
} }
return s.renameNote(note) return s.renameNote(note)
} }

View File

@ -79,7 +79,13 @@ func Marshal(fm *metadata.FrontMatter, fileType metadata.FileType) []byte {
return []byte(content) return []byte(content)
} }
var tagSplitRe = regexp.MustCompile(`[\s,;:]+`) var (
tagSplitRe = regexp.MustCompile(`[\s,;:]+`)
orgFiletagsRe = regexp.MustCompile(`(?m)^(#\+filetags:[ \t]*).*$`)
mdYamlTagsRe = regexp.MustCompile(`(?m)^(tags:[ \t]*).*$`)
mdTomlTagsRe = regexp.MustCompile(`(?m)^(tags[ \t]*=[ \t]*).*$`)
txtTagsRe = regexp.MustCompile(`(?m)^(tags:[ \t]*).*$`)
)
var tagStripRe = regexp.MustCompile(`[^a-z0-9]`) var tagStripRe = regexp.MustCompile(`[^a-z0-9]`)
// parseTags parses a raw tag string into normalized tags. // parseTags parses a raw tag string into normalized tags.
@ -101,6 +107,33 @@ func parseTags(s string) []string {
return tags return tags
} }
// NormalizeTags replaces the tags field in content with the normalized,
// correctly formatted version for the given file type.
// Returns the updated content. If no tags field is found, content is unchanged.
func NormalizeTags(content []byte, tags []string, fileType metadata.FileType) []byte {
formatted := formatTags(tags, fileType)
text := string(content)
switch fileType {
case metadata.FileTypeOrg:
text = orgFiletagsRe.ReplaceAllString(text, "${1}"+formatted)
case metadata.FileTypeMdYaml:
// Only replace within the --- block
yamlRe := regexp.MustCompile(`(?ms)^---\n(.*?)\n---`)
text = yamlRe.ReplaceAllStringFunc(text, func(block string) string {
return mdYamlTagsRe.ReplaceAllString(block, "${1}"+formatted)
})
case metadata.FileTypeMdToml:
// Only replace within the +++ block
tomlRe := regexp.MustCompile(`(?ms)^\+\+\+\n(.*?)\n\+\+\+`)
text = tomlRe.ReplaceAllStringFunc(text, func(block string) string {
return mdTomlTagsRe.ReplaceAllString(block, "${1}"+formatted)
})
case metadata.FileTypeTxt:
text = txtTagsRe.ReplaceAllString(text, "${1}"+formatted)
}
return []byte(text)
}
// Unmarshal extracts front matter from file content. // Unmarshal extracts front matter from file content.
// ext should be the file extension (e.g., ".md", ".org", ".txt"). // ext should be the file extension (e.g., ".md", ".org", ".txt").
// Returns the parsed frontmatter and the detected FileType. // Returns the parsed frontmatter and the detected FileType.