md-semantic-search 1.0.3 → 2.0.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.
- package/README.md +132 -16
- package/RESEARCH.md +207 -3
- package/bin/cli.mjs +449 -13
- package/dist/binary-format.js +9 -0
- package/dist/completions.js +11 -1
- package/dist/core.d.ts +42 -4
- package/dist/core.js +208 -14
- package/dist/federation.d.ts +16 -1
- package/dist/federation.js +33 -16
- package/dist/filter.d.ts +7 -0
- package/dist/filter.js +13 -0
- package/dist/hnsw.d.ts +96 -0
- package/dist/hnsw.js +326 -0
- package/dist/index-format.js +8 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/indexer.d.ts +18 -1
- package/dist/indexer.js +92 -18
- package/dist/ivf.d.ts +14 -7
- package/dist/ivf.js +126 -18
- package/dist/lexical.d.ts +10 -0
- package/dist/lexical.js +98 -15
- package/dist/markdown-parser.d.ts +62 -0
- package/dist/markdown-parser.js +227 -30
- package/dist/mcp.d.ts +229 -7
- package/dist/mcp.js +743 -44
- package/dist/models.d.ts +8 -2
- package/dist/models.js +84 -32
- package/dist/providers.d.ts +56 -0
- package/dist/providers.js +305 -0
- package/dist/quantization.d.ts +12 -0
- package/dist/quantization.js +54 -0
- package/dist/rag.d.ts +132 -7
- package/dist/rag.js +314 -38
- package/dist/repair.d.ts +1 -1
- package/dist/repair.js +53 -2
- package/dist/rerank.d.ts +51 -6
- package/dist/rerank.js +69 -10
- package/dist/search.d.ts +49 -3
- package/dist/search.js +387 -78
- package/dist/serve.d.ts +35 -0
- package/dist/serve.js +574 -58
- package/dist/tui.d.ts +14 -0
- package/dist/tui.js +132 -15
- package/dist/watcher.js +23 -12
- package/dist/webui.d.ts +3 -3
- package/dist/webui.js +225 -5
- package/dist/wikilinks.d.ts +31 -0
- package/dist/wikilinks.js +185 -1
- package/package.json +3 -3
- package/src/binary-format.ts +13 -0
- package/src/completions.ts +11 -1
- package/src/core.ts +249 -13
- package/src/federation.ts +51 -17
- package/src/filter.ts +14 -0
- package/src/hnsw.ts +414 -0
- package/src/index-format.ts +7 -1
- package/src/index.ts +7 -2
- package/src/indexer.ts +112 -19
- package/src/ivf.ts +157 -20
- package/src/lexical.ts +91 -13
- package/src/markdown-parser.ts +279 -37
- package/src/mcp.ts +820 -47
- package/src/models.ts +97 -35
- package/src/providers.ts +345 -0
- package/src/quantization.ts +61 -0
- package/src/rag.ts +451 -34
- package/src/repair.ts +51 -2
- package/src/rerank.ts +121 -14
- package/src/search.ts +424 -82
- package/src/serve.ts +589 -66
- package/src/tui.ts +142 -16
- package/src/watcher.ts +21 -10
- package/src/webui.ts +225 -5
- package/src/wikilinks.ts +220 -1
package/src/markdown-parser.ts
CHANGED
|
@@ -175,12 +175,80 @@ export function resolveChunkBudget(tokens?: number, defaultMax: number = 1200):
|
|
|
175
175
|
return defaultMax;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* issue #202: script-calibrated token estimate for the sentencepiece E5/BGE
|
|
180
|
+
* vocab family — the fast-path alternative to running the real tokenizer per
|
|
181
|
+
* block. Ratios are calibrated so the estimate errs slightly HIGH: a budget
|
|
182
|
+
* checked against this estimate never overflows the model's real maxTokens.
|
|
183
|
+
* Latin/digits/punctuation ≈ 4 chars/token, Cyrillic ≈ 1.8 chars/token
|
|
184
|
+
* (RU systematically under-chunked by the old char budget — the point of
|
|
185
|
+
* this issue), CJK ≈ 1 token/char, whitespace mostly merges into the
|
|
186
|
+
* following subword so it counts a little.
|
|
187
|
+
*/
|
|
188
|
+
export function estimateTokens(text: string): number {
|
|
189
|
+
let tokens = 0;
|
|
190
|
+
for (let i = 0; i < text.length; i++) {
|
|
191
|
+
const c = text.charCodeAt(i);
|
|
192
|
+
if (c >= 0x0400 && c <= 0x04ff) tokens += 0.55; // Cyrillic (+ Cyrillic supplement edges)
|
|
193
|
+
else if (
|
|
194
|
+
(c >= 0x3040 && c <= 0x30ff) || // hiragana / katakana
|
|
195
|
+
(c >= 0x3400 && c <= 0x4dbf) || // CJK ext A
|
|
196
|
+
(c >= 0x4e00 && c <= 0x9fff) || // CJK unified
|
|
197
|
+
(c >= 0xff00 && c <= 0xffef) // fullwidth forms
|
|
198
|
+
) {
|
|
199
|
+
tokens += 1.0;
|
|
200
|
+
} else if (c === 0x20 || (c >= 0x09 && c <= 0x0d)) tokens += 0.08;
|
|
201
|
+
else if (c < 0x80) tokens += 0.25; // ASCII (latin, digits, punctuation, markdown syntax)
|
|
202
|
+
else tokens += 0.35; // other scripts / symbols — conservative middle ground
|
|
203
|
+
}
|
|
204
|
+
return Math.ceil(tokens);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Token measure: injected exact counter when available, else the calibrated estimate. */
|
|
208
|
+
export type TokenCounter = (text: string) => number;
|
|
209
|
+
|
|
210
|
+
export interface ChunkBudget {
|
|
211
|
+
/** Max measured size a single chunk may reach before it is split/flushed. */
|
|
212
|
+
budget: number;
|
|
213
|
+
/** Measures a text in the same unit the budget is expressed in. */
|
|
214
|
+
measure: TokenCounter;
|
|
215
|
+
/** Unit name for diagnostics/tests. */
|
|
216
|
+
unit: 'chars' | 'tokens';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Char budget (pre-#202 behavior) vs token budget (issue #202). */
|
|
220
|
+
export function resolveChunkingBudget(opts: {
|
|
221
|
+
maxChunk?: number;
|
|
222
|
+
maxChunkTokens?: number;
|
|
223
|
+
countTokens?: TokenCounter;
|
|
224
|
+
}): ChunkBudget {
|
|
225
|
+
if (opts.maxChunkTokens && Number.isFinite(opts.maxChunkTokens) && opts.maxChunkTokens > 0) {
|
|
226
|
+
const budget = Math.max(32, Math.round(opts.maxChunkTokens));
|
|
227
|
+
return { budget, measure: opts.countTokens ?? estimateTokens, unit: 'tokens' };
|
|
228
|
+
}
|
|
229
|
+
const budget = resolveChunkBudget(undefined, opts.maxChunk ?? 1200);
|
|
230
|
+
return { budget, measure: (t) => t.length, unit: 'chars' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* issue #202: the DEFAULT chunk budget in tokens, derived from the embedding
|
|
235
|
+
* adapter's context window. Capped at 512 — a passage much larger than that
|
|
236
|
+
* dilutes retrieval granularity even for models with 8k+ context (bge-m3).
|
|
237
|
+
* Returns undefined for adapters without a known maxTokens, which keeps the
|
|
238
|
+
* legacy char budget instead of guessing.
|
|
239
|
+
*/
|
|
240
|
+
export function defaultChunkTokenBudget(adapter: { maxTokens?: number }): number | undefined {
|
|
241
|
+
const mt = adapter?.maxTokens;
|
|
242
|
+
if (!mt || !Number.isFinite(mt) || mt <= 0) return undefined;
|
|
243
|
+
return Math.min(Math.round(mt), 512);
|
|
244
|
+
}
|
|
245
|
+
|
|
178
246
|
export function chunkMarkdownStructural(
|
|
179
247
|
input: string | MarkdownBlock[],
|
|
180
|
-
opts?: number | { maxChunk?: number; targetTokens?: number }
|
|
248
|
+
opts?: number | { maxChunk?: number; targetTokens?: number; maxChunkTokens?: number; countTokens?: TokenCounter }
|
|
181
249
|
): StructuralChunk[] {
|
|
182
250
|
const options = typeof opts === 'number' ? { maxChunk: opts } : (opts || {});
|
|
183
|
-
const maxChunk =
|
|
251
|
+
const { budget: maxChunk, measure } = resolveChunkingBudget(options);
|
|
184
252
|
const blocks = typeof input === 'string' ? parseMarkdownBlocks(input) : input;
|
|
185
253
|
const chunks: StructuralChunk[] = [];
|
|
186
254
|
|
|
@@ -190,6 +258,8 @@ export function chunkMarkdownStructural(
|
|
|
190
258
|
let currentLen = 0;
|
|
191
259
|
let groupStartLine = 0;
|
|
192
260
|
let groupEndLine = 0;
|
|
261
|
+
// measured size of the '\n\n' joiner — 2 in char mode, ~1 in token mode
|
|
262
|
+
const joinerSize = measure('\n\n');
|
|
193
263
|
|
|
194
264
|
const flushGroup = () => {
|
|
195
265
|
if (currentGroup.length === 0) return;
|
|
@@ -223,27 +293,26 @@ export function chunkMarkdownStructural(
|
|
|
223
293
|
const blockText = block.text.trim();
|
|
224
294
|
if (!blockText) continue;
|
|
225
295
|
|
|
226
|
-
if (blockText
|
|
296
|
+
if (measure(blockText) > maxChunk) {
|
|
227
297
|
flushGroup();
|
|
228
298
|
const headingPath = headingStack.map(h => h.heading);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
299
|
+
// Issue #167: pieces carry the block-relative range of REAL source
|
|
300
|
+
// lines backing them — synthetic fence/header repeats must not advance
|
|
301
|
+
// the line cursor, or every later sub-chunk drifts down the file.
|
|
302
|
+
const subPieces = splitOversizedByMeasure(blockText, maxChunk, measure, block.type);
|
|
303
|
+
for (const piece of subPieces) {
|
|
234
304
|
chunks.push({
|
|
235
305
|
heading: headingPath.at(-1) ?? '',
|
|
236
306
|
headingPath: [...headingPath],
|
|
237
|
-
text: piece,
|
|
238
|
-
startLine:
|
|
239
|
-
endLine: Math.min(block.endLine,
|
|
307
|
+
text: piece.text,
|
|
308
|
+
startLine: block.startLine + piece.startLine,
|
|
309
|
+
endLine: Math.min(block.endLine, block.startLine + piece.endLine),
|
|
240
310
|
});
|
|
241
|
-
lineCursor += pieceLineCount;
|
|
242
311
|
}
|
|
243
312
|
continue;
|
|
244
313
|
}
|
|
245
314
|
|
|
246
|
-
const addedLen = currentLen === 0 ? blockText
|
|
315
|
+
const addedLen = currentLen === 0 ? measure(blockText) : currentLen + joinerSize + measure(blockText);
|
|
247
316
|
if (addedLen > maxChunk && currentGroup.length > 0) {
|
|
248
317
|
flushGroup();
|
|
249
318
|
}
|
|
@@ -252,7 +321,7 @@ export function chunkMarkdownStructural(
|
|
|
252
321
|
groupStartLine = block.startLine;
|
|
253
322
|
}
|
|
254
323
|
currentGroup.push(blockText);
|
|
255
|
-
currentLen += (currentGroup.length === 1 ? 0 :
|
|
324
|
+
currentLen += (currentGroup.length === 1 ? 0 : joinerSize) + measure(blockText);
|
|
256
325
|
groupEndLine = block.endLine;
|
|
257
326
|
}
|
|
258
327
|
|
|
@@ -274,14 +343,154 @@ function splitLongLine(line: string, maxChunk: number): string[] {
|
|
|
274
343
|
return pieces;
|
|
275
344
|
}
|
|
276
345
|
|
|
346
|
+
export interface OversizedPiece {
|
|
347
|
+
text: string;
|
|
348
|
+
/** 0-based block-relative range of REAL source lines backing this piece. */
|
|
349
|
+
startLine: number;
|
|
350
|
+
endLine: number;
|
|
351
|
+
}
|
|
352
|
+
|
|
277
353
|
export function splitOversizedBlock(
|
|
278
354
|
text: string,
|
|
279
355
|
maxChunk: number,
|
|
280
356
|
blockType?: MarkdownBlock['type']
|
|
281
357
|
): string[] {
|
|
358
|
+
return splitOversizedBlockWithRanges(text, maxChunk, blockType).map((p) => p.text);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* issue #202: oversized-block splitting under an arbitrary measure (chars or
|
|
363
|
+
* tokens). The heavy lifting (fence repeats, table header repeats, #167 line
|
|
364
|
+
* ranges) stays in the char-based splitOversizedBlockWithRanges; the token
|
|
365
|
+
* budget is converted to a char budget for THIS block via its own chars-per-
|
|
366
|
+
* measured-unit ratio, then every piece is verified — script-skew inside the
|
|
367
|
+
* block (ASCII header + Cyrillic body) is caught by a greedy re-split.
|
|
368
|
+
*/
|
|
369
|
+
export function splitOversizedByMeasure(
|
|
370
|
+
text: string,
|
|
371
|
+
budget: number,
|
|
372
|
+
measure: TokenCounter,
|
|
373
|
+
blockType?: MarkdownBlock['type']
|
|
374
|
+
): OversizedPiece[] {
|
|
375
|
+
if (measure(text) <= budget) {
|
|
376
|
+
return [{ text, startLine: 0, endLine: Number.MAX_SAFE_INTEGER }];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// char equivalent of the budget for this block's script mix; fall back to
|
|
380
|
+
// a plain char split with the raw budget when measuring is degenerate
|
|
381
|
+
const measured = Math.max(1, measure(text));
|
|
382
|
+
const charsPerUnit = text.length / measured;
|
|
383
|
+
const charBudget = Math.max(32, Math.floor(budget * charsPerUnit));
|
|
384
|
+
|
|
385
|
+
const pieces = splitOversizedBlockWithRanges(text, charBudget, blockType);
|
|
386
|
+
const out: OversizedPiece[] = [];
|
|
387
|
+
for (const piece of pieces) {
|
|
388
|
+
if (measure(piece.text) <= budget) {
|
|
389
|
+
out.push(piece);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
// script-skew safety net: greedy line/word accumulation under measure;
|
|
393
|
+
// the greedy splitter tracks consumed source lines so #167 ranges stay real
|
|
394
|
+
const spanLines = piece.endLine - piece.startLine + 1;
|
|
395
|
+
const textLines = piece.text.split('\n');
|
|
396
|
+
// when synthetic lines (fence/table repeats) inflated the piece, the text
|
|
397
|
+
// has more lines than the source span — clamp keeps ranges inside the block
|
|
398
|
+
const lineStride = textLines.length > spanLines ? spanLines / textLines.length : 1;
|
|
399
|
+
for (const sub of greedySplitByMeasure(piece.text, budget, measure)) {
|
|
400
|
+
const startLine = piece.startLine + Math.min(spanLines - 1, Math.floor(sub.startLine * lineStride));
|
|
401
|
+
const endLine = piece.startLine + Math.min(spanLines - 1, Math.max(sub.startLine, Math.floor(sub.endLine * lineStride)));
|
|
402
|
+
out.push({ text: sub.text, startLine, endLine });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Greedy line-based (then word-based, then hard-slice) split under measure, with line ranges. */
|
|
409
|
+
interface MeasuredPiece {
|
|
410
|
+
text: string;
|
|
411
|
+
/** 0-based indices into the input's line array. */
|
|
412
|
+
startLine: number;
|
|
413
|
+
endLine: number;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function greedySplitByMeasure(text: string, budget: number, measure: TokenCounter): MeasuredPiece[] {
|
|
417
|
+
const out: MeasuredPiece[] = [];
|
|
418
|
+
const lines = text.split('\n');
|
|
419
|
+
let bufStart = 0;
|
|
420
|
+
let bufEnd = 0;
|
|
421
|
+
let current = '';
|
|
422
|
+
|
|
423
|
+
const flush = (uptoLine: number) => {
|
|
424
|
+
if (current.trim()) out.push({ text: current.trim(), startLine: bufStart, endLine: uptoLine });
|
|
425
|
+
current = '';
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
for (let i = 0; i < lines.length; i++) {
|
|
429
|
+
const line = lines[i];
|
|
430
|
+
const candidate = current ? `${current}\n${line}` : line;
|
|
431
|
+
if (measure(candidate) <= budget) {
|
|
432
|
+
if (!current) bufStart = i;
|
|
433
|
+
current = candidate;
|
|
434
|
+
bufEnd = i;
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
flush(i - 1);
|
|
438
|
+
if (measure(line) <= budget) {
|
|
439
|
+
bufStart = i;
|
|
440
|
+
current = line;
|
|
441
|
+
bufEnd = i;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
// single line over budget: word-level accumulation, all pieces stay on
|
|
445
|
+
// this line's range
|
|
446
|
+
const words = line.split(/\s+/);
|
|
447
|
+
let wordBuf = '';
|
|
448
|
+
for (const word of words) {
|
|
449
|
+
const wc = wordBuf ? `${wordBuf} ${word}` : word;
|
|
450
|
+
if (measure(wc) <= budget) {
|
|
451
|
+
wordBuf = wc;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (wordBuf.trim()) out.push({ text: wordBuf.trim(), startLine: i, endLine: i });
|
|
455
|
+
if (measure(word) <= budget) {
|
|
456
|
+
wordBuf = word;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
// monster token (no spaces): hard-slice by chars proportional to budget
|
|
460
|
+
const ratio = line.length / Math.max(1, measure(line));
|
|
461
|
+
const sliceLen = Math.max(8, Math.floor(budget * ratio));
|
|
462
|
+
let rest = word;
|
|
463
|
+
while (measure(rest) > budget && rest.length > sliceLen) {
|
|
464
|
+
out.push({ text: rest.slice(0, sliceLen), startLine: i, endLine: i });
|
|
465
|
+
rest = rest.slice(sliceLen);
|
|
466
|
+
}
|
|
467
|
+
wordBuf = rest;
|
|
468
|
+
}
|
|
469
|
+
if (wordBuf.trim()) {
|
|
470
|
+
bufStart = i;
|
|
471
|
+
current = wordBuf;
|
|
472
|
+
bufEnd = i;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
flush(bufEnd);
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Issue #167: like splitOversizedBlock, but each piece also reports the
|
|
481
|
+
* block-relative range of source lines it was built from. Synthetic lines
|
|
482
|
+
* added to keep pieces self-contained (fence repeats, table header repeats)
|
|
483
|
+
* do NOT count — only the first piece's opening fence/header and a real
|
|
484
|
+
* closing fence belong to the source.
|
|
485
|
+
*/
|
|
486
|
+
export function splitOversizedBlockWithRanges(
|
|
487
|
+
text: string,
|
|
488
|
+
maxChunk: number,
|
|
489
|
+
blockType?: MarkdownBlock['type']
|
|
490
|
+
): OversizedPiece[] {
|
|
282
491
|
const lines = text.split('\n');
|
|
283
492
|
if (lines.length <= 1) {
|
|
284
|
-
return splitLongLine(text, maxChunk);
|
|
493
|
+
return splitLongLine(text, maxChunk).map((piece) => ({ text: piece, startLine: 0, endLine: 0 }));
|
|
285
494
|
}
|
|
286
495
|
|
|
287
496
|
// If code block with opening fence
|
|
@@ -291,21 +500,35 @@ export function splitOversizedBlock(
|
|
|
291
500
|
const closeFence = hasCloseFence ? lines[lines.length - 1] : '```';
|
|
292
501
|
const innerLines = lines.slice(1, hasCloseFence ? lines.length - 1 : lines.length);
|
|
293
502
|
|
|
294
|
-
const pieces:
|
|
295
|
-
let currentCode: string
|
|
503
|
+
const pieces: OversizedPiece[] = [];
|
|
504
|
+
let currentCode: Array<{ line: string; srcIdx: number }> = [];
|
|
296
505
|
let currentLen = openFence.length + closeFence.length + 2;
|
|
297
506
|
|
|
298
|
-
|
|
507
|
+
// The opening fence is a real source line only in the first piece; a
|
|
508
|
+
// real closing fence only in the final one.
|
|
509
|
+
const flush = (isFinal: boolean) => {
|
|
510
|
+
const firstReal = pieces.length === 0 ? 0 : currentCode[0].srcIdx;
|
|
511
|
+
const lastReal = isFinal && hasCloseFence ? lines.length - 1 : currentCode[currentCode.length - 1].srcIdx;
|
|
512
|
+
pieces.push({
|
|
513
|
+
text: [openFence, ...currentCode.map((c) => c.line), closeFence].join('\n'),
|
|
514
|
+
startLine: firstReal,
|
|
515
|
+
endLine: lastReal,
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
for (let offset = 0; offset < innerLines.length; offset++) {
|
|
520
|
+
const line = innerLines[offset];
|
|
521
|
+
const srcIdx = 1 + offset;
|
|
299
522
|
if (currentLen + line.length + 1 > maxChunk && currentCode.length > 0) {
|
|
300
|
-
|
|
523
|
+
flush(false);
|
|
301
524
|
currentCode = [];
|
|
302
525
|
currentLen = openFence.length + closeFence.length + 2;
|
|
303
526
|
}
|
|
304
|
-
currentCode.push(line);
|
|
527
|
+
currentCode.push({ line, srcIdx });
|
|
305
528
|
currentLen += line.length + 1;
|
|
306
529
|
}
|
|
307
530
|
if (currentCode.length > 0) {
|
|
308
|
-
|
|
531
|
+
flush(true);
|
|
309
532
|
}
|
|
310
533
|
if (pieces.length > 0) return pieces;
|
|
311
534
|
}
|
|
@@ -316,56 +539,75 @@ export function splitOversizedBlock(
|
|
|
316
539
|
const dataRows = lines.slice(2);
|
|
317
540
|
const headerLen = headerRows[0].length + headerRows[1].length + 2;
|
|
318
541
|
|
|
319
|
-
const pieces:
|
|
320
|
-
let currentRows: string
|
|
542
|
+
const pieces: OversizedPiece[] = [];
|
|
543
|
+
let currentRows: Array<{ row: string; srcIdx: number }> = [];
|
|
321
544
|
let currentLen = headerLen;
|
|
322
545
|
|
|
323
|
-
|
|
546
|
+
const flush = () => {
|
|
547
|
+
const firstReal = pieces.length === 0 ? 0 : currentRows[0].srcIdx;
|
|
548
|
+
pieces.push({
|
|
549
|
+
text: [...headerRows, ...currentRows.map((r) => r.row)].join('\n'),
|
|
550
|
+
startLine: firstReal,
|
|
551
|
+
endLine: currentRows[currentRows.length - 1].srcIdx,
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
for (let offset = 0; offset < dataRows.length; offset++) {
|
|
556
|
+
const row = dataRows[offset];
|
|
557
|
+
const srcIdx = 2 + offset;
|
|
324
558
|
if (currentLen + row.length + 1 > maxChunk && currentRows.length > 0) {
|
|
325
|
-
|
|
559
|
+
flush();
|
|
326
560
|
currentRows = [];
|
|
327
561
|
currentLen = headerLen;
|
|
328
562
|
}
|
|
329
|
-
currentRows.push(row);
|
|
563
|
+
currentRows.push({ row, srcIdx });
|
|
330
564
|
currentLen += row.length + 1;
|
|
331
565
|
}
|
|
332
566
|
if (currentRows.length > 0) {
|
|
333
|
-
|
|
567
|
+
flush();
|
|
334
568
|
}
|
|
335
569
|
if (pieces.length > 0) return pieces;
|
|
336
570
|
}
|
|
337
571
|
|
|
338
572
|
// Standard paragraph/list splitting
|
|
339
|
-
const pieces:
|
|
340
|
-
let currentLines: string
|
|
573
|
+
const pieces: OversizedPiece[] = [];
|
|
574
|
+
let currentLines: Array<{ line: string; srcIdx: number }> = [];
|
|
341
575
|
let currentLen = 0;
|
|
342
576
|
|
|
343
|
-
|
|
577
|
+
const flush = () => {
|
|
578
|
+
pieces.push({
|
|
579
|
+
text: currentLines.map((l) => l.line).join('\n').trim(),
|
|
580
|
+
startLine: currentLines[0].srcIdx,
|
|
581
|
+
endLine: currentLines[currentLines.length - 1].srcIdx,
|
|
582
|
+
});
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (let srcIdx = 0; srcIdx < lines.length; srcIdx++) {
|
|
586
|
+
const line = lines[srcIdx];
|
|
344
587
|
if (currentLen + line.length + 1 > maxChunk && currentLines.length > 0) {
|
|
345
|
-
|
|
588
|
+
flush();
|
|
346
589
|
currentLines = [];
|
|
347
590
|
currentLen = 0;
|
|
348
591
|
}
|
|
349
592
|
|
|
350
593
|
if (line.length > maxChunk) {
|
|
351
594
|
if (currentLines.length > 0) {
|
|
352
|
-
|
|
595
|
+
flush();
|
|
353
596
|
currentLines = [];
|
|
354
597
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
pieces.push(piece);
|
|
598
|
+
for (const piece of splitLongLine(line, maxChunk)) {
|
|
599
|
+
pieces.push({ text: piece, startLine: srcIdx, endLine: srcIdx });
|
|
358
600
|
}
|
|
359
601
|
currentLen = 0;
|
|
360
602
|
continue;
|
|
361
603
|
}
|
|
362
604
|
|
|
363
|
-
currentLines.push(line);
|
|
605
|
+
currentLines.push({ line, srcIdx });
|
|
364
606
|
currentLen += line.length + 1;
|
|
365
607
|
}
|
|
366
608
|
|
|
367
609
|
if (currentLines.length > 0) {
|
|
368
|
-
|
|
610
|
+
flush();
|
|
369
611
|
}
|
|
370
612
|
|
|
371
613
|
return pieces;
|