sensemaking 0.14.0 → 0.15.0

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.
@@ -1,14 +1,92 @@
1
+ import { fenceTracker } from '../fences.js';
1
2
  // tags(path, tag): Obsidian's file.tags grain -- frontmatter list/string tags plus inline
2
3
  // #tags from the prose, deduplicated, source not distinguished. Nested tags store full
3
4
  // (book/scifi); `tag = 'book' OR tag LIKE 'book/%'` is how a caller matches the parent too.
4
- const FENCE_RE = /^(```|~~~)/;
5
- const INLINE_CODE_RE = /`[^`]*`/g;
6
5
  // Obsidian treats [[#Heading]] as a same-note link, not a tag.
7
6
  const WIKILINK_RE = /\[\[.*?\]\]/g; // to the first ]], so a heading holding a lone ] still masks
8
7
  // Obsidian doesn't read tags inside HTML markup.
9
8
  const HTML_TAG_RE = /<\/?[a-zA-Z][^>]*>/g; // tag-shaped only: a comparison's `< 5` must not open a span
10
9
  // Anchors on start-of-line or a preceding whitespace/(/[ so `a#b` and URL fragments don't count.
11
10
  const INLINE_TAG_RE = /(?:^|[\s([])#([\p{L}\p{N}_/-]+)/gu;
11
+ // A markdown link destination `](...)` -- `[text](#anchor)` is a same-page link, not a tag.
12
+ const LINK_DEST_RE = /\]\((?:[^()]|\([^()]*\))*\)/g; // one paren-nesting level, as CommonMark destinations allow: (https://x/a_(b)#frag)
13
+ // CommonMark's HTML-block type-6 list (fixed by the spec, not a drifting enumeration): a line
14
+ // starting with an open or close tag of one of these, at column 0, opens a block that swallows
15
+ // following lines -- including any #tag in them -- until a blank line closes it.
16
+ const HTML_BLOCK_TAGS = new Set([
17
+ 'address',
18
+ 'article',
19
+ 'aside',
20
+ 'base',
21
+ 'basefont',
22
+ 'blockquote',
23
+ 'body',
24
+ 'caption',
25
+ 'center',
26
+ 'col',
27
+ 'colgroup',
28
+ 'dd',
29
+ 'details',
30
+ 'dialog',
31
+ 'dir',
32
+ 'div',
33
+ 'dl',
34
+ 'dt',
35
+ 'fieldset',
36
+ 'figcaption',
37
+ 'figure',
38
+ 'footer',
39
+ 'form',
40
+ 'frame',
41
+ 'frameset',
42
+ 'h1',
43
+ 'h2',
44
+ 'h3',
45
+ 'h4',
46
+ 'h5',
47
+ 'h6',
48
+ 'head',
49
+ 'header',
50
+ 'hr',
51
+ 'html',
52
+ 'iframe',
53
+ 'legend',
54
+ 'li',
55
+ 'link',
56
+ 'main',
57
+ 'menu',
58
+ 'menuitem',
59
+ 'nav',
60
+ 'noframes',
61
+ 'ol',
62
+ 'optgroup',
63
+ 'option',
64
+ 'p',
65
+ 'param',
66
+ 'search',
67
+ 'section',
68
+ 'summary',
69
+ 'table',
70
+ 'tbody',
71
+ 'td',
72
+ 'tfoot',
73
+ 'th',
74
+ 'thead',
75
+ 'title',
76
+ 'tr',
77
+ 'track',
78
+ 'ul'
79
+ ]);
80
+ // Type-1 blocks (script/pre/style/textarea): closes on the line holding the matching end tag,
81
+ // not on a blank line, and that line is the last one skipped.
82
+ const HTML_PRE_TAGS = new Set([
83
+ 'script',
84
+ 'pre',
85
+ 'style',
86
+ 'textarea'
87
+ ]);
88
+ // An opening or closing tag at column 0, tag name captured for the lookups above.
89
+ const HTML_BLOCK_OPEN_RE = /^<\/?([a-zA-Z][a-zA-Z0-9]*)(?:[ \t]|\/?>|$)/;
12
90
  // Strips a leading # (frontmatter entries may carry one) and a trailing /; rejects an
13
91
  // all-digit result -- a tag needs at least one non-digit character.
14
92
  function normalizeTag(raw) {
@@ -31,19 +109,94 @@ function frontmatterTags(data) {
31
109
  }
32
110
  return found;
33
111
  }
34
- // #tag tokens outside fenced code blocks, inline code spans, wikilinks, and HTML tags.
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
+ // #tag tokens outside fenced code blocks, inline code spans, wikilinks, HTML tags, HTML blocks,
155
+ // and link destinations.
35
156
  function inlineTags(body) {
36
157
  const found = [];
37
- let inFence = false;
158
+ const fence = fenceTracker();
159
+ let inHtmlBlock = false;
160
+ let htmlBlockClose = null; // set while inside a type-1 (script/pre/style/textarea) block
38
161
  for (const line of body.split('\n')){
39
- if (FENCE_RE.test(line)) {
40
- inFence = !inFence;
162
+ if (inHtmlBlock) {
163
+ // A fence-like line here is still HTML-block content -- the block wins until it closes.
164
+ if (htmlBlockClose) {
165
+ if (htmlBlockClose.test(line)) {
166
+ inHtmlBlock = false;
167
+ htmlBlockClose = null;
168
+ }
169
+ } else if (/^[ \t>]*$/.test(line)) {
170
+ inHtmlBlock = false;
171
+ }
41
172
  continue;
42
173
  }
43
- if (inFence) continue;
174
+ if (fence.feed(line)) continue;
175
+ if (fence.inFence) continue;
176
+ // Indented or blockquoted HTML blocks still swallow their content in Obsidian, so the
177
+ // opener test runs after stripping leading whitespace and > markers.
178
+ const stripped = line.replace(/^[ \t>]*/, '');
179
+ if (stripped[0] === '<') {
180
+ const openMatch = HTML_BLOCK_OPEN_RE.exec(stripped);
181
+ if (openMatch) {
182
+ const tagName = openMatch[1].toLowerCase();
183
+ const isClosingTag = stripped[1] === '/';
184
+ if (!isClosingTag && HTML_PRE_TAGS.has(tagName)) {
185
+ inHtmlBlock = true;
186
+ // Any of the four type-1 closers ends the block, not only the tag that opened it.
187
+ htmlBlockClose = /<\/(?:script|pre|style|textarea)>/i;
188
+ continue;
189
+ }
190
+ if (HTML_BLOCK_TAGS.has(tagName)) {
191
+ inHtmlBlock = true;
192
+ continue;
193
+ }
194
+ }
195
+ }
44
196
  if (!line.includes('#')) continue; // most lines; skip the regex work
45
- let cleaned = line.includes('`') ? line.replace(INLINE_CODE_RE, (m)=>' '.repeat(m.length)) : line;
197
+ let cleaned = line.includes('`') ? maskCodeSpans(line) : line;
46
198
  if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_RE, (m)=>' '.repeat(m.length));
199
+ if (cleaned.includes('](')) cleaned = cleaned.replace(LINK_DEST_RE, (m)=>' '.repeat(m.length));
47
200
  if (cleaned.includes('<')) cleaned = cleaned.replace(HTML_TAG_RE, (m)=>' '.repeat(m.length));
48
201
  for (const m of cleaned.matchAll(INLINE_TAG_RE)){
49
202
  const tag = normalizeTag(m[1]);
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/tags.ts"],"sourcesContent":["import 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\nconst FENCE_RE = /^(```|~~~)/;\nconst INLINE_CODE_RE = /`[^`]*`/g;\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\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, and HTML tags.\nfunction inlineTags(body: string): string[] {\n const found: string[] = [];\n let inFence = false;\n for (const line of body.split('\\n')) {\n if (FENCE_RE.test(line)) {\n inFence = !inFence;\n continue;\n }\n if (inFence) continue;\n if (!line.includes('#')) continue; // most lines; skip the regex work\n let cleaned = line.includes('`') ? line.replace(INLINE_CODE_RE, (m) => ' '.repeat(m.length)) : line;\n if (cleaned.includes('[[')) cleaned = cleaned.replace(WIKILINK_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":["FENCE_RE","INLINE_CODE_RE","WIKILINK_RE","HTML_TAG_RE","INLINE_TAG_RE","normalizeTag","raw","stripped","replace","test","frontmatterTags","data","tags","list","Array","isArray","found","item","tag","push","inlineTags","body","inFence","line","split","includes","cleaned","m","repeat","length","matchAll","extract","_raw","_search","Set","sort","name","schema","db","exec","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 { 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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"}
@@ -0,0 +1,5 @@
1
+ export interface FenceTracker {
2
+ feed(line: string): boolean;
3
+ readonly inFence: boolean;
4
+ }
5
+ export declare function fenceTracker(): FenceTracker;
@@ -0,0 +1,43 @@
1
+ // Shared line-fence tracker for tags/sections/embed. Opener: >=3 backticks or tildes at column
2
+ // 0 -- no indent allowance, a deliberate divergence from CommonMark's up-to-3-space rule (see
3
+ // test/unit/fences.test.ts DIVERGENCES table). A backtick opener's info string must not itself
4
+ // contain a backtick (spec rule). A closer is a run of the SAME character, length >= the
5
+ // opener's, holding nothing but trailing spaces after the run.
6
+ const BACKTICK_OPEN = /^(`{3,})(.*)$/;
7
+ const TILDE_OPEN = /^(~{3,})(.*)$/;
8
+ export function fenceTracker() {
9
+ let inFence = false;
10
+ let fenceChar = '';
11
+ let fenceLen = 0;
12
+ return {
13
+ feed (line) {
14
+ if (!inFence) {
15
+ const bt = BACKTICK_OPEN.exec(line);
16
+ if (bt && !bt[2].includes('`')) {
17
+ inFence = true;
18
+ fenceChar = '`';
19
+ fenceLen = bt[1].length;
20
+ return true;
21
+ }
22
+ const td = TILDE_OPEN.exec(line);
23
+ if (td) {
24
+ inFence = true;
25
+ fenceChar = '~';
26
+ fenceLen = td[1].length;
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ const run = fenceChar === '`' ? /^(`+)(.*)$/ : /^(~+)(.*)$/;
32
+ const m = run.exec(line);
33
+ if (m && m[1].length >= fenceLen && m[2].trim() === '') {
34
+ inFence = false;
35
+ return true;
36
+ }
37
+ return false;
38
+ },
39
+ get inFence () {
40
+ return inFence;
41
+ }
42
+ };
43
+ }
@@ -0,0 +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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sensemaking",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
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",