canister-frontmatter.mjs
103 lines 3.0 KB
Raw
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