@zenera/rag 1.1.9 → 1.1.11

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.
Files changed (62) hide show
  1. package/README.md +195 -10
  2. package/dist/command.js +2 -1
  3. package/dist/common/cache.d.ts +66 -0
  4. package/dist/common/cache.js +172 -0
  5. package/dist/common/embedder.d.ts +11 -1
  6. package/dist/common/embedder.js +6 -17
  7. package/dist/common/manifest.d.ts +7 -1
  8. package/dist/common/progress.d.ts +16 -1
  9. package/dist/common/progress.js +35 -3
  10. package/dist/common/prose.d.ts +7 -0
  11. package/dist/common/prose.js +13 -0
  12. package/dist/docs/assemble.d.ts +52 -0
  13. package/dist/docs/assemble.js +127 -0
  14. package/dist/docs/build.d.ts +54 -0
  15. package/dist/docs/build.js +134 -0
  16. package/dist/docs/chunk.d.ts +73 -0
  17. package/dist/docs/chunk.js +586 -0
  18. package/dist/docs/command.d.ts +3 -0
  19. package/dist/docs/command.js +575 -0
  20. package/dist/docs/files.d.ts +94 -0
  21. package/dist/docs/files.js +80 -0
  22. package/dist/docs/index.d.ts +13 -0
  23. package/dist/docs/index.js +13 -0
  24. package/dist/docs/load.d.ts +42 -0
  25. package/dist/docs/load.js +203 -0
  26. package/dist/docs/lookup.d.ts +80 -0
  27. package/dist/docs/lookup.js +147 -0
  28. package/dist/docs/outline.d.ts +11 -0
  29. package/dist/docs/outline.js +55 -0
  30. package/dist/docs/parse-cache.d.ts +20 -0
  31. package/dist/docs/parse-cache.js +60 -0
  32. package/dist/docs/parse-worker.d.ts +13 -0
  33. package/dist/docs/parse-worker.js +21 -0
  34. package/dist/docs/parse.d.ts +95 -0
  35. package/dist/docs/parse.js +372 -0
  36. package/dist/docs/pool.d.ts +27 -0
  37. package/dist/docs/pool.js +133 -0
  38. package/dist/docs/readme.d.ts +6 -0
  39. package/dist/docs/readme.js +142 -0
  40. package/dist/docs/render.d.ts +13 -0
  41. package/dist/docs/render.js +46 -0
  42. package/dist/docs/repl.d.ts +7 -0
  43. package/dist/docs/repl.js +130 -0
  44. package/dist/docs/search.d.ts +92 -0
  45. package/dist/docs/search.js +251 -0
  46. package/dist/docs/store.d.ts +79 -0
  47. package/dist/docs/store.js +214 -0
  48. package/dist/docs/tools.d.ts +10 -0
  49. package/dist/docs/tools.js +300 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +3 -0
  52. package/dist/schema/build.d.ts +16 -2
  53. package/dist/schema/build.js +37 -25
  54. package/dist/schema/command.js +39 -10
  55. package/dist/schema/query.js +1 -0
  56. package/dist/schema/readme.js +6 -2
  57. package/dist/schema/search.d.ts +2 -0
  58. package/dist/schema/search.js +18 -2
  59. package/dist/schema/store.d.ts +13 -3
  60. package/dist/schema/store.js +73 -24
  61. package/dist/schema/tools.js +21 -2
  62. package/package.json +17 -4
@@ -0,0 +1,60 @@
1
+ import { Cache, cacheKey } from '@zenera/cli/lib';
2
+ import { createHash } from 'node:crypto';
3
+ // ---------------------------------------------------------------------------
4
+ // Not parsing the same document twice
5
+ //
6
+ // Once vectors are cached, parsing is what a rebuild of an unchanged corpus
7
+ // spends all of its time on — the threads make it several times faster, and
8
+ // this makes it nothing at all. A document that has not changed produces
9
+ // exactly the chunks it produced last time, so the chunks are what is kept.
10
+ //
11
+ // The key is the file's bytes together with everything that decides how they
12
+ // are cut: change the document, the chunk settings or this module, and the
13
+ // entry is a miss rather than a wrong answer. It sits in the machine's shared
14
+ // cache next to the vectors, so a corpus indexed into two directories is read
15
+ // once, and it follows the same rule everything there does — every error is a
16
+ // miss and nothing else.
17
+ // ---------------------------------------------------------------------------
18
+ export const PARSE_KIND = 'docs-parse';
19
+ /** Bumped when chunking changes shape, which invalidates every entry. */
20
+ export const PARSE_VERSION = 1;
21
+ export const NO_PARSE_CACHE = {
22
+ get: () => undefined,
23
+ put: () => { },
24
+ commit: () => { },
25
+ hits: 0,
26
+ };
27
+ /**
28
+ * What the chunks depend on, and nothing else. `tokenCount` is a function and
29
+ * cannot be hashed; a caller that supplies one gets no cache rather than a key
30
+ * that quietly ignores it.
31
+ */
32
+ export function parseKey(bytes, name, options) {
33
+ if (typeof options.tokenCount === 'function') {
34
+ return undefined;
35
+ }
36
+ return cacheKey(PARSE_VERSION, name, options.chunkTokens, options.minChunkTokens, options.maxChunkTokens, options.tableSliceTokens, createHash('sha256').update(bytes).digest('hex'));
37
+ }
38
+ export function openParseCache(dir) {
39
+ return new StoredParses(new Cache(PARSE_KIND, { dir }));
40
+ }
41
+ class StoredParses {
42
+ #store;
43
+ constructor(store) {
44
+ this.#store = store;
45
+ }
46
+ get hits() {
47
+ return this.#store.hits;
48
+ }
49
+ get(key) {
50
+ const found = this.#store.get(key);
51
+ return found?.chunks && found.outline ? found : undefined;
52
+ }
53
+ put(key, parsed) {
54
+ this.#store.put(key, { chunks: parsed.chunks, outline: parsed.outline });
55
+ }
56
+ commit() {
57
+ this.#store.commit();
58
+ }
59
+ }
60
+ //# sourceMappingURL=parse-cache.js.map
@@ -0,0 +1,13 @@
1
+ export interface ParseJob {
2
+ at: number;
3
+ name: string;
4
+ text: string;
5
+ format: 'markdown' | 'text';
6
+ }
7
+ export interface ParseDone {
8
+ at: number;
9
+ chunks?: unknown;
10
+ outline?: unknown;
11
+ error?: string;
12
+ }
13
+ //# sourceMappingURL=parse-worker.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { parentPort, workerData } from 'node:worker_threads';
2
+ import { chunkDocument } from "./chunk.js";
3
+ import { outlineOf } from "./outline.js";
4
+ import { parseDocument } from "./parse.js";
5
+ const options = (workerData ?? {});
6
+ parentPort?.on('message', (job) => {
7
+ try {
8
+ const parsed = parseDocument(job.text, job.name, job.format);
9
+ const chunks = chunkDocument(parsed, options);
10
+ parentPort.postMessage({ at: job.at, chunks, outline: outlineOf(parsed, chunks.length) });
11
+ }
12
+ catch (err) {
13
+ // Sent back rather than thrown: an uncaught throw here kills the thread,
14
+ // and the pool would lose the other documents queued behind this one.
15
+ parentPort.postMessage({
16
+ at: job.at,
17
+ error: err instanceof Error ? err.message : String(err),
18
+ });
19
+ }
20
+ });
21
+ //# sourceMappingURL=parse-worker.js.map
@@ -0,0 +1,95 @@
1
+ /** What a line, a block or a chunk is. The vocabulary is closed on purpose. */
2
+ export type StructureKind = 'heading' | 'paragraph' | 'blockquote' | 'list' | 'list_item' | 'table' | 'table_header' | 'table_row' | 'code' | 'frontmatter' | 'html' | 'hr';
3
+ /** The root of every path: a document is the structure everything else sits in. */
4
+ export declare const ROOT_SEGMENT = "doc";
5
+ export type DocFormat = 'markdown' | 'text';
6
+ /**
7
+ * A heading and everything under it, until a heading of the same depth or
8
+ * shallower. mdast has no such node — a heading there is a SIBLING of the
9
+ * paragraphs that follow it — so the nesting is built here.
10
+ */
11
+ export interface Section {
12
+ id: string;
13
+ path: string;
14
+ title: string;
15
+ /** 0 for the document itself, otherwise the heading depth */
16
+ level: number;
17
+ /** the heading's own line; absent on the document root */
18
+ line: number | undefined;
19
+ parent: Section | undefined;
20
+ }
21
+ export interface TableRowBlock {
22
+ id: string;
23
+ path: string;
24
+ line: number;
25
+ cells: string[];
26
+ /** the row as prose, so it carries its own column names */
27
+ text: string;
28
+ }
29
+ export interface TableBlock {
30
+ columns: string[];
31
+ headerLine: number;
32
+ /** the alignment row; mdast emits no node for it, so it is derived */
33
+ separatorLine: number | undefined;
34
+ /** the paragraph before the table, which is the only caption one ever gets */
35
+ caption: string;
36
+ /** the column that identifies a row, repeated into every slice of it */
37
+ keyColumn: number;
38
+ rows: TableRowBlock[];
39
+ }
40
+ export interface ListItemBlock {
41
+ id: string;
42
+ path: string;
43
+ start: number;
44
+ end: number;
45
+ text: string;
46
+ }
47
+ export interface Block {
48
+ id: string;
49
+ path: string;
50
+ kind: StructureKind;
51
+ /** 1-based, inclusive */
52
+ start: number;
53
+ end: number;
54
+ section: Section;
55
+ /** normalized, never shown: this is what is embedded and indexed */
56
+ text: string;
57
+ items?: ListItemBlock[];
58
+ table?: TableBlock;
59
+ }
60
+ export interface ParsedDoc {
61
+ /** the document's name within the index — its path, and its identity */
62
+ name: string;
63
+ title: string;
64
+ format: DocFormat;
65
+ /** verbatim, CRLF normalized, 1-based when indexed as `lines[n - 1]` */
66
+ lines: string[];
67
+ sections: Section[];
68
+ blocks: Block[];
69
+ }
70
+ /** Every ancestor of a section, the document root first, itself last. */
71
+ export declare function ancestry(section: Section): Section[];
72
+ /** The breadcrumb, which prefixes every chunk of text under this section. */
73
+ export declare const headingPath: (section: Section) => string;
74
+ /** The heading lines a chunk under this section must be rendered with. */
75
+ export declare const headingLines: (section: Section) => number[];
76
+ /**
77
+ * Everything downstream indexes `lines` by number, and mdast positions refer to
78
+ * the exact string handed to the parser — so the normalization happens once,
79
+ * here, and the normalized text is the only version anything ever sees.
80
+ */
81
+ export declare const normalize: (text: string) => string;
82
+ export declare function parseDocument(source: string, name: string, format: DocFormat): ParsedDoc;
83
+ /**
84
+ * A data row does not contain its own column names. Stripping the pipes leaves
85
+ * `V-200-30 3" WCC 98.2`, in which the word `Cv` appears nowhere, so a query
86
+ * naming a column could only ever match the header line and never a row — and
87
+ * hybrid search would silently become vector-only for every table in the
88
+ * corpus. Pairing each cell with its header is what fixes that, and it embeds
89
+ * better too, because `Cv: 45` is a sentence and `| 45 |` is not.
90
+ */
91
+ export declare function rowText(columns: readonly string[], cells: readonly string[]): string;
92
+ export declare function cellText(column: string | undefined, cell: string): string;
93
+ /** Soft wraps inside a paragraph are wrapping, not meaning, so they rejoin. */
94
+ export declare const collapse: (text: string) => string;
95
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1,372 @@
1
+ import remarkFrontmatter from 'remark-frontmatter';
2
+ import remarkGfm from 'remark-gfm';
3
+ import remarkParse from 'remark-parse';
4
+ import { unified } from 'unified';
5
+ /** Path segments carry their kind, so heading ancestors stay recoverable. */
6
+ const SEGMENT = {
7
+ heading: 'sec',
8
+ paragraph: 'para',
9
+ blockquote: 'quote',
10
+ list: 'list',
11
+ list_item: 'item',
12
+ table: 'table',
13
+ table_header: 'header',
14
+ table_row: 'row',
15
+ code: 'code',
16
+ frontmatter: 'fm',
17
+ html: 'html',
18
+ hr: 'hr',
19
+ };
20
+ /** The root of every path: a document is the structure everything else sits in. */
21
+ export const ROOT_SEGMENT = 'doc';
22
+ const processor = unified()
23
+ .use(remarkParse)
24
+ .use(remarkGfm)
25
+ .use(remarkFrontmatter, ['yaml', 'toml']);
26
+ // ---------------------------------------------------------------------------
27
+ /** Every ancestor of a section, the document root first, itself last. */
28
+ export function ancestry(section) {
29
+ const out = [];
30
+ for (let at = section; at; at = at.parent) {
31
+ out.unshift(at);
32
+ }
33
+ return out;
34
+ }
35
+ /** The breadcrumb, which prefixes every chunk of text under this section. */
36
+ export const headingPath = (section) => ancestry(section)
37
+ .map((s) => s.title)
38
+ .filter(Boolean)
39
+ .join(' > ');
40
+ /** The heading lines a chunk under this section must be rendered with. */
41
+ export const headingLines = (section) => ancestry(section)
42
+ .map((s) => s.line)
43
+ .filter((line) => line !== undefined);
44
+ /**
45
+ * Everything downstream indexes `lines` by number, and mdast positions refer to
46
+ * the exact string handed to the parser — so the normalization happens once,
47
+ * here, and the normalized text is the only version anything ever sees.
48
+ */
49
+ export const normalize = (text) => text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
50
+ export function parseDocument(source, name, format) {
51
+ const text = normalize(source);
52
+ const lines = text.split('\n');
53
+ return format === 'text' ? plain(text, lines, name) : markdown(text, lines, name);
54
+ }
55
+ function markdown(text, lines, name) {
56
+ const tree = processor.parse(text);
57
+ const root = {
58
+ id: ROOT_SEGMENT,
59
+ path: ROOT_SEGMENT,
60
+ title: stripExtension(name),
61
+ level: 0,
62
+ line: undefined,
63
+ parent: undefined,
64
+ };
65
+ const walk = { lines, counters: new Map(), sections: [root], blocks: [] };
66
+ let section = root;
67
+ let title = '';
68
+ // Only ever holds the paragraph immediately before the current position,
69
+ // which is the whole of what a markdown table gets for a caption.
70
+ let lead = '';
71
+ for (const node of tree.children) {
72
+ if (node.type === 'heading') {
73
+ const heading = node;
74
+ const line = lineOf(heading);
75
+ const text = collapse(inline(heading.children));
76
+ while (section.parent && section.level >= heading.depth) {
77
+ section = section.parent;
78
+ }
79
+ section = {
80
+ id: id(walk, 'heading'),
81
+ path: '',
82
+ title: text,
83
+ level: heading.depth,
84
+ line,
85
+ parent: section,
86
+ };
87
+ section.path = `${section.parent.path}/${section.id}`;
88
+ walk.sections.push(section);
89
+ if (!title && heading.depth <= 2) {
90
+ title = text;
91
+ }
92
+ lead = '';
93
+ continue;
94
+ }
95
+ const block = blockOf(node, section, walk, lead);
96
+ if (block) {
97
+ walk.blocks.push(block);
98
+ lead = block.kind === 'paragraph' ? block.text : '';
99
+ }
100
+ }
101
+ return {
102
+ name,
103
+ title: title || stripExtension(basenameOf(name)),
104
+ format: 'markdown',
105
+ lines,
106
+ sections: walk.sections,
107
+ blocks: walk.blocks,
108
+ };
109
+ }
110
+ function blockOf(node, section, walk, lead) {
111
+ const start = lineOf(node);
112
+ const end = endLineOf(node);
113
+ switch (node.type) {
114
+ case 'paragraph':
115
+ return make('paragraph', collapse(inline(node.children)));
116
+ case 'blockquote':
117
+ return make('blockquote', collapse(blocks(node.children)));
118
+ case 'code': {
119
+ const code = node;
120
+ return make('code', code.value);
121
+ }
122
+ case 'html': {
123
+ const stripped = collapse(tags(node.value));
124
+ return stripped.length >= 3 ? make('html', stripped) : undefined;
125
+ }
126
+ case 'yaml':
127
+ return make('frontmatter', String(node.value ?? ''));
128
+ case 'thematicBreak':
129
+ return make('hr', '');
130
+ case 'list':
131
+ return list(node, section, walk, start, end);
132
+ case 'table':
133
+ return table(node, section, walk, start, end, lead);
134
+ default:
135
+ // A `+++` block. remark-frontmatter puts it in the tree; mdast's own
136
+ // node union names only the `---` one.
137
+ return node.type === 'toml'
138
+ ? make('frontmatter', String(node.value ?? ''))
139
+ : undefined;
140
+ }
141
+ function make(kind, text) {
142
+ const own = id(walk, kind);
143
+ return { id: own, path: `${section.path}/${own}`, kind, start, end, section, text };
144
+ }
145
+ }
146
+ function list(node, section, walk, start, end) {
147
+ const own = id(walk, 'list');
148
+ const path = `${section.path}/${own}`;
149
+ const ordinal = node.start ?? 1;
150
+ const items = node.children.map((child, at) => {
151
+ const item = child;
152
+ const mine = id(walk, 'list_item');
153
+ const marker = node.ordered ? `${ordinal + at}.` : '-';
154
+ // The marker is kept: enumeration and step order are meaning, and a
155
+ // numbered step reads differently from a bullet.
156
+ return {
157
+ id: mine,
158
+ path: `${path}/${mine}`,
159
+ start: lineOf(item),
160
+ end: endLineOf(item),
161
+ text: `${marker} ${collapse(blocks(item.children))}`.trim(),
162
+ };
163
+ });
164
+ return {
165
+ id: own,
166
+ path,
167
+ kind: 'list',
168
+ start,
169
+ end,
170
+ section,
171
+ text: items.map((i) => i.text).join('\n'),
172
+ items,
173
+ };
174
+ }
175
+ function table(node, section, walk, start, end, caption) {
176
+ const own = id(walk, 'table');
177
+ const path = `${section.path}/${own}`;
178
+ const [header, ...body] = node.children;
179
+ const columns = header ? cellsOf(header) : [];
180
+ const headerLine = header ? lineOf(header) : start;
181
+ const firstRow = body[0] ? lineOf(body[0]) : undefined;
182
+ // mdast has no node for the alignment row — alignment lives on the table —
183
+ // so it is the line after the header, when there is room for it.
184
+ const separatorLine = firstRow === undefined || firstRow > headerLine + 1 ? headerLine + 1 : undefined;
185
+ const rows = body.map((row) => {
186
+ const mine = id(walk, 'table_row');
187
+ const cells = cellsOf(row);
188
+ return {
189
+ id: mine,
190
+ path: `${path}/${mine}`,
191
+ line: lineOf(row),
192
+ cells,
193
+ text: rowText(columns, cells),
194
+ };
195
+ });
196
+ return {
197
+ id: own,
198
+ path,
199
+ kind: 'table',
200
+ start,
201
+ end,
202
+ section,
203
+ text: rows.map((r) => r.text).join('\n'),
204
+ table: {
205
+ columns,
206
+ headerLine,
207
+ separatorLine,
208
+ caption,
209
+ keyColumn: keyColumnOf(columns, rows),
210
+ rows,
211
+ },
212
+ };
213
+ }
214
+ /**
215
+ * A data row does not contain its own column names. Stripping the pipes leaves
216
+ * `V-200-30 3" WCC 98.2`, in which the word `Cv` appears nowhere, so a query
217
+ * naming a column could only ever match the header line and never a row — and
218
+ * hybrid search would silently become vector-only for every table in the
219
+ * corpus. Pairing each cell with its header is what fixes that, and it embeds
220
+ * better too, because `Cv: 45` is a sentence and `| 45 |` is not.
221
+ */
222
+ export function rowText(columns, cells) {
223
+ return cells
224
+ .map((cell, at) => cellText(columns[at], cell))
225
+ .filter(Boolean)
226
+ .join(' ');
227
+ }
228
+ export function cellText(column, cell) {
229
+ const value = cell.trim();
230
+ if (!value) {
231
+ return '';
232
+ }
233
+ return column ? `${column}: ${value}.` : `${value}.`;
234
+ }
235
+ /** Column 1, unless it is numeric — a slice with no name on it is unattributable. */
236
+ function keyColumnOf(columns, rows) {
237
+ const numeric = (at) => rows.length > 0 &&
238
+ rows.every((row) => {
239
+ const cell = (row.cells[at] ?? '').trim();
240
+ return cell === '' || /^[-+]?[\d.,%\s]+$/.test(cell);
241
+ });
242
+ for (let at = 0; at < columns.length; at++) {
243
+ if (!numeric(at)) {
244
+ return at;
245
+ }
246
+ }
247
+ return 0;
248
+ }
249
+ const cellsOf = (row) => row.children.map((cell) => collapse(inline(cell.children)));
250
+ // ---------------------------------------------------------------------------
251
+ // plain text
252
+ // ---------------------------------------------------------------------------
253
+ /**
254
+ * A `.txt` file is read as paragraphs and nothing else. It would be easy to run
255
+ * it through the markdown parser and get headings for free, but they would be
256
+ * invented: a line beginning with `#` in a log or a licence is not a title, and
257
+ * an index that says it is would scope searches to sections nobody wrote.
258
+ */
259
+ function plain(text, lines, name) {
260
+ const root = {
261
+ id: ROOT_SEGMENT,
262
+ path: ROOT_SEGMENT,
263
+ title: stripExtension(name),
264
+ level: 0,
265
+ line: undefined,
266
+ parent: undefined,
267
+ };
268
+ const blocks = [];
269
+ let at = 0;
270
+ let ordinal = 0;
271
+ while (at < lines.length) {
272
+ if ((lines[at] ?? '').trim() === '') {
273
+ at++;
274
+ continue;
275
+ }
276
+ const start = at;
277
+ while (at < lines.length && (lines[at] ?? '').trim() !== '') {
278
+ at++;
279
+ }
280
+ const own = `${SEGMENT.paragraph}:${++ordinal}`;
281
+ blocks.push({
282
+ id: own,
283
+ path: `${root.path}/${own}`,
284
+ kind: 'paragraph',
285
+ start: start + 1,
286
+ end: at,
287
+ section: root,
288
+ text: collapse(lines.slice(start, at).join(' ')),
289
+ });
290
+ }
291
+ const first = lines.find((line) => line.trim() !== '')?.trim() ?? '';
292
+ return {
293
+ name,
294
+ title: first.length > 0 && first.length <= 80 ? first : stripExtension(basenameOf(name)),
295
+ format: 'text',
296
+ lines,
297
+ sections: [root],
298
+ blocks,
299
+ };
300
+ }
301
+ // ---------------------------------------------------------------------------
302
+ // serialization — walking the inline tree, never a regex over the markup
303
+ // ---------------------------------------------------------------------------
304
+ function inline(nodes) {
305
+ return nodes.map(one).join('');
306
+ }
307
+ function one(node) {
308
+ switch (node.type) {
309
+ case 'text':
310
+ return node.value;
311
+ // The value, not the markup: this is the node a regex stripper eats.
312
+ case 'inlineCode':
313
+ return node.value;
314
+ case 'image':
315
+ case 'imageReference':
316
+ // Distinguishable from a sentence on purpose.
317
+ return node.alt ? `image: ${node.alt}` : '';
318
+ case 'break':
319
+ return ' ';
320
+ // Inline html carries no words worth indexing, and its angle brackets
321
+ // would only ever match a query by accident.
322
+ case 'html':
323
+ return '';
324
+ case 'footnoteReference':
325
+ return '';
326
+ default:
327
+ return 'children' in node ? inline(node.children) : '';
328
+ }
329
+ }
330
+ /** Nested block content — a blockquote's paragraphs, a list item's children. */
331
+ function blocks(nodes) {
332
+ return nodes
333
+ .map((node) => {
334
+ switch (node.type) {
335
+ case 'paragraph':
336
+ case 'heading':
337
+ return inline(node.children);
338
+ case 'code':
339
+ return node.value;
340
+ case 'blockquote':
341
+ return blocks(node.children);
342
+ case 'list':
343
+ return node.children
344
+ .map((item) => blocks(item.children))
345
+ .join(' ');
346
+ case 'table':
347
+ return node.children
348
+ .slice(1)
349
+ .map((row) => rowText(cellsOf(node.children[0]), cellsOf(row)))
350
+ .join(' ');
351
+ default:
352
+ return '';
353
+ }
354
+ })
355
+ .filter(Boolean)
356
+ .join(' ');
357
+ }
358
+ /** Soft wraps inside a paragraph are wrapping, not meaning, so they rejoin. */
359
+ export const collapse = (text) => text.replace(/\s+/g, ' ').trim();
360
+ const tags = (html) => html.replace(/<[^>]*>/g, ' ');
361
+ // ---------------------------------------------------------------------------
362
+ function id(walk, kind) {
363
+ const segment = SEGMENT[kind];
364
+ const next = (walk.counters.get(segment) ?? 0) + 1;
365
+ walk.counters.set(segment, next);
366
+ return `${segment}:${next}`;
367
+ }
368
+ const lineOf = (node) => node.position?.start.line ?? 1;
369
+ const endLineOf = (node) => node.position?.end.line ?? 1;
370
+ const basenameOf = (name) => name.split('/').pop() ?? name;
371
+ const stripExtension = (name) => name.replace(/\.[^./]+$/, '');
372
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1,27 @@
1
+ import { type Chunk, type ChunkOptions } from './chunk.ts';
2
+ import type { FileOutline } from './files.ts';
3
+ export interface ParseInput {
4
+ name: string;
5
+ text: string;
6
+ format: 'markdown' | 'text';
7
+ }
8
+ export interface Parsed {
9
+ chunks: Chunk[];
10
+ outline: FileOutline;
11
+ }
12
+ /** One short of the machine, so the thread doing everything else keeps a core. */
13
+ export declare const poolSize: () => number;
14
+ export interface ParseAllOptions {
15
+ chunk?: ChunkOptions;
16
+ /** how many threads to allow; 1 keeps everything here */
17
+ workers?: number;
18
+ onProgress?: (done: number, total: number, pending: readonly string[]) => void;
19
+ }
20
+ export declare function parseAll(inputs: readonly ParseInput[], options?: ParseAllOptions): Promise<Parsed[]>;
21
+ /**
22
+ * The threaded path on its own. `parseAll` falls back to this thread when the
23
+ * workers cannot run, which is right for a build and useless for a test: a
24
+ * worker that never starts would look exactly like a fast one. Tests call this.
25
+ */
26
+ export declare function parseThreaded(inputs: readonly ParseInput[], chunk: ChunkOptions, size: number, onProgress?: (done: number, total: number, pending: readonly string[]) => void): Promise<Parsed[]>;
27
+ //# sourceMappingURL=pool.d.ts.map