@mastra/rag 0.0.0-commonjs-20250227130920
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/.turbo/turbo-build.log +23 -0
- package/CHANGELOG.md +1151 -0
- package/LICENSE +44 -0
- package/README.md +26 -0
- package/dist/_tsup-dts-rollup.d.cts +620 -0
- package/dist/_tsup-dts-rollup.d.ts +620 -0
- package/dist/index.cjs +2524 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +2505 -0
- package/docker-compose.yaml +22 -0
- package/eslint.config.js +6 -0
- package/package.json +72 -0
- package/src/document/document.test.ts +985 -0
- package/src/document/document.ts +281 -0
- package/src/document/index.ts +2 -0
- package/src/document/transformers/character.ts +279 -0
- package/src/document/transformers/html.ts +346 -0
- package/src/document/transformers/json.ts +265 -0
- package/src/document/transformers/latex.ts +19 -0
- package/src/document/transformers/markdown.ts +244 -0
- package/src/document/transformers/text.ts +134 -0
- package/src/document/transformers/token.ts +148 -0
- package/src/document/transformers/transformer.ts +5 -0
- package/src/document/types.ts +103 -0
- package/src/graph-rag/index.test.ts +235 -0
- package/src/graph-rag/index.ts +306 -0
- package/src/index.ts +6 -0
- package/src/rerank/index.test.ts +150 -0
- package/src/rerank/index.ts +154 -0
- package/src/tools/document-chunker.ts +30 -0
- package/src/tools/graph-rag.ts +117 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/vector-query.ts +90 -0
- package/src/utils/default-settings.ts +33 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/vector-prompts.ts +729 -0
- package/src/utils/vector-search.ts +41 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
2
|
+
import { embedMany } from 'ai';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { MDocument } from './document';
|
|
6
|
+
import { Language } from './types';
|
|
7
|
+
|
|
8
|
+
const sampleMarkdown = `
|
|
9
|
+
# Complete Guide to Modern Web Development
|
|
10
|
+
## Introduction
|
|
11
|
+
Welcome to our comprehensive guide on modern web development. This resource covers essential concepts, best practices, and tools that every developer should know in 2024.
|
|
12
|
+
|
|
13
|
+
### Who This Guide Is For
|
|
14
|
+
- Beginning developers looking to establish a solid foundation
|
|
15
|
+
- Intermediate developers wanting to modernize their skillset
|
|
16
|
+
- Senior developers seeking a refresher on current best practices
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const openai = createOpenAI({
|
|
20
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('MDocument', () => {
|
|
24
|
+
describe('basics', () => {
|
|
25
|
+
let chunks: MDocument['chunks'];
|
|
26
|
+
let doc: MDocument;
|
|
27
|
+
it('initialization', () => {
|
|
28
|
+
const doc = new MDocument({ docs: [{ text: 'test' }], type: 'text' });
|
|
29
|
+
expect(doc.getDocs()).toHaveLength(1);
|
|
30
|
+
expect(doc.getText()?.[0]).toBe('test');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('initialization with array', () => {
|
|
34
|
+
doc = new MDocument({ docs: [{ text: 'test' }, { text: 'test2' }], type: 'text' });
|
|
35
|
+
expect(doc.getDocs()).toHaveLength(2);
|
|
36
|
+
expect(doc.getDocs()[0]?.text).toBe('test');
|
|
37
|
+
expect(doc.getDocs()[1]?.text).toBe('test2');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('chunk - metadata title', async () => {
|
|
41
|
+
const doc = MDocument.fromMarkdown(sampleMarkdown);
|
|
42
|
+
|
|
43
|
+
chunks = await doc.chunk({
|
|
44
|
+
size: 1500,
|
|
45
|
+
overlap: 0,
|
|
46
|
+
separator: `\n`,
|
|
47
|
+
extract: {
|
|
48
|
+
keywords: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(doc.getMetadata()?.[0]).toBeTruthy();
|
|
53
|
+
expect(chunks).toBeInstanceOf(Array);
|
|
54
|
+
}, 15000);
|
|
55
|
+
|
|
56
|
+
it('embed - create embedding from chunk', async () => {
|
|
57
|
+
const embeddings = await embedMany({
|
|
58
|
+
values: chunks.map(chunk => chunk.text),
|
|
59
|
+
model: openai.embedding('text-embedding-3-small'),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(embeddings);
|
|
63
|
+
expect(embeddings).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('chunkCharacter', () => {
|
|
68
|
+
it('should split text on simple separator', async () => {
|
|
69
|
+
const text = 'Hello world\n\nHow are you\n\nI am fine';
|
|
70
|
+
|
|
71
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
72
|
+
|
|
73
|
+
await doc.chunk({
|
|
74
|
+
strategy: 'character',
|
|
75
|
+
separator: '\n\n',
|
|
76
|
+
isSeparatorRegex: false,
|
|
77
|
+
size: 50,
|
|
78
|
+
overlap: 5,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const chunks = doc.getDocs();
|
|
82
|
+
|
|
83
|
+
expect(chunks).toHaveLength(3);
|
|
84
|
+
expect(chunks?.[0]?.text).toBe('Hello world');
|
|
85
|
+
expect(chunks?.[1]?.text).toBe('How are you');
|
|
86
|
+
expect(chunks?.[2]?.text).toBe('I am fine');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should handle regex separator', async () => {
|
|
90
|
+
const text = 'Hello world\n\nHow are you';
|
|
91
|
+
|
|
92
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
93
|
+
|
|
94
|
+
await doc.chunk({
|
|
95
|
+
strategy: 'character',
|
|
96
|
+
separator: '\\s+',
|
|
97
|
+
isSeparatorRegex: true,
|
|
98
|
+
size: 50,
|
|
99
|
+
overlap: 5,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(doc.getText().join(' ')).toBe('Hello world How are you');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should keep separator when specified', async () => {
|
|
106
|
+
const text = 'Hello\n\nWorld';
|
|
107
|
+
|
|
108
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
109
|
+
|
|
110
|
+
await doc.chunk({
|
|
111
|
+
strategy: 'character',
|
|
112
|
+
separator: '\n\n',
|
|
113
|
+
isSeparatorRegex: false,
|
|
114
|
+
size: 50,
|
|
115
|
+
overlap: 5,
|
|
116
|
+
keepSeparator: 'end',
|
|
117
|
+
});
|
|
118
|
+
const chunks = doc.getText();
|
|
119
|
+
|
|
120
|
+
expect(chunks[0]).toBe('Hello\n\n');
|
|
121
|
+
expect(chunks[1]).toBe('World');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('separator handling', () => {
|
|
125
|
+
it('should keep separator at end when specified', async () => {
|
|
126
|
+
const text = 'Hello\n\nWorld';
|
|
127
|
+
|
|
128
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
129
|
+
|
|
130
|
+
await doc.chunk({
|
|
131
|
+
strategy: 'character',
|
|
132
|
+
separator: '\n\n',
|
|
133
|
+
isSeparatorRegex: false,
|
|
134
|
+
size: 50,
|
|
135
|
+
overlap: 5,
|
|
136
|
+
keepSeparator: 'end',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const chunks = doc.getText();
|
|
140
|
+
|
|
141
|
+
expect(chunks).toHaveLength(2);
|
|
142
|
+
expect(chunks[0]).toBe('Hello\n\n');
|
|
143
|
+
expect(chunks[1]).toBe('World');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should keep separator at start when specified', async () => {
|
|
147
|
+
const text = 'Hello\n\nWorld\n\nTest';
|
|
148
|
+
|
|
149
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
150
|
+
|
|
151
|
+
await doc.chunk({
|
|
152
|
+
strategy: 'character',
|
|
153
|
+
separator: '\n\n',
|
|
154
|
+
isSeparatorRegex: false,
|
|
155
|
+
size: 50,
|
|
156
|
+
overlap: 5,
|
|
157
|
+
keepSeparator: 'start',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const chunks = doc.getText();
|
|
161
|
+
|
|
162
|
+
expect(chunks).toHaveLength(3);
|
|
163
|
+
expect(chunks[0]).toBe('Hello');
|
|
164
|
+
expect(chunks[1]).toBe('\n\nWorld');
|
|
165
|
+
expect(chunks[2]).toBe('\n\nTest');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should handle multiple consecutive separators', async () => {
|
|
169
|
+
const text = 'Hello\n\n\n\nWorld';
|
|
170
|
+
|
|
171
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
172
|
+
|
|
173
|
+
await doc.chunk({
|
|
174
|
+
strategy: 'character',
|
|
175
|
+
separator: '\n\n',
|
|
176
|
+
isSeparatorRegex: false,
|
|
177
|
+
size: 50,
|
|
178
|
+
overlap: 5,
|
|
179
|
+
keepSeparator: 'end',
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const chunks = doc.getText();
|
|
183
|
+
|
|
184
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
185
|
+
expect(chunks.join('')).toBe(text);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle text ending with separator', async () => {
|
|
189
|
+
const text = 'Hello\n\nWorld\n\n';
|
|
190
|
+
|
|
191
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
192
|
+
|
|
193
|
+
await doc.chunk({
|
|
194
|
+
strategy: 'character',
|
|
195
|
+
separator: '\n\n',
|
|
196
|
+
isSeparatorRegex: false,
|
|
197
|
+
size: 50,
|
|
198
|
+
overlap: 5,
|
|
199
|
+
keepSeparator: 'end',
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const chunks = doc.getText();
|
|
203
|
+
|
|
204
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
205
|
+
expect(chunks.join('')).toBe(text);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle text starting with separator', async () => {
|
|
209
|
+
const text = '\n\nHello\n\nWorld';
|
|
210
|
+
|
|
211
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
212
|
+
|
|
213
|
+
await doc.chunk({
|
|
214
|
+
strategy: 'character',
|
|
215
|
+
separator: '\n\n',
|
|
216
|
+
isSeparatorRegex: false,
|
|
217
|
+
size: 50,
|
|
218
|
+
overlap: 5,
|
|
219
|
+
keepSeparator: 'start',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const chunks = doc.getText();
|
|
223
|
+
|
|
224
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
225
|
+
expect(chunks.join('')).toBe(text);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('chunkRecursive', () => {
|
|
231
|
+
it('chunkRecursive', async () => {
|
|
232
|
+
const text =
|
|
233
|
+
'Hello world.\n\nThis is a test of the recursive splitting system.\nIt should handle multiple lines and different separators appropriately.';
|
|
234
|
+
|
|
235
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
236
|
+
|
|
237
|
+
await doc.chunk({
|
|
238
|
+
strategy: 'recursive',
|
|
239
|
+
separators: ['\n\n', '\n', ' ', ''],
|
|
240
|
+
isSeparatorRegex: false,
|
|
241
|
+
size: 50,
|
|
242
|
+
overlap: 5,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(doc.getDocs()?.length).toBeGreaterThan(1);
|
|
246
|
+
|
|
247
|
+
doc.getText()?.forEach(t => {
|
|
248
|
+
expect(t.length).toBeLessThanOrEqual(50);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('chunkRecursive - language options', async () => {
|
|
253
|
+
const tsCode = `
|
|
254
|
+
interface User {
|
|
255
|
+
name: string;
|
|
256
|
+
age: number;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function greet(user: User) {
|
|
260
|
+
console.log(\`Hello \${user.name}\`);
|
|
261
|
+
}
|
|
262
|
+
`;
|
|
263
|
+
|
|
264
|
+
const doc = MDocument.fromText(tsCode, { meta: 'data' });
|
|
265
|
+
|
|
266
|
+
await doc.chunk({
|
|
267
|
+
size: 50,
|
|
268
|
+
overlap: 5,
|
|
269
|
+
language: Language.TS,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(doc.getDocs().length).toBeGreaterThan(1);
|
|
273
|
+
expect(doc.getText().some(chunk => chunk.includes('interface'))).toBe(true);
|
|
274
|
+
expect(doc.getText().some(chunk => chunk.includes('function'))).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should throw error for unsupported language', async () => {
|
|
278
|
+
const doc = MDocument.fromText('tsCode', { meta: 'data' });
|
|
279
|
+
|
|
280
|
+
await expect(
|
|
281
|
+
doc.chunk({
|
|
282
|
+
size: 50,
|
|
283
|
+
overlap: 5,
|
|
284
|
+
language: 'invalid-language' as any,
|
|
285
|
+
}),
|
|
286
|
+
).rejects.toThrow();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should maintain context with overlap', async () => {
|
|
290
|
+
const text = 'This is a test.\nIt has multiple lines.\nEach line should be handled properly.';
|
|
291
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
292
|
+
|
|
293
|
+
await doc.chunk();
|
|
294
|
+
|
|
295
|
+
for (let i = 1; i < doc.getDocs().length; i++) {
|
|
296
|
+
const prevChunk = doc.getDocs()[i - 1]?.text;
|
|
297
|
+
const currentChunk = doc.getDocs()?.[i]?.text;
|
|
298
|
+
|
|
299
|
+
const hasOverlap = prevChunk?.split(' ').some(word => currentChunk?.includes(word));
|
|
300
|
+
|
|
301
|
+
expect(hasOverlap).toBe(true);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('chunkHTML', () => {
|
|
307
|
+
it('should split HTML with headers correctly', async () => {
|
|
308
|
+
const html = `
|
|
309
|
+
<html>
|
|
310
|
+
<body>
|
|
311
|
+
<h1>Main Title</h1>
|
|
312
|
+
<p>Main content.</p>
|
|
313
|
+
<h2>Section 1</h2>
|
|
314
|
+
<p>Section 1 content.</p>
|
|
315
|
+
<h3>Subsection 1.1</h3>
|
|
316
|
+
<p>Subsection content.</p>
|
|
317
|
+
</body>
|
|
318
|
+
</html>
|
|
319
|
+
`;
|
|
320
|
+
|
|
321
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
322
|
+
|
|
323
|
+
await doc.chunk({
|
|
324
|
+
strategy: 'html',
|
|
325
|
+
headers: [
|
|
326
|
+
['h1', 'Header 1'],
|
|
327
|
+
['h2', 'Header 2'],
|
|
328
|
+
['h3', 'Header 3'],
|
|
329
|
+
],
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const docs = doc.getDocs();
|
|
333
|
+
expect(docs.length).toBeGreaterThan(1);
|
|
334
|
+
expect(docs?.[0]?.metadata?.['Header 1']).toBe('Main Title');
|
|
335
|
+
expect(docs?.[1]?.metadata?.['Header 2']).toBe('Section 1');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should handle nested content', async () => {
|
|
339
|
+
const html = `
|
|
340
|
+
<html>
|
|
341
|
+
<body>
|
|
342
|
+
<h1>Title</h1>
|
|
343
|
+
<div>
|
|
344
|
+
<p>Nested content.</p>
|
|
345
|
+
<div>
|
|
346
|
+
<p>Deeply nested content.</p>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</body>
|
|
350
|
+
</html>
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
354
|
+
|
|
355
|
+
await doc.chunk({
|
|
356
|
+
strategy: 'html',
|
|
357
|
+
headers: [
|
|
358
|
+
['h1', 'Header 1'],
|
|
359
|
+
['h2', 'Header 2'],
|
|
360
|
+
['h3', 'Header 3'],
|
|
361
|
+
],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const docs = doc.getDocs();
|
|
365
|
+
const mainSection = docs.find(doc => doc.metadata?.['Header 1'] === 'Title');
|
|
366
|
+
expect(mainSection?.text).toContain('Nested content');
|
|
367
|
+
expect(mainSection?.text).toContain('Deeply nested content');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should respect returnEachElement option', async () => {
|
|
371
|
+
const html = `
|
|
372
|
+
<html>
|
|
373
|
+
<body>
|
|
374
|
+
<h1>Title</h1>
|
|
375
|
+
<p>Paragraph 1</p>
|
|
376
|
+
<h1>Title</h1>
|
|
377
|
+
<p>Paragraph 2</p>
|
|
378
|
+
<h1>Title</h1>
|
|
379
|
+
<p>Paragraph 3</p>
|
|
380
|
+
</body>
|
|
381
|
+
</html>
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
385
|
+
|
|
386
|
+
await doc.chunk({
|
|
387
|
+
strategy: 'html',
|
|
388
|
+
|
|
389
|
+
returnEachLine: true,
|
|
390
|
+
headers: [
|
|
391
|
+
['h1', 'Header 1'],
|
|
392
|
+
['h2', 'Header 2'],
|
|
393
|
+
['h3', 'Header 3'],
|
|
394
|
+
],
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const docs = doc.getDocs();
|
|
398
|
+
|
|
399
|
+
expect(docs.length).toBeGreaterThan(2);
|
|
400
|
+
docs.forEach(doc => {
|
|
401
|
+
expect(doc.metadata?.['Header 1']).toBe('Title');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should split HTML into sections', async () => {
|
|
406
|
+
const html = `
|
|
407
|
+
<html>
|
|
408
|
+
<body>
|
|
409
|
+
<h1>Document Title</h1>
|
|
410
|
+
<p>Introduction text.</p>
|
|
411
|
+
<h2>First Section</h2>
|
|
412
|
+
<p>First section content.</p>
|
|
413
|
+
<h2>Second Section</h2>
|
|
414
|
+
<p>Second section content.</p>
|
|
415
|
+
</body>
|
|
416
|
+
</html>
|
|
417
|
+
`;
|
|
418
|
+
|
|
419
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
420
|
+
|
|
421
|
+
await doc.chunk({
|
|
422
|
+
strategy: 'html',
|
|
423
|
+
sections: [
|
|
424
|
+
['h1', 'Header 1'],
|
|
425
|
+
['h2', 'Header 2'],
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
const docs = doc.getDocs();
|
|
429
|
+
|
|
430
|
+
expect(docs.length).toBe(3);
|
|
431
|
+
expect(docs?.[0]?.metadata?.['Header 1']).toBe('Document Title');
|
|
432
|
+
expect(docs?.[1]?.metadata?.['Header 2']).toBe('First Section');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should properly merge metadata', async () => {
|
|
436
|
+
const doc = new MDocument({
|
|
437
|
+
docs: [
|
|
438
|
+
{
|
|
439
|
+
text: `
|
|
440
|
+
<h1>Title 1</h1>
|
|
441
|
+
<p>Content 1</p>
|
|
442
|
+
`,
|
|
443
|
+
metadata: { source: 'doc1' },
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
text: `
|
|
447
|
+
<h1>Title 2</h1>
|
|
448
|
+
<p>Content 2</p>
|
|
449
|
+
`,
|
|
450
|
+
metadata: { source: 'doc2' },
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
type: 'html',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await doc.chunk({
|
|
457
|
+
strategy: 'html',
|
|
458
|
+
sections: [
|
|
459
|
+
['h1', 'Header 1'],
|
|
460
|
+
['h2', 'Header 2'],
|
|
461
|
+
],
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
doc.getDocs().forEach(doc => {
|
|
465
|
+
expect(doc?.metadata).toHaveProperty('source');
|
|
466
|
+
expect(doc?.metadata).toHaveProperty('Header 1');
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should handle empty or invalid HTML', async () => {
|
|
471
|
+
const emptyHtml = '';
|
|
472
|
+
const invalidHtml = '<unclosed>test';
|
|
473
|
+
const noHeadersHtml = '<div>test</div>';
|
|
474
|
+
|
|
475
|
+
const doc1 = MDocument.fromHTML(emptyHtml, { meta: 'data' });
|
|
476
|
+
const doc2 = MDocument.fromHTML(invalidHtml, { meta: 'data' });
|
|
477
|
+
const doc3 = MDocument.fromHTML(noHeadersHtml, { meta: 'data' });
|
|
478
|
+
|
|
479
|
+
await doc1.chunk({
|
|
480
|
+
strategy: 'html',
|
|
481
|
+
headers: [
|
|
482
|
+
['h1', 'Header 1'],
|
|
483
|
+
['h2', 'Header 2'],
|
|
484
|
+
],
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
await doc2.chunk({
|
|
488
|
+
strategy: 'html',
|
|
489
|
+
headers: [
|
|
490
|
+
['h1', 'Header 1'],
|
|
491
|
+
['h2', 'Header 2'],
|
|
492
|
+
],
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
await doc3.chunk({
|
|
496
|
+
strategy: 'html',
|
|
497
|
+
headers: [
|
|
498
|
+
['h1', 'Header 1'],
|
|
499
|
+
['h2', 'Header 2'],
|
|
500
|
+
],
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
expect(doc1.getDocs()).toHaveLength(0);
|
|
504
|
+
expect(doc2.getDocs()).toHaveLength(0);
|
|
505
|
+
expect(doc3.getDocs()).toHaveLength(0);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should handle complex nested header hierarchies', async () => {
|
|
509
|
+
const html = `
|
|
510
|
+
<html>
|
|
511
|
+
<body>
|
|
512
|
+
<h1>Main Title</h1>
|
|
513
|
+
<p>Main content</p>
|
|
514
|
+
<h2>Section 1</h2>
|
|
515
|
+
<p>Section 1 content</p>
|
|
516
|
+
<h3>Subsection 1.1</h3>
|
|
517
|
+
<p>Subsection 1.1 content</p>
|
|
518
|
+
<h2>Section 2</h2>
|
|
519
|
+
<h3>Subsection 2.1</h3>
|
|
520
|
+
<p>Subsection 2.1 content</p>
|
|
521
|
+
</body>
|
|
522
|
+
</html>
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
526
|
+
await doc.chunk({
|
|
527
|
+
strategy: 'html',
|
|
528
|
+
headers: [
|
|
529
|
+
['h1', 'Header 1'],
|
|
530
|
+
['h2', 'Header 2'],
|
|
531
|
+
['h3', 'Header 3'],
|
|
532
|
+
],
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const docs = doc.getDocs();
|
|
536
|
+
expect(docs.length).toBeGreaterThan(3);
|
|
537
|
+
expect(docs.some(d => d.metadata?.['Header 1'] === 'Main Title')).toBe(true);
|
|
538
|
+
expect(docs.some(d => d.metadata?.['Header 2'] === 'Section 1')).toBe(true);
|
|
539
|
+
expect(docs.some(d => d.metadata?.['Header 3'] === 'Subsection 1.1')).toBe(true);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle headers with mixed content and special characters', async () => {
|
|
543
|
+
const html = `
|
|
544
|
+
<html>
|
|
545
|
+
<body>
|
|
546
|
+
<h1>Title with <strong>bold</strong> & <em>emphasis</em></h1>
|
|
547
|
+
<p>Content 1</p>
|
|
548
|
+
<h2>Section with <tags> & symbols</h2>
|
|
549
|
+
<p>Content 2</p>
|
|
550
|
+
</body>
|
|
551
|
+
</html>
|
|
552
|
+
`;
|
|
553
|
+
|
|
554
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
555
|
+
await doc.chunk({
|
|
556
|
+
strategy: 'html',
|
|
557
|
+
headers: [
|
|
558
|
+
['h1', 'Header 1'],
|
|
559
|
+
['h2', 'Header 2'],
|
|
560
|
+
],
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const docs = doc.getDocs();
|
|
564
|
+
expect(docs.length).toBeGreaterThan(1);
|
|
565
|
+
expect(docs[0]?.metadata?.['Header 1']).toContain('bold');
|
|
566
|
+
expect(docs[0]?.metadata?.['Header 1']).toContain('&');
|
|
567
|
+
expect(docs[0]?.metadata?.['Header 1']).toContain('emphasis');
|
|
568
|
+
expect(docs[1]?.metadata?.['Header 2']).toContain('<tags>');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should handle headers with no content or whitespace content', async () => {
|
|
572
|
+
const html = `
|
|
573
|
+
<html>
|
|
574
|
+
<body>
|
|
575
|
+
<h1>Empty Section</h1>
|
|
576
|
+
<h2>Whitespace Section</h2>
|
|
577
|
+
|
|
578
|
+
<h2>Valid Section</h2>
|
|
579
|
+
<p>Content</p>
|
|
580
|
+
</body>
|
|
581
|
+
</html>
|
|
582
|
+
`;
|
|
583
|
+
|
|
584
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
585
|
+
await doc.chunk({
|
|
586
|
+
strategy: 'html',
|
|
587
|
+
headers: [
|
|
588
|
+
['h1', 'Header 1'],
|
|
589
|
+
['h2', 'Header 2'],
|
|
590
|
+
],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const docs = doc.getDocs();
|
|
594
|
+
expect(docs.some(d => d.metadata?.['Header 1'] === 'Empty Section')).toBe(true);
|
|
595
|
+
expect(docs.some(d => d.metadata?.['Header 2'] === 'Valid Section')).toBe(true);
|
|
596
|
+
expect(docs.find(d => d.metadata?.['Header 2'] === 'Valid Section')?.text).toContain('Content');
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should generate correct XPaths for deeply nested elements', async () => {
|
|
600
|
+
const html = `
|
|
601
|
+
<html>
|
|
602
|
+
<body>
|
|
603
|
+
<div class="container">
|
|
604
|
+
<section id="main">
|
|
605
|
+
<div>
|
|
606
|
+
<h1>Deeply Nested Title</h1>
|
|
607
|
+
<p>Content</p>
|
|
608
|
+
</div>
|
|
609
|
+
<div>
|
|
610
|
+
<h1>Second Title</h1>
|
|
611
|
+
<p>More Content</p>
|
|
612
|
+
</div>
|
|
613
|
+
</section>
|
|
614
|
+
</div>
|
|
615
|
+
</body>
|
|
616
|
+
</html>
|
|
617
|
+
`;
|
|
618
|
+
|
|
619
|
+
const doc = MDocument.fromHTML(html, { meta: 'data' });
|
|
620
|
+
await doc.chunk({
|
|
621
|
+
strategy: 'html',
|
|
622
|
+
headers: [['h1', 'Header 1']],
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const docs = doc.getDocs();
|
|
626
|
+
expect(docs).toHaveLength(2);
|
|
627
|
+
|
|
628
|
+
// First h1
|
|
629
|
+
expect(docs[0]?.metadata?.['Header 1']).toBe('Deeply Nested Title');
|
|
630
|
+
const xpath1 = docs[0]?.metadata?.xpath as string;
|
|
631
|
+
expect(xpath1).toBeDefined();
|
|
632
|
+
expect(xpath1).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/section\[1\]\/div\[1\]\/h1\[1\]$/);
|
|
633
|
+
|
|
634
|
+
// Second h1
|
|
635
|
+
expect(docs[1]?.metadata?.['Header 1']).toBe('Second Title');
|
|
636
|
+
const xpath2 = docs[1]?.metadata?.xpath as string;
|
|
637
|
+
expect(xpath2).toBeDefined();
|
|
638
|
+
expect(xpath2).toMatch(/^\/html\[1\]\/body\[1\]\/div\[1\]\/section\[1\]\/div\[2\]\/h1\[1\]$/);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
describe('chunkJson', () => {
|
|
643
|
+
describe('Unicode handling', () => {
|
|
644
|
+
it('should handle Unicode characters correctly', async () => {
|
|
645
|
+
const input = {
|
|
646
|
+
key1: '你好',
|
|
647
|
+
key2: '世界',
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const doc = MDocument.fromJSON(JSON.stringify(input), { meta: 'data' });
|
|
651
|
+
|
|
652
|
+
await doc.chunk({
|
|
653
|
+
strategy: 'json',
|
|
654
|
+
maxSize: 50,
|
|
655
|
+
minSize: 50,
|
|
656
|
+
ensureAscii: true,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(doc.getText().some(chunk => chunk.includes('\\u'))).toBe(true);
|
|
660
|
+
|
|
661
|
+
const combined = doc
|
|
662
|
+
.getText()
|
|
663
|
+
.map(chunk => {
|
|
664
|
+
const c = JSON.parse(chunk);
|
|
665
|
+
const retVal: Record<string, string> = {};
|
|
666
|
+
Object.entries(c).forEach(([key, value]) => {
|
|
667
|
+
retVal[key] = JSON.parse(`"${value as string}"`);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
return retVal;
|
|
671
|
+
})
|
|
672
|
+
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
673
|
+
|
|
674
|
+
expect(combined?.key1?.charCodeAt(0)).toBe('你'.charCodeAt(0));
|
|
675
|
+
expect(combined?.key1?.charCodeAt(1)).toBe('好'.charCodeAt(0));
|
|
676
|
+
expect(combined?.key2?.charCodeAt(0)).toBe('世'.charCodeAt(0));
|
|
677
|
+
expect(combined?.key2?.charCodeAt(1)).toBe('界'.charCodeAt(0));
|
|
678
|
+
|
|
679
|
+
expect(combined?.key1).toBe('你好');
|
|
680
|
+
expect(combined?.key2).toBe('世界');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('should handle non-ASCII without escaping when ensureAscii is false', async () => {
|
|
684
|
+
const input = {
|
|
685
|
+
key1: '你好',
|
|
686
|
+
key2: '世界',
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const doc = MDocument.fromJSON(JSON.stringify(input), { meta: 'data' });
|
|
690
|
+
|
|
691
|
+
await doc.chunk({
|
|
692
|
+
strategy: 'json',
|
|
693
|
+
maxSize: 50,
|
|
694
|
+
ensureAscii: false,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
expect(doc.getText().some(chunk => chunk.includes('你好'))).toBe(true);
|
|
698
|
+
|
|
699
|
+
const combined = doc
|
|
700
|
+
.getText()
|
|
701
|
+
.map(chunk => JSON.parse(chunk))
|
|
702
|
+
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
703
|
+
|
|
704
|
+
expect(combined.key1).toBe('你好');
|
|
705
|
+
expect(combined.key2).toBe('世界');
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
describe('chunkToken', () => {
|
|
711
|
+
it('should handle different encodings', async () => {
|
|
712
|
+
const text = 'This is a test text for different encodings.';
|
|
713
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
714
|
+
|
|
715
|
+
await doc.chunk({
|
|
716
|
+
strategy: 'token',
|
|
717
|
+
encodingName: 'cl100k_base',
|
|
718
|
+
size: 10,
|
|
719
|
+
overlap: 2,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const chunks = doc.getText();
|
|
723
|
+
|
|
724
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
725
|
+
expect(chunks.join(' ').trim()).toBe(text);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('should handle special tokens correctly', async () => {
|
|
729
|
+
const text = 'Test text <|endoftext|> more text';
|
|
730
|
+
|
|
731
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
732
|
+
|
|
733
|
+
await doc.chunk({
|
|
734
|
+
strategy: 'token',
|
|
735
|
+
encodingName: 'gpt2',
|
|
736
|
+
size: 10,
|
|
737
|
+
disallowedSpecial: new Set(),
|
|
738
|
+
allowedSpecial: new Set(['<|endoftext|>']),
|
|
739
|
+
overlap: 2,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const chunks = doc.getText();
|
|
743
|
+
|
|
744
|
+
expect(chunks.join(' ').includes('<|endoftext|>')).toBe(true);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should strip whitespace when configured', async () => {
|
|
748
|
+
const text = ' This has whitespace ';
|
|
749
|
+
|
|
750
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
751
|
+
|
|
752
|
+
await doc.chunk({
|
|
753
|
+
strategy: 'token',
|
|
754
|
+
encodingName: 'gpt2',
|
|
755
|
+
size: 10,
|
|
756
|
+
disallowedSpecial: new Set(),
|
|
757
|
+
allowedSpecial: new Set(['<|endoftext|>']),
|
|
758
|
+
overlap: 2,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const chunks = doc.getText();
|
|
762
|
+
|
|
763
|
+
chunks.forEach(chunk => {
|
|
764
|
+
expect(chunk).not.toMatch(/^\s+|\s+$/);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
describe('Error cases', () => {
|
|
769
|
+
it('should throw error for invalid chunk size and overlap', async () => {
|
|
770
|
+
const text = ' This has whitespace ';
|
|
771
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
772
|
+
|
|
773
|
+
await expect(
|
|
774
|
+
doc.chunk({
|
|
775
|
+
strategy: 'token',
|
|
776
|
+
size: 100,
|
|
777
|
+
overlap: 150, // overlap larger than chunk size
|
|
778
|
+
}),
|
|
779
|
+
).rejects.toThrow();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should handle invalid encoding name', async () => {
|
|
783
|
+
const text = ' This has whitespace ';
|
|
784
|
+
const doc = MDocument.fromText(text, { meta: 'data' });
|
|
785
|
+
|
|
786
|
+
await expect(
|
|
787
|
+
doc.chunk({
|
|
788
|
+
strategy: 'token',
|
|
789
|
+
encodingName: 'invalid-encoding' as any,
|
|
790
|
+
size: 100,
|
|
791
|
+
overlap: 150, // overlap larger than chunk size
|
|
792
|
+
}),
|
|
793
|
+
).rejects.toThrow();
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
describe('chunkMarkdown', () => {
|
|
799
|
+
it('should split markdown text correctly', async () => {
|
|
800
|
+
const text = `# Header 1
|
|
801
|
+
|
|
802
|
+
This is some text under header 1.
|
|
803
|
+
|
|
804
|
+
## Header 2
|
|
805
|
+
|
|
806
|
+
This is some text under header 2.
|
|
807
|
+
|
|
808
|
+
### Header 3
|
|
809
|
+
|
|
810
|
+
- List item 1
|
|
811
|
+
- List item 2`;
|
|
812
|
+
|
|
813
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
814
|
+
|
|
815
|
+
await doc.chunk({
|
|
816
|
+
strategy: 'markdown',
|
|
817
|
+
size: 100,
|
|
818
|
+
overlap: 10,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const chunks = doc.getText();
|
|
822
|
+
expect(chunks.length).toBeGreaterThan(1);
|
|
823
|
+
expect(chunks[0]).toContain('# Header 1');
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
it('should handle code blocks', async () => {
|
|
827
|
+
const text = `# Code Example
|
|
828
|
+
|
|
829
|
+
\`\`\`javascript
|
|
830
|
+
function hello() {
|
|
831
|
+
console.log('Hello, World!');
|
|
832
|
+
}
|
|
833
|
+
\`\`\`
|
|
834
|
+
|
|
835
|
+
Regular text after code block.`;
|
|
836
|
+
|
|
837
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
838
|
+
|
|
839
|
+
await doc.chunk({
|
|
840
|
+
strategy: 'markdown',
|
|
841
|
+
size: 100,
|
|
842
|
+
overlap: 10,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const chunks = doc.getText();
|
|
846
|
+
expect(chunks.some(chunk => chunk.includes('```javascript'))).toBe(true);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
describe('MarkdownHeader', () => {
|
|
851
|
+
it('should split on headers and preserve metadata', async () => {
|
|
852
|
+
const text = `# Main Title
|
|
853
|
+
|
|
854
|
+
Some content here.
|
|
855
|
+
|
|
856
|
+
## Section 1
|
|
857
|
+
|
|
858
|
+
Section 1 content.
|
|
859
|
+
|
|
860
|
+
### Subsection 1.1
|
|
861
|
+
|
|
862
|
+
Subsection content.
|
|
863
|
+
|
|
864
|
+
## Section 2
|
|
865
|
+
|
|
866
|
+
Final content.`;
|
|
867
|
+
|
|
868
|
+
const doc = MDocument.fromMarkdown(text);
|
|
869
|
+
|
|
870
|
+
await doc.chunk({
|
|
871
|
+
strategy: 'markdown',
|
|
872
|
+
headers: [
|
|
873
|
+
['#', 'Header 1'],
|
|
874
|
+
['##', 'Header 2'],
|
|
875
|
+
['###', 'Header 3'],
|
|
876
|
+
],
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const docs = doc.getDocs();
|
|
880
|
+
|
|
881
|
+
expect(docs.length).toBeGreaterThan(1);
|
|
882
|
+
expect(docs?.[0]?.metadata?.['Header 1']).toBe('Main Title');
|
|
883
|
+
|
|
884
|
+
const section1 = docs.find(doc => doc?.metadata?.['Header 2'] === 'Section 1');
|
|
885
|
+
expect(section1).toBeDefined();
|
|
886
|
+
expect(section1?.text).toContain('Section 1 content');
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('should handle nested headers correctly', async () => {
|
|
890
|
+
const text = `# Top Level
|
|
891
|
+
|
|
892
|
+
## Section A
|
|
893
|
+
Content A
|
|
894
|
+
|
|
895
|
+
### Subsection A1
|
|
896
|
+
Content A1
|
|
897
|
+
|
|
898
|
+
## Section B
|
|
899
|
+
Content B`;
|
|
900
|
+
|
|
901
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
902
|
+
|
|
903
|
+
await doc.chunk({
|
|
904
|
+
strategy: 'markdown',
|
|
905
|
+
headers: [
|
|
906
|
+
['#', 'Header 1'],
|
|
907
|
+
['##', 'Header 2'],
|
|
908
|
+
['###', 'Header 3'],
|
|
909
|
+
],
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const subsectionDoc = doc.getDocs().find(doc => doc?.metadata?.['Header 3'] === 'Subsection A1');
|
|
913
|
+
expect(subsectionDoc).toBeDefined();
|
|
914
|
+
expect(subsectionDoc?.metadata?.['Header 1']).toBe('Top Level');
|
|
915
|
+
expect(subsectionDoc?.metadata?.['Header 2']).toBe('Section A');
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it('should handle code blocks without splitting them', async () => {
|
|
919
|
+
const text = `# Code Section
|
|
920
|
+
|
|
921
|
+
\`\`\`python
|
|
922
|
+
def hello():
|
|
923
|
+
print("Hello World")
|
|
924
|
+
\`\`\`
|
|
925
|
+
|
|
926
|
+
## Next Section`;
|
|
927
|
+
|
|
928
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
929
|
+
|
|
930
|
+
await doc.chunk({
|
|
931
|
+
strategy: 'markdown',
|
|
932
|
+
headers: [
|
|
933
|
+
['#', 'Header 1'],
|
|
934
|
+
['##', 'Header 2'],
|
|
935
|
+
['###', 'Header 3'],
|
|
936
|
+
],
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const codeDoc = doc.getDocs().find(doc => doc?.text?.includes('```python'));
|
|
940
|
+
expect(codeDoc?.text).toContain('print("Hello World")');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should respect returnEachLine option', async () => {
|
|
944
|
+
const text = `# Title
|
|
945
|
+
|
|
946
|
+
Line 1
|
|
947
|
+
Line 2
|
|
948
|
+
Line 3`;
|
|
949
|
+
|
|
950
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
951
|
+
|
|
952
|
+
await doc.chunk({
|
|
953
|
+
strategy: 'markdown',
|
|
954
|
+
headers: [['#', 'Header 1']],
|
|
955
|
+
returnEachLine: true,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
expect(doc.getDocs().length).toBe(4); // Title + 3 lines
|
|
959
|
+
doc
|
|
960
|
+
.getDocs()
|
|
961
|
+
.slice(1)
|
|
962
|
+
.forEach(doc => {
|
|
963
|
+
expect(doc.metadata?.['Header 1']).toBe('Title');
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it('should handle stripHeaders option', async () => {
|
|
968
|
+
const text = `# Title
|
|
969
|
+
|
|
970
|
+
Content`;
|
|
971
|
+
|
|
972
|
+
const doc = MDocument.fromMarkdown(text, { meta: 'data' });
|
|
973
|
+
|
|
974
|
+
await doc.chunk({
|
|
975
|
+
strategy: 'markdown',
|
|
976
|
+
headers: [['#', 'Header 1']],
|
|
977
|
+
returnEachLine: false,
|
|
978
|
+
stripHeaders: false,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const docs = doc.getDocs();
|
|
982
|
+
expect(docs?.[0]?.text).toContain('# Title');
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
});
|