diff --git a/internal/disk/loader.go b/internal/disk/loader.go index cb2f6e5..709c4de 100644 --- a/internal/disk/loader.go +++ b/internal/disk/loader.go @@ -5,6 +5,7 @@ import ( "denote/pkg/metadata" "denote/pkg/util" "fmt" + "log" "os" "path/filepath" "regexp" @@ -169,6 +170,10 @@ func LoadAll(dir string) (metadata.Results, error) { // Only include files with valid identifiers if note.Identifier != "" { + // Warn on invalid tags but continue loading + if invalid := metadata.ValidateTags(note.Tags); len(invalid) > 0 { + log.Printf("warning: %s:1 invalid tags %v (must be lowercase alphanumeric)", path, invalid) + } notes = append(notes, note) } diff --git a/main.go b/main.go index 361bd35..1441d58 100644 --- a/main.go +++ b/main.go @@ -376,8 +376,8 @@ func main() { break } - // Parse the window content - updated, err := results.Unmarshal(body) + // Parse the window content with strict validation + updated, err := results.UnmarshalStrict(body) if err != nil { log.Printf("failed to parse window: %v", err) break diff --git a/pkg/encoding/results/results.go b/pkg/encoding/results/results.go index 60b9b78..6ca12b7 100644 --- a/pkg/encoding/results/results.go +++ b/pkg/encoding/results/results.go @@ -3,6 +3,7 @@ package results import ( "bytes" "fmt" + "log" "regexp" "strings" @@ -27,7 +28,18 @@ func Marshal(rs metadata.Results) []byte { // Unmarshal parses pipe-delimited byte data into Results. // Format: identifier | title | tags (comma-separated) +// Invalid tags produce warnings but parsing continues. func Unmarshal(data []byte) (metadata.Results, error) { + return unmarshal(data, false) +} + +// UnmarshalStrict parses pipe-delimited byte data into Results with strict tag validation. +// Returns an error if any tags are invalid. +func UnmarshalStrict(data []byte) (metadata.Results, error) { + return unmarshal(data, true) +} + +func unmarshal(data []byte, strict bool) (metadata.Results, error) { var results metadata.Results lines := bytes.Split(bytes.TrimSpace(data), []byte("\n")) // Allow lowercase Latin letters, other letters (CJK, etc.), and digits, no spaces @@ -55,7 +67,10 @@ func Unmarshal(data []byte) (metadata.Results, error) { var tags []string if tagsStr != "" { if !tagPattern.MatchString(tagsStr) { - return nil, fmt.Errorf("line %d: tags must be comma-delimited lowercase unicode words (no spaces): got '%s'", lineNum+1, tagsStr) + if strict { + return nil, fmt.Errorf("line %d: tags must be comma-delimited lowercase unicode words (no spaces): got '%s'", lineNum+1, tagsStr) + } + log.Printf("warning: line %d: invalid tags '%s' (must be lowercase alphanumeric)", lineNum+1, tagsStr) } tags = strings.Split(tagsStr, ",") } else { diff --git a/pkg/metadata/metadata.go b/pkg/metadata/metadata.go index 7d33ccf..00208b6 100644 --- a/pkg/metadata/metadata.go +++ b/pkg/metadata/metadata.go @@ -7,6 +7,7 @@ import ( "sort" "strings" "time" + "unicode" ) // Metadata is the metadata encoded into Denote-style @@ -89,6 +90,30 @@ func ParseFilename(path string) *Metadata { return note } +// IsValidTag returns true if the tag contains only lowercase letters, other unicode letters, or digits. +func IsValidTag(tag string) bool { + for _, r := range tag { + if !unicode.IsLetter(r) && !unicode.IsDigit(r) { + return false + } + if unicode.IsLetter(r) && unicode.IsUpper(r) { + return false + } + } + return len(tag) > 0 +} + +// ValidateTags checks all tags and returns invalid ones. +func ValidateTags(tags []string) []string { + var invalid []string + for _, tag := range tags { + if !IsValidTag(tag) { + invalid = append(invalid, tag) + } + } + return invalid +} + // GenerateIdentifier creates a new identifier timestamp. func GenerateIdentifier() string { return time.Now().Format("20060102T150405") diff --git a/scripts/Djournal b/scripts/Djournal index 5a0c416..c3330f3 100755 --- a/scripts/Djournal +++ b/scripts/Djournal @@ -40,7 +40,7 @@ if(! ~ $#* 0) { } # Calculate reference timestamp based on interval -epoch=`{date -n} +epoch=`{9 date -n} # Adjust epoch to interval start if no offset provided if(~ $#* 0) { @@ -48,7 +48,7 @@ if(~ $#* 0) { case weekly # Get start of week (Monday) # date outputs: Sun Nov 24 07:30:00 EST 2025 - dow=`{date $epoch | awk '{print $1}'} + dow=`{9 date $epoch | awk '{print $1}'} # Calculate days since Monday (Mon=0, Tue=1, ..., Sun=6) daysback=0 switch($dow) { @@ -64,20 +64,20 @@ if(~ $#* 0) { case monthly # Get start of month (day 1) # Extract year, month from current date - yearmonth=`{date $epoch | awk '{print $6 " " $2}'} + yearmonth=`{9 date $epoch | awk '{print $6 " " $2}'} # Use date with format: Nov 1 2025 - epoch=`{date -n `{echo $yearmonth | awk '{print $2 " 1 " $1}'}} + epoch=`{9 date -n `{echo $yearmonth | awk '{print $2 " 1 " $1}'}} case yearly # Get start of year (Jan 1) - year=`{date $epoch | awk '{print $6}'} - epoch=`{date -n 'Jan 1 '^$year} + year=`{9 date $epoch | awk '{print $6}'} + epoch=`{9 date -n 'Jan 1 '^$year} } } targetepoch=`{hoc -e 'int('$epoch' + '$offset')'} # Get formatted date (e.g., "Wednesday 12 November 2025 22:11") -datefull=`{date $targetepoch | awk ' +datefull=`{9 date $targetepoch | awk ' BEGIN { m["Jan"]="January"; m["Feb"]="February"; m["Mar"]="March" m["Apr"]="April"; m["May"]="May"; m["Jun"]="June"