Warn on invalid tags instead of exiting
- Add IsValidTag/ValidateTags to pkg/metadata/metadata.go - LoadAll warns on invalid tags with acme-style path (path:1) - Split results.Unmarshal: lenient (warns) vs UnmarshalStrict (errors) - Put command uses strict validation, index reading uses lenient - Program continues running with invalid tags instead of panicking
This commit is contained in:
parent
464ce00cde
commit
3657805994
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
4
main.go
4
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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue