denotesrv/pkg/encoding/frontmatter/frontmatter.go

173 lines
5.1 KiB
Go

package frontmatter
import (
"denotesrv/pkg/metadata"
"fmt"
"regexp"
"strings"
"time"
)
// Templates for denote frontmatter (org, md, txt)
var templates = map[metadata.FileType]string{
metadata.FileTypeOrg: `#+title: %s
#+date: %s
#+filetags: %s
#+identifier: %s
#+signature: %s
`,
metadata.FileTypeMdYaml: `---
title: %s
date: %s
tags: %s
identifier: %s
signature: %s
---
`,
metadata.FileTypeMdToml: `+++
title = %s
date = %s
tags = %s
identifier = %s
signature = %s
+++
`,
metadata.FileTypeTxt: `title: %s
date: %s
tags: %s
identifier: %s
signature: %s
---------------------------
`,
}
// formatTags formats tags according to file type
func formatTags(tags []string, fileType metadata.FileType) string {
if len(tags) == 0 {
return ""
}
switch fileType {
case metadata.FileTypeOrg:
return ":" + strings.Join(tags, ":") + ":"
case metadata.FileTypeMdYaml, metadata.FileTypeMdToml:
return "[" + strings.Join(tags, ", ") + "]"
default:
return strings.Join(tags, " ")
}
}
// Marshal returns the formatted frontmatter content as bytes
func Marshal(fm *metadata.FrontMatter, fileType metadata.FileType) []byte {
template := templates[fileType]
dateStr := time.Now().Format("2006-01-02 Mon 15:04")
// For org-mode, wrap date in brackets for timestamp
if fileType == metadata.FileTypeOrg {
dateStr = "[" + dateStr + "]"
}
keywordsStr := formatTags(fm.Tags, fileType)
content := fmt.Sprintf(template, fm.Title, dateStr, keywordsStr, fm.Identifier, fm.Signature)
return []byte(content)
}
// Unmarshal extracts front matter from file content.
// ext should be the file extension (e.g., ".md", ".org", ".txt").
// Returns the parsed frontmatter and the detected FileType.
func Unmarshal(content []byte, ext string) (*metadata.FrontMatter, metadata.FileType, error) {
ext = strings.ToLower(ext)
text := string(content)
fm := &metadata.FrontMatter{}
var fileType metadata.FileType
switch ext {
case ".org":
fileType = metadata.FileTypeOrg
if m := regexp.MustCompile(`(?m)^#\+title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Title = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^#\+filetags:[ \t]*:(.+):$`).FindStringSubmatch(text); m != nil {
fm.Tags = strings.Split(m[1], ":")
}
if m := regexp.MustCompile(`(?m)^#\+identifier:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Identifier = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^#\+signature:[ \t]*(.*)$`).FindStringSubmatch(text); m != nil {
fm.Signature = strings.TrimSpace(m[1])
}
case ".md":
// Try YAML first
yamlRe := regexp.MustCompile(`(?ms)^---\n(.*?)\n---`)
if m := yamlRe.FindStringSubmatch(text); m != nil {
fileType = metadata.FileTypeMdYaml
yamlContent := m[1]
if m := regexp.MustCompile(`(?m)^title:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil {
fm.Title = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^tags:[ \t]*\[(.+?)\]$`).FindStringSubmatch(yamlContent); m != nil {
tags := strings.Split(m[1], ",")
for i, t := range tags {
t = strings.TrimSpace(t)
t = strings.Trim(t, `"'`)
tags[i] = t
}
fm.Tags = tags
}
if m := regexp.MustCompile(`(?m)^identifier:[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(yamlContent); m != nil {
fm.Identifier = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^signature:[ \t]*["']?(.*)["']?$`).FindStringSubmatch(yamlContent); m != nil {
fm.Signature = strings.TrimSpace(m[1])
}
} else {
// Try TOML
tomlRe := regexp.MustCompile(`(?ms)^\+\+\+\n(.*?)\n\+\+\+`)
if m := tomlRe.FindStringSubmatch(text); m != nil {
fileType = metadata.FileTypeMdToml
tomlContent := m[1]
if m := regexp.MustCompile(`(?m)^title[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil {
fm.Title = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^tags[ \t]*=[ \t]*\[(.+?)\]$`).FindStringSubmatch(tomlContent); m != nil {
tags := strings.Split(m[1], ",")
for i, t := range tags {
t = strings.TrimSpace(t)
t = strings.Trim(t, `"'`)
tags[i] = t
}
fm.Tags = tags
}
if m := regexp.MustCompile(`(?m)^identifier[ \t]*=[ \t]*["']?(.+?)["']?$`).FindStringSubmatch(tomlContent); m != nil {
fm.Identifier = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^signature[ \t]*=[ \t]*["']?(.*)["']?$`).FindStringSubmatch(tomlContent); m != nil {
fm.Signature = strings.TrimSpace(m[1])
}
}
}
case ".txt":
fileType = metadata.FileTypeTxt
if m := regexp.MustCompile(`(?m)^title:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Title = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^tags:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Tags = strings.Fields(m[1])
}
if m := regexp.MustCompile(`(?m)^identifier:[ \t]*(.+)$`).FindStringSubmatch(text); m != nil {
fm.Identifier = strings.TrimSpace(m[1])
}
if m := regexp.MustCompile(`(?m)^signature:[ \t]*(.*)$`).FindStringSubmatch(text); m != nil {
fm.Signature = strings.TrimSpace(m[1])
}
}
return fm, fileType, nil
}