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:
+118
-28
@@ -2,13 +2,13 @@
|
||||
'use strict';
|
||||
|
||||
const GLYPHS = {
|
||||
note: '—', todo: '○', event: '◇',
|
||||
note: '—', todo: '○', event: '◇', reminder: '△',
|
||||
snippet: '◆', template: '◈', checklist: '☐',
|
||||
decision: '⚖', link: '↗',
|
||||
};
|
||||
|
||||
const GLYPH_CLASSES = {
|
||||
note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event',
|
||||
note: 'glyph-note', todo: 'glyph-todo', event: 'glyph-event', reminder: 'glyph-reminder',
|
||||
snippet: 'glyph-snippet', template: 'glyph-template',
|
||||
checklist: 'glyph-checklist', decision: 'glyph-decision',
|
||||
link: 'glyph-link',
|
||||
@@ -120,23 +120,76 @@
|
||||
|
||||
const VALID_CARDS = { card: 'snippet', c: 'snippet', snippet: 'snippet', template: 'template', checklist: 'checklist', decision: 'decision', link: 'link' };
|
||||
|
||||
function validateTime(s) {
|
||||
const parts = s.split(':');
|
||||
if (parts.length !== 2) return false;
|
||||
const h = parseInt(parts[0], 10), m = parseInt(parts[1], 10);
|
||||
return !isNaN(h) && !isNaN(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59;
|
||||
}
|
||||
|
||||
function parseInput(input) {
|
||||
input = input.trim();
|
||||
if (!input) return null;
|
||||
|
||||
let glyph = 'note';
|
||||
let remaining = input;
|
||||
let timeAnchor = null, cardSuffix = null, pin = false, query = false;
|
||||
const tags = [], seenTags = {}, filterTags = [];
|
||||
|
||||
const sp = remaining.indexOf(' ');
|
||||
if (sp >= 0) {
|
||||
const first = remaining.slice(0, sp);
|
||||
if (first === '-' || first === '▸') { glyph = 'todo'; remaining = remaining.slice(sp + 1).trim(); }
|
||||
else if (first === '*' || first === '◇') { glyph = 'event'; remaining = remaining.slice(sp + 1).trim(); }
|
||||
} else {
|
||||
if (remaining === '-' || remaining === '▸') { glyph = 'todo'; remaining = ''; }
|
||||
else if (remaining === '*' || remaining === '◇') { glyph = 'event'; remaining = ''; }
|
||||
// Step 1: Escape check — `\` prefix → thought, skip prefix detection
|
||||
if (remaining.startsWith('\\')) {
|
||||
remaining = remaining.slice(1);
|
||||
const result = extractModifiers(remaining, true);
|
||||
if (!result.body) return null;
|
||||
return { body: result.body, glyph: 'note', title: null, description: null, timeAnchor: result.timeAnchor, tags: result.tags, cardSuffix: result.cardSuffix, pin: result.pin, query: false, filterTags: [] };
|
||||
}
|
||||
|
||||
// Step 2: Query check — `?` prefix → search mode
|
||||
if (remaining.startsWith('?')) {
|
||||
remaining = remaining.slice(1).trim();
|
||||
const tokens = remaining.split(/\s+/).filter(Boolean);
|
||||
const bodyParts = [];
|
||||
for (const tok of tokens) {
|
||||
if (tok.startsWith('#') && tok.length > 1 && !tok.startsWith('##')) {
|
||||
filterTags.push(tok.slice(1).toLowerCase());
|
||||
} else {
|
||||
bodyParts.push(tok);
|
||||
}
|
||||
}
|
||||
return { body: bodyParts.join(' '), glyph: '', title: null, description: null, timeAnchor: null, tags: [], cardSuffix: null, pin: false, query: true, filterTags };
|
||||
}
|
||||
|
||||
// Step 3: Kind prefix — `-`, `@time`, `!time`
|
||||
if (remaining.startsWith('- ')) {
|
||||
glyph = 'todo';
|
||||
remaining = remaining.slice(2).trim();
|
||||
} else if (remaining === '-') {
|
||||
glyph = 'todo';
|
||||
remaining = '';
|
||||
} else if (remaining.startsWith('@')) {
|
||||
const afterAt = remaining.slice(1).trim();
|
||||
const sp = afterAt.indexOf(' ');
|
||||
const timeTok = sp >= 0 ? afterAt.slice(0, sp) : afterAt;
|
||||
if (validateTime(timeTok)) {
|
||||
glyph = 'event';
|
||||
timeAnchor = timeTok;
|
||||
remaining = sp >= 0 ? afterAt.slice(sp + 1).trim() : '';
|
||||
}
|
||||
} else if (remaining.startsWith('!')) {
|
||||
const afterBang = remaining.slice(1).trim();
|
||||
const firstWord = afterBang.split(/\s+/)[0] || '';
|
||||
if (firstWord.toLowerCase() !== 'pin') {
|
||||
const sp = afterBang.indexOf(' ');
|
||||
const timeTok = sp >= 0 ? afterBang.slice(0, sp) : afterBang;
|
||||
if (validateTime(timeTok)) {
|
||||
glyph = 'reminder';
|
||||
timeAnchor = timeTok;
|
||||
remaining = sp >= 0 ? afterBang.slice(sp + 1).trim() : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Steps 4-5: Title and description extraction
|
||||
let titleRaw = null, descRaw = null, hasTitle = false;
|
||||
const lines = remaining.split('\n');
|
||||
const firstLine = (lines[0] || '').trim();
|
||||
@@ -174,42 +227,59 @@
|
||||
}
|
||||
}
|
||||
|
||||
let timeAnchor = null, cardSuffix = null;
|
||||
const tags = [], seenTags = {};
|
||||
|
||||
function extract(text) {
|
||||
// Steps 6-8: Extract flags, tags, time, card suffix
|
||||
function extractModifiers(text, handleFlags) {
|
||||
const tokens = text.split(/\s+/).filter(Boolean);
|
||||
const parts = [];
|
||||
let localTime = timeAnchor, localPin = pin, localCard = cardSuffix;
|
||||
const localTags = [...tags];
|
||||
const localSeen = { ...seenTags };
|
||||
|
||||
for (const tok of tokens) {
|
||||
if (tok.startsWith('@') && tok.length > 1) {
|
||||
timeAnchor = tok.slice(1);
|
||||
if (handleFlags && tok.toLowerCase() === '!pin') {
|
||||
localPin = true;
|
||||
} else if (tok.startsWith('##') && tok.length > 2) {
|
||||
parts.push('#' + tok.slice(2));
|
||||
} else if (tok.startsWith('@') && tok.length > 1) {
|
||||
const ts = tok.slice(1);
|
||||
if (validateTime(ts) && localTime === null) {
|
||||
localTime = ts;
|
||||
} else {
|
||||
parts.push(tok);
|
||||
}
|
||||
} else if (tok.startsWith('#') && tok.length > 1) {
|
||||
const tag = tok.slice(1);
|
||||
if (!seenTags[tag]) { tags.push(tag); seenTags[tag] = true; }
|
||||
const tag = tok.slice(1).toLowerCase();
|
||||
if (!localSeen[tag]) { localTags.push(tag); localSeen[tag] = true; }
|
||||
} else if (tok.startsWith('^') && tok.length > 1) {
|
||||
const suffix = tok.slice(1);
|
||||
if (VALID_CARDS[suffix]) cardSuffix = VALID_CARDS[suffix];
|
||||
if (VALID_CARDS[suffix] && localCard === null) localCard = VALID_CARDS[suffix];
|
||||
else parts.push(tok);
|
||||
} else {
|
||||
parts.push(tok);
|
||||
}
|
||||
}
|
||||
return parts.join(' ');
|
||||
return { body: parts.join(' '), timeAnchor: localTime, tags: localTags, seen: localSeen, cardSuffix: localCard, pin: localPin };
|
||||
}
|
||||
|
||||
let title = null, description = null;
|
||||
if (hasTitle) {
|
||||
const clean = extract(titleRaw || '');
|
||||
if (clean) title = clean;
|
||||
const r = extractModifiers(titleRaw || '', false);
|
||||
if (r.body) title = r.body;
|
||||
timeAnchor = r.timeAnchor; Object.assign(seenTags, r.seen); tags.length = 0; tags.push(...r.tags); cardSuffix = r.cardSuffix; pin = r.pin;
|
||||
}
|
||||
if (descRaw) {
|
||||
const clean = extract(descRaw);
|
||||
if (clean) description = clean;
|
||||
const r = extractModifiers(descRaw, false);
|
||||
if (r.body) description = r.body;
|
||||
timeAnchor = r.timeAnchor; Object.assign(seenTags, r.seen); tags.length = 0; tags.push(...r.tags); cardSuffix = r.cardSuffix; pin = r.pin;
|
||||
}
|
||||
|
||||
const body = extract(remaining);
|
||||
const bodyResult = extractModifiers(remaining, true);
|
||||
const body = bodyResult.body;
|
||||
timeAnchor = bodyResult.timeAnchor; tags.length = 0; tags.push(...bodyResult.tags); cardSuffix = bodyResult.cardSuffix; pin = bodyResult.pin;
|
||||
|
||||
if (!body && !title) return null;
|
||||
|
||||
return { body, glyph, title, description, timeAnchor, tags, cardSuffix };
|
||||
return { body, glyph, title, description, timeAnchor, tags, cardSuffix, pin, query: false, filterTags: [] };
|
||||
}
|
||||
|
||||
function detectCardType(body) {
|
||||
@@ -334,6 +404,7 @@
|
||||
el.addEventListener('click', () => {
|
||||
state.intent = el.dataset.intent;
|
||||
renderTagRail();
|
||||
renderEntityList();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -371,6 +442,16 @@
|
||||
const parsed = parseInput(val);
|
||||
if (!parsed) return;
|
||||
|
||||
// Query mode → switch to search
|
||||
if (parsed.query) {
|
||||
state.searchQuery = parsed.body;
|
||||
const searchInput = $('#search-input');
|
||||
if (searchInput) searchInput.value = parsed.body + (parsed.filterTags.length ? ' ' + parsed.filterTags.map(t => '#' + t).join(' ') : '');
|
||||
input.value = '';
|
||||
renderEntityList();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
body: parsed.body,
|
||||
glyph: parsed.glyph,
|
||||
@@ -380,6 +461,7 @@
|
||||
if (parsed.description) data.description = parsed.description;
|
||||
if (parsed.timeAnchor) data.time_anchor = parsed.timeAnchor;
|
||||
if (parsed.cardSuffix) data.card_type = parsed.cardSuffix;
|
||||
if (parsed.pin) data.pinned = true;
|
||||
|
||||
await api.createEntity(data);
|
||||
input.value = '';
|
||||
@@ -1399,12 +1481,20 @@
|
||||
if (ev.key === 'Escape') { searchInput.value = ''; state.searchQuery = ''; renderEntityList(); searchInput.blur(); }
|
||||
});
|
||||
|
||||
function filterByIntent(entities) {
|
||||
if (state.view !== 'cards' || state.intent === 'grab') return entities;
|
||||
if (state.intent === 'read') return entities.filter(e => e.card_data);
|
||||
if (state.intent === 'fill') return entities.filter(e => e.body && /\$\{.+\}/.test(e.body));
|
||||
return entities;
|
||||
}
|
||||
|
||||
function filterBySearch(entities) {
|
||||
if (!state.searchQuery) return entities;
|
||||
const intentFiltered = filterByIntent(entities);
|
||||
if (!state.searchQuery) return intentFiltered;
|
||||
let query = state.searchQuery;
|
||||
let filterTags = [];
|
||||
query = query.replace(/#(\S+)/g, (_, tag) => { filterTags.push(tag); return ''; }).trim();
|
||||
return entities.filter(e => {
|
||||
return intentFiltered.filter(e => {
|
||||
if (filterTags.length) {
|
||||
const eTags = (e.tags || []).map(t => t.toLowerCase());
|
||||
if (!filterTags.every(ft => eTags.includes(ft))) return false;
|
||||
|
||||
@@ -356,6 +356,7 @@ main {
|
||||
.glyph-note { color: var(--muted); }
|
||||
.glyph-todo { color: var(--todo); }
|
||||
.glyph-event { color: var(--event); }
|
||||
.glyph-reminder { color: var(--remind); }
|
||||
.glyph-snippet { color: var(--accent); }
|
||||
.glyph-template { color: var(--lineage); }
|
||||
.glyph-checklist { color: var(--remind); }
|
||||
|
||||
Reference in New Issue
Block a user