sensemaking 0.15.0 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/commands/peek.js +3 -3
- package/dist/cjs/commands/peek.js.map +1 -1
- package/dist/cjs/commands/related.js +2 -2
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/db/open.d.cts +1 -1
- package/dist/cjs/db/open.d.ts +1 -1
- package/dist/cjs/db/open.js +1 -1
- package/dist/cjs/db/open.js.map +1 -1
- package/dist/cjs/features/links.js +101 -16
- package/dist/cjs/features/links.js.map +1 -1
- package/dist/cjs/features/tags.js +13 -48
- package/dist/cjs/features/tags.js.map +1 -1
- package/dist/cjs/fences.d.cts +7 -0
- package/dist/cjs/fences.d.ts +7 -0
- package/dist/cjs/fences.js +106 -3
- package/dist/cjs/fences.js.map +1 -1
- package/dist/esm/commands/peek.js +3 -3
- package/dist/esm/commands/peek.js.map +1 -1
- package/dist/esm/commands/related.js +2 -2
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/db/open.d.ts +1 -1
- package/dist/esm/db/open.js +1 -1
- package/dist/esm/db/open.js.map +1 -1
- package/dist/esm/features/links.js +64 -15
- package/dist/esm/features/links.js.map +1 -1
- package/dist/esm/features/tags.js +14 -49
- package/dist/esm/features/tags.js.map +1 -1
- package/dist/esm/fences.d.ts +7 -0
- package/dist/esm/fences.js +95 -0
- package/dist/esm/fences.js.map +1 -1
- package/package.json +1 -1
- package/skills/sense/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds) plus relative markdown links to .md files.\nfunction extract(_raw: string, body: string): Array<{ target: string; embed: boolean }> {\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in code, since a class-based regex stops at the ].\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = m[1].split('|')[0].split('#')[0].trim();\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(([^)]+\\.md)(?:#[^)]*)?\\)/g)) {\n const target = m[2].trim();\n if (target && !/^[a-z]+:\\/\\//i.test(target)) add(target, m[1] === '!');\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (lexicographically first on ties).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n return byBase.get(baseKey(clean))?.[0] ?? null;\n}\n\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n for (const path of files.map((f) => f.relPath).sort()) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n for (const { target, embed } of targets) insert.run(path, target, baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","extract","_raw","body","seen","Set","results","add","target","embed","key","has","push","m","matchAll","split","trim","index","test","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","get","buildByBase","files","Map","map","f","relPath","sort","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","p","added","vanished","candidates","collect","collectIn","column","keys","i","length","chunk","slice","placeholders","reparsed","values","linkEdges","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","links","name","schema","exec","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"AAAA,OAAOA,WAAW,kBAAkB;AAKpC,0FAA0F;AAC1F,iFAAiF;AACjF,+FAA+F;AAC/F,uFAAuF;AAEvF,qGAAqG;AACrG,SAASC,QAAQC,IAAY,EAAEC,IAAY;IACzC,MAAMC,OAAO,IAAIC;IACjB,MAAMC,UAAqD,EAAE;IAC7D,MAAMC,MAAM,CAACC,QAAgBC;QAC3B,MAAMC,MAAM,GAAGF,OAAO,EAAE,EAAEC,QAAQ,MAAM,KAAK;QAC7C,IAAIL,KAAKO,GAAG,CAACD,MAAM;QACnBN,KAAKG,GAAG,CAACG;QACTJ,QAAQM,IAAI,CAAC;YAAEJ;YAAQC;QAAM;IAC/B;IACA,uFAAuF;IACvF,yFAAyF;IACzF,KAAK,MAAMI,KAAKV,KAAKW,QAAQ,CAAC,kBAAmB;YAEjBD;QAD9B,MAAML,SAASK,CAAC,CAAC,EAAE,CAACE,KAAK,CAAC,IAAI,CAAC,EAAE,CAACA,KAAK,CAAC,IAAI,CAAC,EAAE,CAACC,IAAI;QACpD,IAAIR,QAAQD,IAAIC,QAAQL,IAAI,CAAC,EAACU,WAAAA,EAAEI,KAAK,cAAPJ,sBAAAA,WAAW,KAAK,EAAE,KAAK;IACvD;IACA,KAAK,MAAMA,KAAKV,KAAKW,QAAQ,CAAC,6CAA8C;QAC1E,MAAMN,SAASK,CAAC,CAAC,EAAE,CAACG,IAAI;QACxB,IAAIR,UAAU,CAAC,gBAAgBU,IAAI,CAACV,SAASD,IAAIC,QAAQK,CAAC,CAAC,EAAE,KAAK;IACpE;IACA,OAAOP;AACT;AAEA,SAASa,YAAYX,MAAc;IACjC,OAAOA,OAAOY,OAAO,CAAC,OAAO,KAAKA,OAAO,CAAC,SAAS;AACrD;AAEA,SAASC,QAAQC,IAAY;IAC3B,OAAOtB,MAAMuB,QAAQ,CAACD,MAAMF,OAAO,CAAC,UAAU,IAAII,WAAW;AAC/D;AAEA,uFAAuF;AACvF,2EAA2E;AAC3E,SAASC,cAAcC,GAAW,EAAElB,MAAc,EAAEmB,OAAoB,EAAEC,MAA6B;;QAM9FA;IALP,MAAMC,QAAQV,YAAYX;IAC1B,MAAMsB,UAAU9B,MAAM+B,SAAS,CAAC/B,MAAMgC,IAAI,CAAChC,MAAMiC,OAAO,CAACP,MAAMG;IAC/D,KAAK,MAAMK,aAAa;QAACL;QAAO,GAAGA,MAAM,GAAG,CAAC;QAAEC;QAAS,GAAGA,QAAQ,GAAG,CAAC;KAAC,CAAE;QACxE,IAAIH,QAAQhB,GAAG,CAACuB,YAAY,OAAOA;IACrC;IACA,gBAAON,cAAAA,OAAOO,GAAG,CAACd,QAAQQ,qBAAnBD,kCAAAA,WAA4B,CAAC,EAAE,uCAAI;AAC5C;AAEA,SAASQ,YAAYC,KAAiB;IACpC,MAAMT,SAAS,IAAIU;IACnB,KAAK,MAAMhB,QAAQe,MAAME,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO,EAAEC,IAAI,GAAI;QACrD,MAAMhC,MAAMW,QAAQC;QACpB,MAAMqB,OAAOf,OAAOO,GAAG,CAACzB;QACxB,IAAIiC,MAAMA,KAAK/B,IAAI,CAACU;aACfM,OAAOgB,GAAG,CAAClC,KAAK;YAACY;SAAK;IAC7B;IACA,OAAOM;AACT;AASA,4FAA4F;AAC5F,0FAA0F;AAC1F,0BAA0B;AAC1B,SAASiB,YAAYC,EAAgB,EAAEC,IAAe,EAAEpB,OAAoB,EAAEC,MAA6B;IACzG,MAAMoB,SAASF,GAAGG,OAAO,CAAC;IAC1B,IAAIC,UAAU;IACd,KAAK,MAAMC,OAAOJ,KAAM;QACtB,MAAMK,MAAM3B,cAAc0B,IAAIzB,GAAG,EAAEyB,IAAI3C,MAAM,EAAEmB,SAASC;QACxD,IAAIwB,QAAQD,IAAIC,GAAG,EAAE;YACnBJ,OAAOK,GAAG,CAACD,KAAKD,IAAIzB,GAAG,EAAEyB,IAAI3C,MAAM;YACnC0C,UAAU;QACZ;IACF;IACA,OAAOA;AACT;AAEA,yFAAyF;AACzF,+EAA+E;AAC/E,SAASI,WAAWR,EAAgB,EAAET,KAAiB;IACrD,MAAMV,UAAU,IAAItB,IAAIgC,MAAME,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO;IAClD,MAAMb,SAASQ,YAAYC;IAC3B,MAAMU,OAAOD,GAAGG,OAAO,CAAC,6CAA6CM,GAAG;IACxE,OAAOV,YAAYC,IAAIC,MAAMpB,SAASC;AACxC;AAEA,yFAAyF;AACzF,yFAAyF;AACzF,kEAAkE;AAClE,SAAS4B,mBAAmBV,EAAgB,EAAEW,KAAqB;IACjE,MAAM9B,UAAU,IAAItB,IAAIoD,MAAMpB,KAAK,CAACE,GAAG,CAAC,CAACC,IAAMA,EAAEC,OAAO;IACxD,MAAMb,SAASQ,YAAYqB,MAAMpB,KAAK;IAEtC,MAAMqB,mBAAmB,IAAIrD;IAC7B,KAAK,MAAMsD,KAAKF,MAAMG,KAAK,CAAEF,iBAAiBnD,GAAG,CAACc,QAAQsC;IAC1D,KAAK,MAAMA,KAAKF,MAAMI,QAAQ,CAAEH,iBAAiBnD,GAAG,CAACc,QAAQsC;IAE7D,+EAA+E;IAC/E,MAAMG,aAAa,IAAIxB;IACvB,MAAMyB,UAAU,CAAChB;QACf,KAAK,MAAMI,OAAOJ,KAAMe,WAAWlB,GAAG,CAAC,GAAGO,IAAIzB,GAAG,CAAC,EAAE,EAAEyB,IAAI3C,MAAM,CAAC,EAAE,EAAE2C,IAAI1C,KAAK,EAAE,EAAE0C;IACpF;IACA,qFAAqF;IACrF,6CAA6C;IAC7C,MAAMa,YAAY,CAACC,QAAgBC;QACjC,IAAK,IAAIC,IAAI,GAAGA,IAAID,KAAKE,MAAM,EAAED,KAAK,IAAK;YACzC,MAAME,QAAQH,KAAKI,KAAK,CAACH,GAAGA,IAAI;YAChC,MAAMI,eAAeF,MAAM9B,GAAG,CAAC,IAAM,KAAKP,IAAI,CAAC;YAC/C+B,QAAQjB,GAAGG,OAAO,CAAC,CAAC,gDAAgD,EAAEgB,OAAO,KAAK,EAAEM,aAAa,CAAC,CAAC,EAAEhB,GAAG,IAAIc;QAC9G;IACF;IAEAL,UAAU,OAAOP,MAAMe,QAAQ;IAC/BR,UAAU,eAAe;WAAIN;KAAiB;IAC9CM,UAAU,OAAOP,MAAMI,QAAQ;IAE/B,OAAOhB,YAAYC,IAAI;WAAIgB,WAAWW,MAAM;KAAG,EAAE9C,SAASC;AAC5D;AAEA,6DAA6D;AAC7D,OAAO,SAAS8C,UAAU5B,EAAgB;IACxC,0FAA0F;IAC1F,yFAAyF;IACzF,yFAAyF;IACzF,0FAA0F;IAC1F,iFAAiF;IACjF,MAAM6B,MAAM,CAAC;;;4GAG6F,CAAC;IAC3G,OAAO,AAAC7B,GAAGG,OAAO,CAAC0B,KAAKpB,GAAG,GAA2ChB,GAAG,CAAC,CAACqC,IAAM;YAACA,EAAElD,GAAG;YAAEkD,EAAExB,GAAG;SAAC;AACjG;AAEA,4FAA4F;AAC5F,+FAA+F;AAC/F,MAAMyB,mBAAmB,IAAIC;AAE7B,SAASC,cAActB,KAAqB,EAAEnC,IAAY;IACxD,IAAIsB,MAAMiC,iBAAiB1C,GAAG,CAACsB;IAC/B,IAAI,CAACb,KAAK;QACRA,MAAM,IAAIvC;QACVwE,iBAAiBjC,GAAG,CAACa,OAAOb;IAC9B;IACAA,IAAIrC,GAAG,CAACe;AACV;AAEA,qFAAqF;AACrF,MAAM0D,eAAe,IAAIF;AACzB,SAASG,YAAYxB,KAAqB;IACxC,IAAIb,MAAMoC,aAAa7C,GAAG,CAACsB;IAC3B,IAAI,CAACb,KAAK;QACRA,MAAM,IAAIvC,IAAIoD,MAAMI,QAAQ;QAC5BmB,aAAapC,GAAG,CAACa,OAAOb;IAC1B;IACA,OAAOA;AACT;AAEA,OAAO,MAAMsC,QAAiB;IAC5BC,MAAM;IACNC,QAAOtC,EAAE;QACPA,GAAGuC,IAAI,CAAC;QACRvC,GAAGuC,IAAI,CAAC;QACRvC,GAAGuC,IAAI,CAAC;IACV;IACApF;IACA,gFAAgF;IAChF,uFAAuF;IACvF,kFAAkF;IAClFqF,QAAOxC,EAAE,EAAExB,IAAI,EAAEmC,KAAK;QACpB,IAAI,CAACwB,YAAYxB,OAAO9C,GAAG,CAACW,OAAO;QACnC,MAAMiE,SAASzC,GAAGG,OAAO,CAAC,mCAAmCI,GAAG,CAAC/B;QACjE,IAAIkE,OAAOD,OAAOE,OAAO,IAAI,GAAGV,cAActB,OAAOnC;IACvD;IACAoE,OAAM5C,EAAE,EAAExB,IAAI,EAAEqE,SAAS,EAAElC,KAAK;QAC9B,MAAMmC,UAAUD;QAChB,2FAA2F;QAC3F,oEAAoE;QACpE,IAAIE;QACJ,IAAID,QAAQxB,MAAM,KAAK,GAAG;YACxByB,QAAQ/C,GAAGG,OAAO,CAAC,mCAAmCI,GAAG,CAAC/B;QAC5D,OAAO;YACL,MAAMiD,eAAeqB,QAAQrD,GAAG,CAAC,IAAM,KAAKP,IAAI,CAAC;YACjD,MAAMkC,OAAO0B,QAAQrD,GAAG,CAAC,CAACuD,IAAM,GAAGA,EAAEtF,MAAM,CAAC,EAAE,EAAEsF,EAAErF,KAAK,GAAG,MAAM,KAAK;YACrEoF,QAAQ/C,GAAGG,OAAO,CAAC,CAAC,yEAAyE,EAAEsB,aAAa,CAAC,CAAC,EAAElB,GAAG,CAAC/B,SAAS4C;QAC/H;QACA,IAAIsB,OAAOK,MAAMJ,OAAO,IAAI,GAAGV,cAActB,OAAOnC;QACpD,oFAAoF;QACpF,sEAAsE;QACtE,MAAMyE,SAASjD,GAAGG,OAAO,CAAC;QAC1B,KAAK,MAAM,EAAEzC,MAAM,EAAEC,KAAK,EAAE,IAAImF,QAASG,OAAO1C,GAAG,CAAC/B,MAAMd,QAAQa,QAAQF,YAAYX,UAAUC,QAAQ,IAAI;IAC9G;IACAuF,gBAAelD,EAAE,EAAEW,KAAK;;YAIEoB;QAHxB,mFAAmF;QACnF,+EAA+E;QAC/E,iEAAiE;QACjE,MAAMoB,iBAAiB,UAACpB,wBAAAA,iBAAiB1C,GAAG,CAACsB,oBAArBoB,4CAAAA,sBAA6BqB,IAAI,uCAAI,KAAK;QAElE,MAAMC,QAAQ1C,MAAMe,QAAQ,CAACJ,MAAM,GAAGX,MAAMI,QAAQ,CAACO,MAAM;QAC3D,+EAA+E;QAC/E,iFAAiF;QACjF,MAAMgC,QAAQ3C,MAAMpB,KAAK,CAAC+B,MAAM,KAAK,KAAK+B,QAAQ,MAAM1C,MAAMpB,KAAK,CAAC+B,MAAM;QAE1E,MAAMiC,aAAaD,QAAQ9C,WAAWR,IAAIW,MAAMpB,KAAK,IAAImB,mBAAmBV,IAAIW;QAChFA,MAAM6C,YAAY,GAAGD,cAAcJ;IACrC;AACF,EAAE"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport { maskRegions } from '../fences.ts';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\n// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #\n// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise\n// the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","maskRegions","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","seen","Set","results","add","embed","key","has","push","m","matchAll","index","dest","includes","value","Object","values","item","Array","isArray","exec","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","p","added","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","linkEdges","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","links","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fenceTracker } from '../fences.js';
|
|
1
|
+
import { commentTracker, fenceTracker, maskCodeSpans } from '../fences.js';
|
|
2
2
|
// tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline
|
|
3
3
|
// #tags from the prose, deduplicated, source not distinguished. Nested tags store full
|
|
4
4
|
// (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` is how a caller matches the parent too.
|
|
@@ -109,53 +109,12 @@ function frontmatterTags(data) {
|
|
|
109
109
|
}
|
|
110
110
|
return found;
|
|
111
111
|
}
|
|
112
|
-
// A code span opens on a run of N backticks and closes at the next run of exactly N -- a
|
|
113
|
-
// shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).
|
|
114
|
-
// Masked with spaces so column positions and tag-boundary whitespace are unaffected.
|
|
115
|
-
function maskCodeSpans(line) {
|
|
116
|
-
let out = '';
|
|
117
|
-
let i = 0;
|
|
118
|
-
while(i < line.length){
|
|
119
|
-
if (line[i] !== '`') {
|
|
120
|
-
out += line[i];
|
|
121
|
-
i++;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
let j = i;
|
|
125
|
-
while(line[j] === '`')j++;
|
|
126
|
-
const n = j - i;
|
|
127
|
-
let k = j;
|
|
128
|
-
let closeStart = -1;
|
|
129
|
-
let closeEnd = -1;
|
|
130
|
-
while(k < line.length){
|
|
131
|
-
if (line[k] !== '`') {
|
|
132
|
-
k++;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
let m = k;
|
|
136
|
-
while(line[m] === '`')m++;
|
|
137
|
-
if (m - k === n) {
|
|
138
|
-
closeStart = k;
|
|
139
|
-
closeEnd = m;
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
k = m;
|
|
143
|
-
}
|
|
144
|
-
if (closeStart >= 0) {
|
|
145
|
-
out += ' '.repeat(closeEnd - i);
|
|
146
|
-
i = closeEnd;
|
|
147
|
-
} else {
|
|
148
|
-
out += line.slice(i, j);
|
|
149
|
-
i = j;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return out;
|
|
153
|
-
}
|
|
154
112
|
// #tag tokens outside fenced code blocks, inline code spans, wikilinks, HTML tags, HTML blocks,
|
|
155
|
-
// and link destinations.
|
|
113
|
+
// <!-- --> comments, and link destinations.
|
|
156
114
|
function inlineTags(body) {
|
|
157
115
|
const found = [];
|
|
158
116
|
const fence = fenceTracker();
|
|
117
|
+
const comment = commentTracker();
|
|
159
118
|
let inHtmlBlock = false;
|
|
160
119
|
let htmlBlockClose = null; // set while inside a type-1 (script/pre/style/textarea) block
|
|
161
120
|
for (const line of body.split('\n')){
|
|
@@ -171,11 +130,17 @@ function inlineTags(body) {
|
|
|
171
130
|
}
|
|
172
131
|
continue;
|
|
173
132
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
133
|
+
if (!comment.inComment) {
|
|
134
|
+
if (fence.feed(line)) continue;
|
|
135
|
+
if (fence.inFence) continue;
|
|
136
|
+
}
|
|
137
|
+
// Obsidian parses nothing inside a <!-- --> comment; mask its span (which may open, close,
|
|
138
|
+
// or run the whole line) before any other check sees this line's text. Skipped when there
|
|
139
|
+
// is no comment marker anywhere near this line -- most lines, so worth the branch.
|
|
140
|
+
const masked = comment.inComment || line.includes('<!--') ? comment.mask(line) : line;
|
|
176
141
|
// Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the
|
|
177
142
|
// opener test runs after stripping leading whitespace and > markers.
|
|
178
|
-
const stripped =
|
|
143
|
+
const stripped = masked.replace(/^[ \t>]*/, '');
|
|
179
144
|
if (stripped[0] === '<') {
|
|
180
145
|
const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);
|
|
181
146
|
if (openMatch) {
|
|
@@ -193,8 +158,8 @@ function inlineTags(body) {
|
|
|
193
158
|
}
|
|
194
159
|
}
|
|
195
160
|
}
|
|
196
|
-
if (!
|
|
197
|
-
let cleaned =
|
|
161
|
+
if (!masked.includes('#')) continue; // most lines; skip the regex work
|
|
162
|
+
let cleaned = masked.includes('`') ? maskCodeSpans(masked) : masked;
|
|
198
163
|
if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m)=>' '.repeat(m.length));
|
|
199
164
|
if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m)=>' '.repeat(m.length));
|
|
200
165
|
if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m)=>' '.repeat(m.length));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/tags.ts"],"sourcesContent":["import { fenceTracker } from '../fences.ts';\nimport type { Feature } from './types.ts';\n\n// tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline\n// #tags from the prose, deduplicated, source not distinguished. Nested tags store full\n// (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` is how a caller matches the parent too.\n\n// Obsidian treats [[#Heading]] as a same-note link, not a tag.\nconst WIKILINK_RE = /\\[\\[.*?\\]\\]/g; // to the first ]], so a heading holding a lone ] still masks\n// Obsidian doesn't read tags inside HTML markup.\nconst HTML_TAG_RE = /<\\/?[a-zA-Z][^>]*>/g; // tag-shaped only: a comparison's `< 5` must not open a span\n// Anchors on start-of-line or a preceding whitespace/(/[ so `a#b` and URL fragments don't count.\nconst INLINE_TAG_RE = /(?:^|[\\s([])#([\\p{L}\\p{N}_/-]+)/gu;\n// A markdown link destination `](...)` -- `[text](#anchor)` is a same-page link, not a tag.\nconst LINK_DEST_RE = /\\]\\((?:[^()]|\\([^()]*\\))*\\)/g; // one paren-nesting level, as CommonMark destinations allow: (https://x/a_(b)#frag)\n\n// CommonMark's HTML-block type-6 list (fixed by the spec, not a drifting enumeration): a line\n// starting with an open or close tag of one of these, at column 0, opens a block that swallows\n// following lines -- including any #tag in them -- until a blank line closes it.\nconst HTML_BLOCK_TAGS = new Set([\n 'address',\n 'article',\n 'aside',\n 'base',\n 'basefont',\n 'blockquote',\n 'body',\n 'caption',\n 'center',\n 'col',\n 'colgroup',\n 'dd',\n 'details',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'footer',\n 'form',\n 'frame',\n 'frameset',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hr',\n 'html',\n 'iframe',\n 'legend',\n 'li',\n 'link',\n 'main',\n 'menu',\n 'menuitem',\n 'nav',\n 'noframes',\n 'ol',\n 'optgroup',\n 'option',\n 'p',\n 'param',\n 'search',\n 'section',\n 'summary',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'title',\n 'tr',\n 'track',\n 'ul',\n]);\n// Type-1 blocks (script/pre/style/textarea): closes on the line holding the matching end tag,\n// not on a blank line, and that line is the last one skipped.\nconst HTML_PRE_TAGS = new Set(['script', 'pre', 'style', 'textarea']);\n// An opening or closing tag at column 0, tag name captured for the lookups above.\nconst HTML_BLOCK_OPEN_RE = /^<\\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \\t]|\\/?>|$)/;\n\n// Strips a leading # (frontmatter entries may carry one) and a trailing /; rejects an\n// all-digit result -- a tag needs at least one non-digit character.\nfunction normalizeTag(raw: string): string | null {\n const stripped = raw.replace(/^#/, '').replace(/\\/+$/, '');\n if (!stripped || /^\\d+$/.test(stripped)) return null;\n return stripped;\n}\n\n// data.tags: a YAML list (Obsidian also accepts a bare string). Null members and non-string\n// members are skipped rather than throwing -- `tags:\\n -` parses to [null].\nfunction frontmatterTags(data?: Record<string, unknown>): string[] {\n const raw = data?.tags;\n const list = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const found: string[] = [];\n for (const item of list) {\n if (typeof item !== 'string') continue;\n const tag = normalizeTag(item);\n if (tag) found.push(tag);\n }\n return found;\n}\n\n// A code span opens on a run of N backticks and closes at the next run of exactly N -- a\n// shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).\n// Masked with spaces so column positions and tag-boundary whitespace are unaffected.\nfunction maskCodeSpans(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (line[i] !== '`') {\n out += line[i];\n i++;\n continue;\n }\n let j = i;\n while (line[j] === '`') j++;\n const n = j - i;\n let k = j;\n let closeStart = -1;\n let closeEnd = -1;\n while (k < line.length) {\n if (line[k] !== '`') {\n k++;\n continue;\n }\n let m = k;\n while (line[m] === '`') m++;\n if (m - k === n) {\n closeStart = k;\n closeEnd = m;\n break;\n }\n k = m;\n }\n if (closeStart >= 0) {\n out += ' '.repeat(closeEnd - i);\n i = closeEnd;\n } else {\n out += line.slice(i, j);\n i = j;\n }\n }\n return out;\n}\n\n// #tag tokens outside fenced code blocks, inline code spans, wikilinks, HTML tags, HTML blocks,\n// and link destinations.\nfunction inlineTags(body: string): string[] {\n const found: string[] = [];\n const fence = fenceTracker();\n let inHtmlBlock = false;\n let htmlBlockClose: RegExp | null = null; // set while inside a type-1 (script/pre/style/textarea) block\n for (const line of body.split('\\n')) {\n if (inHtmlBlock) {\n // A fence-like line here is still HTML-block content -- the block wins until it closes.\n if (htmlBlockClose) {\n if (htmlBlockClose.test(line)) {\n inHtmlBlock = false;\n htmlBlockClose = null;\n }\n } else if (/^[ \\t>]*$/.test(line)) {\n inHtmlBlock = false;\n }\n continue;\n }\n if (fence.feed(line)) continue;\n if (fence.inFence) continue;\n // Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the\n // opener test runs after stripping leading whitespace and > markers.\n const stripped = line.replace(/^[ \\t>]*/, '');\n if (stripped[0] === '<') {\n const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);\n if (openMatch) {\n const tagName = openMatch[1].toLowerCase();\n const isClosingTag = stripped[1] === '/';\n if (!isClosingTag && HTML_PRE_TAGS.has(tagName)) {\n inHtmlBlock = true;\n // Any of the four type-1 closers ends the block, not only the tag that opened it.\n htmlBlockClose = /<\\/(?:script|pre|style|textarea)>/i;\n continue;\n }\n if (HTML_BLOCK_TAGS.has(tagName)) {\n inHtmlBlock = true;\n continue;\n }\n }\n }\n if (!line.includes('#')) continue; // most lines; skip the regex work\n let cleaned = line.includes('`') ? maskCodeSpans(line) : line;\n if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m) => ' '.repeat(m.length));\n for (const m of cleaned.matchAll(INLINE_TAG_RE)) {\n const tag = normalizeTag(m[1]);\n if (tag) found.push(tag);\n }\n }\n return found;\n}\n\nfunction extract(_raw: string, body: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): string[] {\n return [...new Set([...frontmatterTags(data), ...inlineTags(body)])].sort();\n}\n\nexport const tags: Feature = {\n name: 'tags',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS tags (\"path\" TEXT, tag TEXT, PRIMARY KEY (\"path\", tag))');\n db.exec('CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag)');\n },\n extract,\n // Per-file rows with nothing else to resolve, so only a vanished file needs a delete here;\n // store() below handles a reparse's stale rows itself.\n remove(db, path, delta) {\n if (!delta.vanished.includes(path)) return;\n db.prepare('DELETE FROM tags WHERE \"path\" = ?').run(path);\n },\n store(db, path, extracted) {\n const found = extracted as string[];\n if (found.length === 0) {\n db.prepare('DELETE FROM tags WHERE \"path\" = ?').run(path);\n } else {\n const placeholders = found.map(() => '?').join(', ');\n db.prepare(`DELETE FROM tags WHERE \"path\" = ? AND tag NOT IN (${placeholders})`).run(path, ...found);\n }\n const insert = db.prepare('INSERT OR IGNORE INTO tags (\"path\", tag) VALUES (?, ?)');\n for (const tag of found) insert.run(path, tag);\n },\n};\n"],"names":["fenceTracker","WIKILINK_RE","HTML_TAG_RE","INLINE_TAG_RE","LINK_DEST_RE","HTML_BLOCK_TAGS","Set","HTML_PRE_TAGS","HTML_BLOCK_OPEN_RE","normalizeTag","raw","stripped","replace","test","frontmatterTags","data","tags","list","Array","isArray","found","item","tag","push","maskCodeSpans","line","out","i","length","j","n","k","closeStart","closeEnd","m","repeat","slice","inlineTags","body","fence","inHtmlBlock","htmlBlockClose","split","feed","inFence","openMatch","exec","tagName","toLowerCase","isClosingTag","has","includes","cleaned","matchAll","extract","_raw","_search","sort","name","schema","db","remove","path","delta","vanished","prepare","run","store","extracted","placeholders","map","join","insert"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/tags.ts"],"sourcesContent":["import { commentTracker, fenceTracker, maskCodeSpans } from '../fences.ts';\nimport type { Feature } from './types.ts';\n\n// tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline\n// #tags from the prose, deduplicated, source not distinguished. Nested tags store full\n// (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` is how a caller matches the parent too.\n\n// Obsidian treats [[#Heading]] as a same-note link, not a tag.\nconst WIKILINK_RE = /\\[\\[.*?\\]\\]/g; // to the first ]], so a heading holding a lone ] still masks\n// Obsidian doesn't read tags inside HTML markup.\nconst HTML_TAG_RE = /<\\/?[a-zA-Z][^>]*>/g; // tag-shaped only: a comparison's `< 5` must not open a span\n// Anchors on start-of-line or a preceding whitespace/(/[ so `a#b` and URL fragments don't count.\nconst INLINE_TAG_RE = /(?:^|[\\s([])#([\\p{L}\\p{N}_/-]+)/gu;\n// A markdown link destination `](...)` -- `[text](#anchor)` is a same-page link, not a tag.\nconst LINK_DEST_RE = /\\]\\((?:[^()]|\\([^()]*\\))*\\)/g; // one paren-nesting level, as CommonMark destinations allow: (https://x/a_(b)#frag)\n\n// CommonMark's HTML-block type-6 list (fixed by the spec, not a drifting enumeration): a line\n// starting with an open or close tag of one of these, at column 0, opens a block that swallows\n// following lines -- including any #tag in them -- until a blank line closes it.\nconst HTML_BLOCK_TAGS = new Set([\n 'address',\n 'article',\n 'aside',\n 'base',\n 'basefont',\n 'blockquote',\n 'body',\n 'caption',\n 'center',\n 'col',\n 'colgroup',\n 'dd',\n 'details',\n 'dialog',\n 'dir',\n 'div',\n 'dl',\n 'dt',\n 'fieldset',\n 'figcaption',\n 'figure',\n 'footer',\n 'form',\n 'frame',\n 'frameset',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'header',\n 'hr',\n 'html',\n 'iframe',\n 'legend',\n 'li',\n 'link',\n 'main',\n 'menu',\n 'menuitem',\n 'nav',\n 'noframes',\n 'ol',\n 'optgroup',\n 'option',\n 'p',\n 'param',\n 'search',\n 'section',\n 'summary',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'title',\n 'tr',\n 'track',\n 'ul',\n]);\n// Type-1 blocks (script/pre/style/textarea): closes on the line holding the matching end tag,\n// not on a blank line, and that line is the last one skipped.\nconst HTML_PRE_TAGS = new Set(['script', 'pre', 'style', 'textarea']);\n// An opening or closing tag at column 0, tag name captured for the lookups above.\nconst HTML_BLOCK_OPEN_RE = /^<\\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \\t]|\\/?>|$)/;\n\n// Strips a leading # (frontmatter entries may carry one) and a trailing /; rejects an\n// all-digit result -- a tag needs at least one non-digit character.\nfunction normalizeTag(raw: string): string | null {\n const stripped = raw.replace(/^#/, '').replace(/\\/+$/, '');\n if (!stripped || /^\\d+$/.test(stripped)) return null;\n return stripped;\n}\n\n// data.tags: a YAML list (Obsidian also accepts a bare string). Null members and non-string\n// members are skipped rather than throwing -- `tags:\\n -` parses to [null].\nfunction frontmatterTags(data?: Record<string, unknown>): string[] {\n const raw = data?.tags;\n const list = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];\n const found: string[] = [];\n for (const item of list) {\n if (typeof item !== 'string') continue;\n const tag = normalizeTag(item);\n if (tag) found.push(tag);\n }\n return found;\n}\n\n// #tag tokens outside fenced code blocks, inline code spans, wikilinks, HTML tags, HTML blocks,\n// <!-- --> comments, and link destinations.\nfunction inlineTags(body: string): string[] {\n const found: string[] = [];\n const fence = fenceTracker();\n const comment = commentTracker();\n let inHtmlBlock = false;\n let htmlBlockClose: RegExp | null = null; // set while inside a type-1 (script/pre/style/textarea) block\n for (const line of body.split('\\n')) {\n if (inHtmlBlock) {\n // A fence-like line here is still HTML-block content -- the block wins until it closes.\n if (htmlBlockClose) {\n if (htmlBlockClose.test(line)) {\n inHtmlBlock = false;\n htmlBlockClose = null;\n }\n } else if (/^[ \\t>]*$/.test(line)) {\n inHtmlBlock = false;\n }\n continue;\n }\n if (!comment.inComment) {\n if (fence.feed(line)) continue;\n if (fence.inFence) continue;\n }\n // Obsidian parses nothing inside a <!-- --> comment; mask its span (which may open, close,\n // or run the whole line) before any other check sees this line's text. Skipped when there\n // is no comment marker anywhere near this line -- most lines, so worth the branch.\n const masked = comment.inComment || line.includes('<!--') ? comment.mask(line) : line;\n // Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the\n // opener test runs after stripping leading whitespace and > markers.\n const stripped = masked.replace(/^[ \\t>]*/, '');\n if (stripped[0] === '<') {\n const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);\n if (openMatch) {\n const tagName = openMatch[1].toLowerCase();\n const isClosingTag = stripped[1] === '/';\n if (!isClosingTag && HTML_PRE_TAGS.has(tagName)) {\n inHtmlBlock = true;\n // Any of the four type-1 closers ends the block, not only the tag that opened it.\n htmlBlockClose = /<\\/(?:script|pre|style|textarea)>/i;\n continue;\n }\n if (HTML_BLOCK_TAGS.has(tagName)) {\n inHtmlBlock = true;\n continue;\n }\n }\n }\n if (!masked.includes('#')) continue; // most lines; skip the regex work\n let cleaned = masked.includes('`') ? maskCodeSpans(masked) : masked;\n if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m) => ' '.repeat(m.length));\n if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m) => ' '.repeat(m.length));\n for (const m of cleaned.matchAll(INLINE_TAG_RE)) {\n const tag = normalizeTag(m[1]);\n if (tag) found.push(tag);\n }\n }\n return found;\n}\n\nfunction extract(_raw: string, body: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): string[] {\n return [...new Set([...frontmatterTags(data), ...inlineTags(body)])].sort();\n}\n\nexport const tags: Feature = {\n name: 'tags',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS tags (\"path\" TEXT, tag TEXT, PRIMARY KEY (\"path\", tag))');\n db.exec('CREATE INDEX IF NOT EXISTS tags_tag ON tags(tag)');\n },\n extract,\n // Per-file rows with nothing else to resolve, so only a vanished file needs a delete here;\n // store() below handles a reparse's stale rows itself.\n remove(db, path, delta) {\n if (!delta.vanished.includes(path)) return;\n db.prepare('DELETE FROM tags WHERE \"path\" = ?').run(path);\n },\n store(db, path, extracted) {\n const found = extracted as string[];\n if (found.length === 0) {\n db.prepare('DELETE FROM tags WHERE \"path\" = ?').run(path);\n } else {\n const placeholders = found.map(() => '?').join(', ');\n db.prepare(`DELETE FROM tags WHERE \"path\" = ? AND tag NOT IN (${placeholders})`).run(path, ...found);\n }\n const insert = db.prepare('INSERT OR IGNORE INTO tags (\"path\", tag) VALUES (?, ?)');\n for (const tag of found) insert.run(path, tag);\n },\n};\n"],"names":["commentTracker","fenceTracker","maskCodeSpans","WIKILINK_RE","HTML_TAG_RE","INLINE_TAG_RE","LINK_DEST_RE","HTML_BLOCK_TAGS","Set","HTML_PRE_TAGS","HTML_BLOCK_OPEN_RE","normalizeTag","raw","stripped","replace","test","frontmatterTags","data","tags","list","Array","isArray","found","item","tag","push","inlineTags","body","fence","comment","inHtmlBlock","htmlBlockClose","line","split","inComment","feed","inFence","masked","includes","mask","openMatch","exec","tagName","toLowerCase","isClosingTag","has","cleaned","m","repeat","length","matchAll","extract","_raw","_search","sort","name","schema","db","remove","path","delta","vanished","prepare","run","store","extracted","placeholders","map","join","insert"],"mappings":"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"}
|
package/dist/esm/fences.d.ts
CHANGED
|
@@ -3,3 +3,10 @@ export interface FenceTracker {
|
|
|
3
3
|
readonly inFence: boolean;
|
|
4
4
|
}
|
|
5
5
|
export declare function fenceTracker(): FenceTracker;
|
|
6
|
+
export interface CommentTracker {
|
|
7
|
+
mask(line: string): string;
|
|
8
|
+
readonly inComment: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function commentTracker(): CommentTracker;
|
|
11
|
+
export declare function maskRegions(body: string): string;
|
|
12
|
+
export declare function maskCodeSpans(line: string): string;
|
package/dist/esm/fences.js
CHANGED
|
@@ -41,3 +41,98 @@ export function fenceTracker() {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
+
export function commentTracker() {
|
|
45
|
+
let inComment = false;
|
|
46
|
+
return {
|
|
47
|
+
mask (line) {
|
|
48
|
+
let out = '';
|
|
49
|
+
let i = 0;
|
|
50
|
+
while(i < line.length){
|
|
51
|
+
if (inComment) {
|
|
52
|
+
const end = line.indexOf('-->', i);
|
|
53
|
+
if (end === -1) {
|
|
54
|
+
out += ' '.repeat(line.length - i);
|
|
55
|
+
i = line.length;
|
|
56
|
+
} else {
|
|
57
|
+
out += ' '.repeat(end + 3 - i);
|
|
58
|
+
i = end + 3;
|
|
59
|
+
inComment = false;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const start = line.indexOf('<!--', i);
|
|
64
|
+
if (start === -1) {
|
|
65
|
+
out += line.slice(i);
|
|
66
|
+
i = line.length;
|
|
67
|
+
} else {
|
|
68
|
+
out += `${line.slice(i, start)} `;
|
|
69
|
+
i = start + 4;
|
|
70
|
+
inComment = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
},
|
|
75
|
+
get inComment () {
|
|
76
|
+
return inComment;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// Body with fenced-code regions (delimiter and content lines) and <!-- --> comment spans
|
|
81
|
+
// replaced by spaces, newlines preserved so line numbers and offsets survive. Bodies without a
|
|
82
|
+
// backtick/tilde/`<!--` fast-path out untouched.
|
|
83
|
+
export function maskRegions(body) {
|
|
84
|
+
if (!/[`~]/.test(body) && !body.includes('<!--')) return body;
|
|
85
|
+
const fence = fenceTracker();
|
|
86
|
+
const comment = commentTracker();
|
|
87
|
+
return body.split('\n').map((line)=>{
|
|
88
|
+
if (!comment.inComment) {
|
|
89
|
+
const isDelim = fence.feed(line);
|
|
90
|
+
if (isDelim || fence.inFence) return ' '.repeat(line.length);
|
|
91
|
+
}
|
|
92
|
+
const uncommented = comment.mask(line);
|
|
93
|
+
// Inline code spans hide their content too: `[[x]]` in prose is not a link.
|
|
94
|
+
return uncommented.includes('`') ? maskCodeSpans(uncommented) : uncommented;
|
|
95
|
+
}).join('\n');
|
|
96
|
+
}
|
|
97
|
+
// A code span opens on a run of N backticks and closes at the next run of exactly N -- a
|
|
98
|
+
// shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).
|
|
99
|
+
// Masked with spaces so column positions and tag-boundary whitespace are unaffected.
|
|
100
|
+
export function maskCodeSpans(line) {
|
|
101
|
+
let out = '';
|
|
102
|
+
let i = 0;
|
|
103
|
+
while(i < line.length){
|
|
104
|
+
if (line[i] !== '`') {
|
|
105
|
+
out += line[i];
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let j = i;
|
|
110
|
+
while(line[j] === '`')j++;
|
|
111
|
+
const n = j - i;
|
|
112
|
+
let k = j;
|
|
113
|
+
let closeStart = -1;
|
|
114
|
+
let closeEnd = -1;
|
|
115
|
+
while(k < line.length){
|
|
116
|
+
if (line[k] !== '`') {
|
|
117
|
+
k++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
let m = k;
|
|
121
|
+
while(line[m] === '`')m++;
|
|
122
|
+
if (m - k === n) {
|
|
123
|
+
closeStart = k;
|
|
124
|
+
closeEnd = m;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
k = m;
|
|
128
|
+
}
|
|
129
|
+
if (closeStart >= 0) {
|
|
130
|
+
out += ' '.repeat(closeEnd - i);
|
|
131
|
+
i = closeEnd;
|
|
132
|
+
} else {
|
|
133
|
+
out += line.slice(i, j);
|
|
134
|
+
i = j;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
package/dist/esm/fences.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/fences.ts"],"sourcesContent":["// Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column\n// 0 -- no indent allowance, a deliberate divergence from CommonMark's up-to-3-space rule (see\n// test/unit/fences.test.ts DIVERGENCES table). A backtick opener's info string must not itself\n// contain a backtick (spec rule). A closer is a run of the SAME character, length >= the\n// opener's, holding nothing but trailing spaces after the run.\n\nexport interface FenceTracker {\n feed(line: string): boolean; // true iff this line is a fence delimiter (open or close)\n readonly inFence: boolean;\n}\n\nconst BACKTICK_OPEN = /^(`{3,})(.*)$/;\nconst TILDE_OPEN = /^(~{3,})(.*)$/;\n\nexport function fenceTracker(): FenceTracker {\n let inFence = false;\n let fenceChar = '';\n let fenceLen = 0;\n return {\n feed(line: string): boolean {\n if (!inFence) {\n const bt = BACKTICK_OPEN.exec(line);\n if (bt && !bt[2].includes('`')) {\n inFence = true;\n fenceChar = '`';\n fenceLen = bt[1].length;\n return true;\n }\n const td = TILDE_OPEN.exec(line);\n if (td) {\n inFence = true;\n fenceChar = '~';\n fenceLen = td[1].length;\n return true;\n }\n return false;\n }\n const run = fenceChar === '`' ? /^(`+)(.*)$/ : /^(~+)(.*)$/;\n const m = run.exec(line);\n if (m && m[1].length >= fenceLen && m[2].trim() === '') {\n inFence = false;\n return true;\n }\n return false;\n },\n get inFence() {\n return inFence;\n },\n };\n}\n"],"names":["BACKTICK_OPEN","TILDE_OPEN","fenceTracker","inFence","fenceChar","fenceLen","feed","line","bt","exec","includes","length","td","run","m","trim"],"mappings":"AAAA,+FAA+F;AAC/F,8FAA8F;AAC9F,+FAA+F;AAC/F,yFAAyF;AACzF,+DAA+D;AAO/D,MAAMA,gBAAgB;AACtB,MAAMC,aAAa;AAEnB,OAAO,SAASC;IACd,IAAIC,UAAU;IACd,IAAIC,YAAY;IAChB,IAAIC,WAAW;IACf,OAAO;QACLC,MAAKC,IAAY;YACf,IAAI,CAACJ,SAAS;gBACZ,MAAMK,KAAKR,cAAcS,IAAI,CAACF;gBAC9B,IAAIC,MAAM,CAACA,EAAE,CAAC,EAAE,CAACE,QAAQ,CAAC,MAAM;oBAC9BP,UAAU;oBACVC,YAAY;oBACZC,WAAWG,EAAE,CAAC,EAAE,CAACG,MAAM;oBACvB,OAAO;gBACT;gBACA,MAAMC,KAAKX,WAAWQ,IAAI,CAACF;gBAC3B,IAAIK,IAAI;oBACNT,UAAU;oBACVC,YAAY;oBACZC,WAAWO,EAAE,CAAC,EAAE,CAACD,MAAM;oBACvB,OAAO;gBACT;gBACA,OAAO;YACT;YACA,MAAME,MAAMT,cAAc,MAAM,eAAe;YAC/C,MAAMU,IAAID,IAAIJ,IAAI,CAACF;YACnB,IAAIO,KAAKA,CAAC,CAAC,EAAE,CAACH,MAAM,IAAIN,YAAYS,CAAC,CAAC,EAAE,CAACC,IAAI,OAAO,IAAI;gBACtDZ,UAAU;gBACV,OAAO;YACT;YACA,OAAO;QACT;QACA,IAAIA,WAAU;YACZ,OAAOA;QACT;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/fences.ts"],"sourcesContent":["// Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column\n// 0 -- no indent allowance, a deliberate divergence from CommonMark's up-to-3-space rule (see\n// test/unit/fences.test.ts DIVERGENCES table). A backtick opener's info string must not itself\n// contain a backtick (spec rule). A closer is a run of the SAME character, length >= the\n// opener's, holding nothing but trailing spaces after the run.\n\nexport interface FenceTracker {\n feed(line: string): boolean; // true iff this line is a fence delimiter (open or close)\n readonly inFence: boolean;\n}\n\nconst BACKTICK_OPEN = /^(`{3,})(.*)$/;\nconst TILDE_OPEN = /^(~{3,})(.*)$/;\n\nexport function fenceTracker(): FenceTracker {\n let inFence = false;\n let fenceChar = '';\n let fenceLen = 0;\n return {\n feed(line: string): boolean {\n if (!inFence) {\n const bt = BACKTICK_OPEN.exec(line);\n if (bt && !bt[2].includes('`')) {\n inFence = true;\n fenceChar = '`';\n fenceLen = bt[1].length;\n return true;\n }\n const td = TILDE_OPEN.exec(line);\n if (td) {\n inFence = true;\n fenceChar = '~';\n fenceLen = td[1].length;\n return true;\n }\n return false;\n }\n const run = fenceChar === '`' ? /^(`+)(.*)$/ : /^(~+)(.*)$/;\n const m = run.exec(line);\n if (m && m[1].length >= fenceLen && m[2].trim() === '') {\n inFence = false;\n return true;\n }\n return false;\n },\n get inFence() {\n return inFence;\n },\n };\n}\n\n// Masks <!-- ... --> spans that may cross line boundaries, one line at a time so callers can\n// interleave it with their own per-line state. An unclosed <!-- masks to end of input, matching\n// CommonMark type-2 HTML comments' practical effect (everything after is swallowed).\nexport interface CommentTracker {\n mask(line: string): string;\n readonly inComment: boolean;\n}\n\nexport function commentTracker(): CommentTracker {\n let inComment = false;\n return {\n mask(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (inComment) {\n const end = line.indexOf('-->', i);\n if (end === -1) {\n out += ' '.repeat(line.length - i);\n i = line.length;\n } else {\n out += ' '.repeat(end + 3 - i);\n i = end + 3;\n inComment = false;\n }\n continue;\n }\n const start = line.indexOf('<!--', i);\n if (start === -1) {\n out += line.slice(i);\n i = line.length;\n } else {\n out += `${line.slice(i, start)} `;\n i = start + 4;\n inComment = true;\n }\n }\n return out;\n },\n get inComment() {\n return inComment;\n },\n };\n}\n\n// Body with fenced-code regions (delimiter and content lines) and <!-- --> comment spans\n// replaced by spaces, newlines preserved so line numbers and offsets survive. Bodies without a\n// backtick/tilde/`<!--` fast-path out untouched.\nexport function maskRegions(body: string): string {\n if (!/[`~]/.test(body) && !body.includes('<!--')) return body;\n const fence = fenceTracker();\n const comment = commentTracker();\n return body\n .split('\\n')\n .map((line) => {\n if (!comment.inComment) {\n const isDelim = fence.feed(line);\n if (isDelim || fence.inFence) return ' '.repeat(line.length);\n }\n const uncommented = comment.mask(line);\n // Inline code spans hide their content too: `[[x]]` in prose is not a link.\n return uncommented.includes('`') ? maskCodeSpans(uncommented) : uncommented;\n })\n .join('\\n');\n}\n\n// A code span opens on a run of N backticks and closes at the next run of exactly N -- a\n// shorter or longer run in between is literal text, not a delimiter (CommonMark code spans).\n// Masked with spaces so column positions and tag-boundary whitespace are unaffected.\nexport function maskCodeSpans(line: string): string {\n let out = '';\n let i = 0;\n while (i < line.length) {\n if (line[i] !== '`') {\n out += line[i];\n i++;\n continue;\n }\n let j = i;\n while (line[j] === '`') j++;\n const n = j - i;\n let k = j;\n let closeStart = -1;\n let closeEnd = -1;\n while (k < line.length) {\n if (line[k] !== '`') {\n k++;\n continue;\n }\n let m = k;\n while (line[m] === '`') m++;\n if (m - k === n) {\n closeStart = k;\n closeEnd = m;\n break;\n }\n k = m;\n }\n if (closeStart >= 0) {\n out += ' '.repeat(closeEnd - i);\n i = closeEnd;\n } else {\n out += line.slice(i, j);\n i = j;\n }\n }\n return out;\n}\n"],"names":["BACKTICK_OPEN","TILDE_OPEN","fenceTracker","inFence","fenceChar","fenceLen","feed","line","bt","exec","includes","length","td","run","m","trim","commentTracker","inComment","mask","out","i","end","indexOf","repeat","start","slice","maskRegions","body","test","fence","comment","split","map","isDelim","uncommented","maskCodeSpans","join","j","n","k","closeStart","closeEnd"],"mappings":"AAAA,+FAA+F;AAC/F,8FAA8F;AAC9F,+FAA+F;AAC/F,yFAAyF;AACzF,+DAA+D;AAO/D,MAAMA,gBAAgB;AACtB,MAAMC,aAAa;AAEnB,OAAO,SAASC;IACd,IAAIC,UAAU;IACd,IAAIC,YAAY;IAChB,IAAIC,WAAW;IACf,OAAO;QACLC,MAAKC,IAAY;YACf,IAAI,CAACJ,SAAS;gBACZ,MAAMK,KAAKR,cAAcS,IAAI,CAACF;gBAC9B,IAAIC,MAAM,CAACA,EAAE,CAAC,EAAE,CAACE,QAAQ,CAAC,MAAM;oBAC9BP,UAAU;oBACVC,YAAY;oBACZC,WAAWG,EAAE,CAAC,EAAE,CAACG,MAAM;oBACvB,OAAO;gBACT;gBACA,MAAMC,KAAKX,WAAWQ,IAAI,CAACF;gBAC3B,IAAIK,IAAI;oBACNT,UAAU;oBACVC,YAAY;oBACZC,WAAWO,EAAE,CAAC,EAAE,CAACD,MAAM;oBACvB,OAAO;gBACT;gBACA,OAAO;YACT;YACA,MAAME,MAAMT,cAAc,MAAM,eAAe;YAC/C,MAAMU,IAAID,IAAIJ,IAAI,CAACF;YACnB,IAAIO,KAAKA,CAAC,CAAC,EAAE,CAACH,MAAM,IAAIN,YAAYS,CAAC,CAAC,EAAE,CAACC,IAAI,OAAO,IAAI;gBACtDZ,UAAU;gBACV,OAAO;YACT;YACA,OAAO;QACT;QACA,IAAIA,WAAU;YACZ,OAAOA;QACT;IACF;AACF;AAUA,OAAO,SAASa;IACd,IAAIC,YAAY;IAChB,OAAO;QACLC,MAAKX,IAAY;YACf,IAAIY,MAAM;YACV,IAAIC,IAAI;YACR,MAAOA,IAAIb,KAAKI,MAAM,CAAE;gBACtB,IAAIM,WAAW;oBACb,MAAMI,MAAMd,KAAKe,OAAO,CAAC,OAAOF;oBAChC,IAAIC,QAAQ,CAAC,GAAG;wBACdF,OAAO,IAAII,MAAM,CAAChB,KAAKI,MAAM,GAAGS;wBAChCA,IAAIb,KAAKI,MAAM;oBACjB,OAAO;wBACLQ,OAAO,IAAII,MAAM,CAACF,MAAM,IAAID;wBAC5BA,IAAIC,MAAM;wBACVJ,YAAY;oBACd;oBACA;gBACF;gBACA,MAAMO,QAAQjB,KAAKe,OAAO,CAAC,QAAQF;gBACnC,IAAII,UAAU,CAAC,GAAG;oBAChBL,OAAOZ,KAAKkB,KAAK,CAACL;oBAClBA,IAAIb,KAAKI,MAAM;gBACjB,OAAO;oBACLQ,OAAO,GAAGZ,KAAKkB,KAAK,CAACL,GAAGI,OAAO,IAAI,CAAC;oBACpCJ,IAAII,QAAQ;oBACZP,YAAY;gBACd;YACF;YACA,OAAOE;QACT;QACA,IAAIF,aAAY;YACd,OAAOA;QACT;IACF;AACF;AAEA,yFAAyF;AACzF,+FAA+F;AAC/F,iDAAiD;AACjD,OAAO,SAASS,YAAYC,IAAY;IACtC,IAAI,CAAC,OAAOC,IAAI,CAACD,SAAS,CAACA,KAAKjB,QAAQ,CAAC,SAAS,OAAOiB;IACzD,MAAME,QAAQ3B;IACd,MAAM4B,UAAUd;IAChB,OAAOW,KACJI,KAAK,CAAC,MACNC,GAAG,CAAC,CAACzB;QACJ,IAAI,CAACuB,QAAQb,SAAS,EAAE;YACtB,MAAMgB,UAAUJ,MAAMvB,IAAI,CAACC;YAC3B,IAAI0B,WAAWJ,MAAM1B,OAAO,EAAE,OAAO,IAAIoB,MAAM,CAAChB,KAAKI,MAAM;QAC7D;QACA,MAAMuB,cAAcJ,QAAQZ,IAAI,CAACX;QACjC,4EAA4E;QAC5E,OAAO2B,YAAYxB,QAAQ,CAAC,OAAOyB,cAAcD,eAAeA;IAClE,GACCE,IAAI,CAAC;AACV;AAEA,yFAAyF;AACzF,6FAA6F;AAC7F,qFAAqF;AACrF,OAAO,SAASD,cAAc5B,IAAY;IACxC,IAAIY,MAAM;IACV,IAAIC,IAAI;IACR,MAAOA,IAAIb,KAAKI,MAAM,CAAE;QACtB,IAAIJ,IAAI,CAACa,EAAE,KAAK,KAAK;YACnBD,OAAOZ,IAAI,CAACa,EAAE;YACdA;YACA;QACF;QACA,IAAIiB,IAAIjB;QACR,MAAOb,IAAI,CAAC8B,EAAE,KAAK,IAAKA;QACxB,MAAMC,IAAID,IAAIjB;QACd,IAAImB,IAAIF;QACR,IAAIG,aAAa,CAAC;QAClB,IAAIC,WAAW,CAAC;QAChB,MAAOF,IAAIhC,KAAKI,MAAM,CAAE;YACtB,IAAIJ,IAAI,CAACgC,EAAE,KAAK,KAAK;gBACnBA;gBACA;YACF;YACA,IAAIzB,IAAIyB;YACR,MAAOhC,IAAI,CAACO,EAAE,KAAK,IAAKA;YACxB,IAAIA,IAAIyB,MAAMD,GAAG;gBACfE,aAAaD;gBACbE,WAAW3B;gBACX;YACF;YACAyB,IAAIzB;QACN;QACA,IAAI0B,cAAc,GAAG;YACnBrB,OAAO,IAAII,MAAM,CAACkB,WAAWrB;YAC7BA,IAAIqB;QACN,OAAO;YACLtB,OAAOZ,KAAKkB,KAAK,CAACL,GAAGiB;YACrBjB,IAAIiB;QACN;IACF;IACA,OAAOlB;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sensemaking",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
package/skills/sense/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: "Query a markdown tree with the sense CLI: filter notes by frontmat
|
|
|
5
5
|
|
|
6
6
|
# sense
|
|
7
7
|
|
|
8
|
-
SQL over a markdown tree, kept fresh by a filesystem check on every query. Every file becomes rows in `frontmatter` (one column per key, plus `path`/`_mtime`/`_ctime`/`_size`/`_rank`/`_parse_error`; `_ctime` is filesystem birthtime, which a clone or copy resets just like `_mtime`), `content` (FTS5: `title`, `summary`, `text`, `path`, plus machine-written `title_seg`/`summary_seg`/`text_seg` sidecars used for matching Chinese, Japanese, Thai, Khmer, Lao, and Burmese text, not for reading), `links` (`src`, `target`, `dst`, `embed`; `NULL` dst = dead link; `embed` 1 for `![[...]]` embeds, 0 for links; one row per distinct written target and kind with alias and anchor stripped, so `[[Foo]]` and `[[Foo|alias]]` are one row while `[[Foo]]` and `[[notes/Foo]]` are two rows that can share a `dst`, and a target both linked and embedded is a row of each kind), `tags` (`path`, `tag`: frontmatter and inline `#tags` merged and deduplicated; nested tags stored full, so `book/scifi` matches `tag = 'book' OR tag LIKE 'book/%'`), `sections` (heading outline with line ranges and token estimates), and `preset_files` (`path`, `preset`: which presets cover which files). Features add their own storage; `map` and `status` report which are on.
|
|
8
|
+
SQL over a markdown tree, kept fresh by a filesystem check on every query. Every file becomes rows in `frontmatter` (one column per key, plus `path`/`_mtime`/`_ctime`/`_size`/`_rank`/`_parse_error`; `_ctime` is filesystem birthtime, which a clone or copy resets just like `_mtime`), `content` (FTS5: `title`, `summary`, `text`, `path`, plus machine-written `title_seg`/`summary_seg`/`text_seg` sidecars used for matching Chinese, Japanese, Thai, Khmer, Lao, and Burmese text, not for reading), `links` (`src`, `target`, `dst`, `embed`; `NULL` dst = dead link; `embed` 1 for `![[...]]` embeds, 0 for links; one row per distinct written target and kind with alias and anchor stripped, so `[[Foo]]` and `[[Foo|alias]]` are one row while `[[Foo]]` and `[[notes/Foo]]` are two rows that can share a `dst`, and a target both linked and embedded is a row of each kind; extraction matches Obsidian's own graph: comments, code, and link-syntax text yield no rows, `[[#Anchor]]` is a self-edge, a frontmatter value that is exactly `[[X]]` is a link, and a basename collision resolves to the linking note itself, else the shortest path), `tags` (`path`, `tag`: frontmatter and inline `#tags` merged and deduplicated; nested tags stored full, so `book/scifi` matches `tag = 'book' OR tag LIKE 'book/%'`), `sections` (heading outline with line ranges and token estimates), and `preset_files` (`path`, `preset`: which presets cover which files). Features add their own storage; `map` and `status` report which are on.
|
|
9
9
|
|
|
10
10
|
## What each tool is for
|
|
11
11
|
|