canister-frontmatter.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Hub canister note APIs return `frontmatter` as JSON **text** (Motoko escapes it in the outer JSON). |
| 3 | * Browsers/clients may also see an extra JSON string layer. Parse to an object for `title` and friends. |
| 4 | */ |
| 5 | |
| 6 | /** |
| 7 | * @param {unknown} fm |
| 8 | * @returns {Record<string, unknown>|null} |
| 9 | */ |
| 10 | export function parseCanisterFrontmatter(fm) { |
| 11 | if (fm == null) return null; |
| 12 | if (typeof fm === 'object' && fm !== null && !Array.isArray(fm)) { |
| 13 | return /** @type {Record<string, unknown>} */ (fm); |
| 14 | } |
| 15 | if (typeof fm !== 'string') return null; |
| 16 | let s = fm.trim(); |
| 17 | if (!s || s === '{}') return null; |
| 18 | |
| 19 | const tryParseObject = (t) => { |
| 20 | try { |
| 21 | const o = JSON.parse(t); |
| 22 | if (o && typeof o === 'object' && !Array.isArray(o)) return /** @type {Record<string, unknown>} */ (o); |
| 23 | if (typeof o === 'string') return tryParseObject(o.trim()); |
| 24 | return null; |
| 25 | } catch { |
| 26 | return null; |
| 27 | } |
| 28 | }; |
| 29 | |
| 30 | let o = tryParseObject(s); |
| 31 | if (o) return o; |
| 32 | |
| 33 | if (/\\"/.test(s)) { |
| 34 | o = tryParseObject(s.replace(/\\"/g, '"')); |
| 35 | if (o) return o; |
| 36 | } |
| 37 | |
| 38 | if (s.startsWith('"') && s.endsWith('"')) { |
| 39 | o = tryParseObject(JSON.parse(s)); |
| 40 | if (o) return o; |
| 41 | } |
| 42 | |
| 43 | return null; |
| 44 | } |
| 45 | |
| 46 | /** |
| 47 | * @param {unknown} fm |
| 48 | * @returns {string|null} |
| 49 | */ |
| 50 | export function titleFromCanisterFrontmatter(fm) { |
| 51 | const o = parseCanisterFrontmatter(fm); |
| 52 | if (!o || o.title == null) return null; |
| 53 | const t = String(o.title).trim(); |
| 54 | return t !== '' ? t : null; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * First ATX `# heading` in the note body (canister stores body without the YAML block). |
| 59 | * @param {unknown} body |
| 60 | * @returns {string|null} |
| 61 | */ |
| 62 | export function titleFromMarkdownBody(body) { |
| 63 | if (typeof body !== 'string' || !body.trim()) return null; |
| 64 | for (const line of body.split(/\r?\n/)) { |
| 65 | // ATX headings: one or more # then whitespace (## is common as first visible heading). |
| 66 | const m = /^\s{0,3}(#{1,6})\s+(.+)$/.exec(line); |
| 67 | if (m) { |
| 68 | let t = m[2].trim(); |
| 69 | t = t.replace(/\s+#+\s*$/, '').trim(); |
| 70 | return t || null; |
| 71 | } |
| 72 | } |
| 73 | return null; |
| 74 | } |
| 75 | |
| 76 | /** |
| 77 | * @param {unknown} path vault-relative path |
| 78 | * @returns {string|null} |
| 79 | */ |
| 80 | export function titleFromPathStem(path) { |
| 81 | if (typeof path !== 'string' || !path.trim()) return null; |
| 82 | const base = path.split('/').pop() || path; |
| 83 | const stem = base.replace(/\.md$/i, ''); |
| 84 | if (!stem) return null; |
| 85 | return stem.replace(/[-_]/g, ' ').trim() || null; |
| 86 | } |
| 87 | |
| 88 | /** |
| 89 | * Title for relate / list-style UX when `frontmatter.title` is absent (common on hosted notes). |
| 90 | * Order: JSON `title` → first `#` line in body → filename stem. |
| 91 | * |
| 92 | * @param {{ frontmatter?: unknown, body?: unknown, path?: unknown }} note |
| 93 | * @returns {string|null} |
| 94 | */ |
| 95 | export function displayTitleFromHostedNote(note) { |
| 96 | if (!note || typeof note !== 'object') return null; |
| 97 | const pth = note.path != null ? String(note.path) : ''; |
| 98 | const fromFm = titleFromCanisterFrontmatter(note.frontmatter); |
| 99 | if (fromFm) return fromFm; |
| 100 | const fromBody = titleFromMarkdownBody(note.body != null ? String(note.body) : ''); |
| 101 | if (fromBody) return fromBody; |
| 102 | return titleFromPathStem(pth); |
| 103 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
2 days ago