fix(parser): align Go and JS parsers with capture grammar spec
Kind prefixes now follow the canonical grammar: `-` for todo, `@time` for event, `!time` for reminder. Removed `*`/`◇`/`▸` as capture aliases (display-layer only). Added `\` escape prefix, `?` query mode, `!pin` flag extraction, `##word` hash escape, and tag lowercasing. Both parsers produce identical results.
This commit is contained in:
@@ -18,56 +18,87 @@ func TestParse(t *testing.T) {
|
||||
wantTime *string
|
||||
wantTags []string
|
||||
wantCard *string
|
||||
wantPin bool
|
||||
wantQuery bool
|
||||
wantFilter []string
|
||||
wantErrSub string
|
||||
}{
|
||||
// Glyph detection
|
||||
{"plain note", "hello world", "hello world", "note", nil, nil, nil, nil, nil, ""},
|
||||
{"dash todo", "- deploy nginx", "deploy nginx", "todo", nil, nil, nil, nil, nil, ""},
|
||||
{"unicode todo", "▸ deploy nginx", "deploy nginx", "todo", nil, nil, nil, nil, nil, ""},
|
||||
{"star event", "* dentist", "dentist", "event", nil, nil, nil, nil, nil, ""},
|
||||
{"unicode event", "◇ dentist", "dentist", "event", nil, nil, nil, nil, nil, ""},
|
||||
// Kind prefixes
|
||||
{"plain note", "hello world", "hello world", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"dash todo", "- deploy nginx", "deploy nginx", "todo", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"dash todo requires space", "-deploy", "-deploy", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"event prefix", "@14:00 dentist", "dentist", "event", nil, nil, sp("14:00"), nil, nil, false, false, nil, ""},
|
||||
{"event no body", "@9:30", "", "event", nil, nil, sp("9:30"), nil, nil, false, false, nil, "empty body"},
|
||||
{"reminder prefix", "!15:00 call dentist", "call dentist", "reminder", nil, nil, sp("15:00"), nil, nil, false, false, nil, ""},
|
||||
{"reminder no body", "!9:30", "", "reminder", nil, nil, sp("9:30"), nil, nil, false, false, nil, "empty body"},
|
||||
|
||||
// Time anchor
|
||||
{"with time", "meeting @14:00", "meeting", "note", nil, nil, sp("14:00"), nil, nil, ""},
|
||||
{"time at start", "@9:30 standup", "standup", "note", nil, nil, sp("9:30"), nil, nil, ""},
|
||||
{"invalid hours", "meeting @25:00", "", "", nil, nil, nil, nil, nil, "invalid time"},
|
||||
{"invalid minutes", "meeting @14:60", "", "", nil, nil, nil, nil, nil, "invalid time"},
|
||||
// Event/reminder with invalid time — @ stays as body token, ! stays as body token
|
||||
{"at-sign not time", "@nottime hello", "@nottime hello", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"bang not time", "!nottime hello", "!nottime hello", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Tags
|
||||
{"single tag", "deploy #ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, ""},
|
||||
{"multiple tags", "deploy #ops #infra", "deploy", "note", nil, nil, nil, []string{"ops", "infra"}, nil, ""},
|
||||
{"duplicate tags", "deploy #ops #ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, ""},
|
||||
{"tag with hyphen", "task #dev-ops", "task", "note", nil, nil, nil, []string{"dev-ops"}, nil, ""},
|
||||
// Escape prefix
|
||||
{"escape dash", `\- this is not a todo`, "- this is not a todo", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"escape at", `\@14:00 not event`, "not event", "note", nil, nil, sp("14:00"), nil, nil, false, false, nil, ""},
|
||||
{"escape plain", `\hello`, "hello", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Card suffix
|
||||
{"caret card", "trick #nginx ^card", "trick", "note", nil, nil, nil, []string{"nginx"}, sp("snippet"), ""},
|
||||
{"caret c", "trick ^c", "trick", "note", nil, nil, nil, nil, sp("snippet"), ""},
|
||||
{"caret template", "deploy ${host} ^template", "deploy ${host}", "note", nil, nil, nil, nil, sp("template"), ""},
|
||||
{"caret snippet explicit", "trick ^snippet", "trick", "note", nil, nil, nil, nil, sp("snippet"), ""},
|
||||
{"invalid card type", "thing ^bogus", "", "", nil, nil, nil, nil, nil, "invalid card type"},
|
||||
// Query mode
|
||||
{"query basic", "? proxy config", "proxy config", "", nil, nil, nil, nil, nil, false, true, nil, ""},
|
||||
{"query with tags", "? proxy config #ops #infra", "proxy config", "", nil, nil, nil, nil, nil, false, true, []string{"ops", "infra"}, ""},
|
||||
{"query tags only", "? #ops", "", "", nil, nil, nil, nil, nil, false, true, []string{"ops"}, ""},
|
||||
|
||||
// Inline time anchor
|
||||
{"inline time", "meeting @14:00", "meeting", "note", nil, nil, sp("14:00"), nil, nil, false, false, nil, ""},
|
||||
{"todo due time", "- buy milk @9:30", "buy milk", "todo", nil, nil, sp("9:30"), nil, nil, false, false, nil, ""},
|
||||
{"invalid hours stays as body", "meeting @25:00", "meeting @25:00", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"invalid minutes stays as body", "meeting @14:60", "meeting @14:60", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Tags (lowercased)
|
||||
{"single tag", "deploy #Ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"multiple tags", "deploy #ops #Infra", "deploy", "note", nil, nil, nil, []string{"ops", "infra"}, nil, false, false, nil, ""},
|
||||
{"duplicate tags", "deploy #ops #Ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"tag with hyphen", "task #dev-ops", "task", "note", nil, nil, nil, []string{"dev-ops"}, nil, false, false, nil, ""},
|
||||
|
||||
// Hash escape
|
||||
{"double hash escape", "use ##channel in slack", "use #channel in slack", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"double hash with tag", "use ##channel #ops", "use #channel", "note", nil, nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
|
||||
// Pin flag
|
||||
{"pin flag", "important thing !pin", "important thing", "note", nil, nil, nil, nil, nil, true, false, nil, ""},
|
||||
{"pin case insensitive", "important !Pin #work", "important", "note", nil, nil, nil, []string{"work"}, nil, true, false, nil, ""},
|
||||
{"pin with todo", "- urgent task !pin", "urgent task", "todo", nil, nil, nil, nil, nil, true, false, nil, ""},
|
||||
|
||||
// !pin at start — not a reminder, flag is extracted
|
||||
{"bang pin only", "!pin important", "important", "note", nil, nil, nil, nil, nil, true, false, nil, ""},
|
||||
|
||||
// Card suffix (kept for now)
|
||||
{"caret card", "trick #nginx ^card", "trick", "note", nil, nil, nil, []string{"nginx"}, sp("snippet"), false, false, nil, ""},
|
||||
{"caret c", "trick ^c", "trick", "note", nil, nil, nil, nil, sp("snippet"), false, false, nil, ""},
|
||||
{"caret template", "deploy ${host} ^template", "deploy ${host}", "note", nil, nil, nil, nil, sp("template"), false, false, nil, ""},
|
||||
{"invalid card type", "thing ^bogus", "", "", nil, nil, nil, nil, nil, false, false, nil, "invalid card type"},
|
||||
|
||||
// Combined
|
||||
{"full input", "- deploy nginx to staging @15:00 #ops", "deploy nginx to staging", "todo", nil, nil, sp("15:00"), []string{"ops"}, nil, ""},
|
||||
{"full with card", "figured out the proxy_pass trick #nginx ^card", "figured out the proxy_pass trick", "note", nil, nil, nil, []string{"nginx"}, sp("snippet"), ""},
|
||||
{"full todo", "- deploy nginx @15:00 #ops", "deploy nginx", "todo", nil, nil, sp("15:00"), []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"full event", "@14:00 lunch with alex #personal", "lunch with alex", "event", nil, nil, sp("14:00"), []string{"personal"}, nil, false, false, nil, ""},
|
||||
{"full reminder", "!15:00 call dentist #health", "call dentist", "reminder", nil, nil, sp("15:00"), []string{"health"}, nil, false, false, nil, ""},
|
||||
|
||||
// Title
|
||||
{"title with body", "|nginx trick\nproxy_pass trailing slash #ops", "proxy_pass trailing slash", "note", sp("nginx trick"), nil, nil, []string{"ops"}, nil, ""},
|
||||
{"no title", "no pipe here #ops", "no pipe here", "note", nil, nil, nil, []string{"ops"}, nil, ""},
|
||||
{"todo with title", "- |deploy staging\nrebuild docker #ops", "rebuild docker", "todo", sp("deploy staging"), nil, nil, []string{"ops"}, nil, ""},
|
||||
{"title only", "|title only", "", "note", sp("title only"), nil, nil, nil, nil, ""},
|
||||
{"title and desc", "|title // description #ops\nbody here", "body here", "note", sp("title"), sp("description"), nil, []string{"ops"}, nil, ""},
|
||||
{"todo title desc", "- |deploy staging // rebuild and push #ops", "", "todo", sp("deploy staging"), sp("rebuild and push"), nil, []string{"ops"}, nil, ""},
|
||||
{"title with body", "|nginx trick\nproxy_pass trailing slash #ops", "proxy_pass trailing slash", "note", sp("nginx trick"), nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"no title", "no pipe here #ops", "no pipe here", "note", nil, nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"todo with title", "- |deploy staging\nrebuild docker #ops", "rebuild docker", "todo", sp("deploy staging"), nil, nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"title only", "|title only", "", "note", sp("title only"), nil, nil, nil, nil, false, false, nil, ""},
|
||||
{"title and desc", "|title // description #ops\nbody here", "body here", "note", sp("title"), sp("description"), nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
{"todo title desc", "- |deploy staging // rebuild and push #ops", "", "todo", sp("deploy staging"), sp("rebuild and push"), nil, []string{"ops"}, nil, false, false, nil, ""},
|
||||
|
||||
// Description without title
|
||||
{"leading desc", "// leading desc\nbody content", "body content", "note", nil, sp("leading desc"), nil, nil, nil, ""},
|
||||
{"inline desc", "body text // inline desc", "body text", "note", nil, sp("inline desc"), nil, nil, nil, ""},
|
||||
{"url no split", "http://example.com // should not split", "http://example.com // should not split", "note", nil, nil, nil, nil, nil, ""},
|
||||
{"leading desc", "// leading desc\nbody content", "body content", "note", nil, sp("leading desc"), nil, nil, nil, false, false, nil, ""},
|
||||
{"inline desc", "body text // inline desc", "body text", "note", nil, sp("inline desc"), nil, nil, nil, false, false, nil, ""},
|
||||
{"url no split", "http://example.com // should not split", "http://example.com // should not split", "note", nil, nil, nil, nil, nil, false, false, nil, ""},
|
||||
|
||||
// Edge cases
|
||||
{"empty input", "", "", "", nil, nil, nil, nil, nil, "empty"},
|
||||
{"only glyph", "-", "", "", nil, nil, nil, nil, nil, "empty body"},
|
||||
{"only modifiers", "#ops @14:00", "", "", nil, nil, nil, nil, nil, "empty body"},
|
||||
{"whitespace only", " ", "", "", nil, nil, nil, nil, nil, "empty"},
|
||||
{"empty input", "", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty"},
|
||||
{"only glyph", "-", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty body"},
|
||||
{"only modifiers", "#ops @14:00", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty body"},
|
||||
{"whitespace only", " ", "", "", nil, nil, nil, nil, nil, false, false, nil, "empty"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -109,6 +140,15 @@ func TestParse(t *testing.T) {
|
||||
if !ptrEq(got.CardSuffix, tt.wantCard) {
|
||||
t.Errorf("card_suffix: got %v, want %v", strPtr(got.CardSuffix), strPtr(tt.wantCard))
|
||||
}
|
||||
if got.Pin != tt.wantPin {
|
||||
t.Errorf("pin: got %v, want %v", got.Pin, tt.wantPin)
|
||||
}
|
||||
if got.Query != tt.wantQuery {
|
||||
t.Errorf("query: got %v, want %v", got.Query, tt.wantQuery)
|
||||
}
|
||||
if !tagsEq(got.FilterTags, tt.wantFilter) {
|
||||
t.Errorf("filter_tags: got %v, want %v", got.FilterTags, tt.wantFilter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user