@mintlify/common 1.0.1137 → 1.0.1139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mdx/plugins/remark/remarkMdxRemoveJs.d.ts +4 -0
- package/dist/mdx/plugins/remark/remarkMdxRemoveJs.js +78 -13
- package/dist/search/highlight.d.ts +7 -0
- package/dist/search/highlight.js +400 -0
- package/dist/search/highlight.test.d.ts +1 -0
- package/dist/search/highlight.test.js +176 -0
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.js +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { getHighlightedSnippet, getHighlightedText, stripDuplicatedTitleLine, } from './highlight.js';
|
|
3
|
+
const PRE = '<mark><b>';
|
|
4
|
+
const POST = '</b></mark>';
|
|
5
|
+
const ELLIPSIS = '…';
|
|
6
|
+
describe('getHighlightedText', () => {
|
|
7
|
+
test('returns escaped text unchanged when query is empty', () => {
|
|
8
|
+
expect(getHighlightedText('Quickstart', '')).toBe('Quickstart');
|
|
9
|
+
expect(getHighlightedText('', 'foo')).toBe('');
|
|
10
|
+
});
|
|
11
|
+
test('returns escaped text unchanged when no match is found', () => {
|
|
12
|
+
expect(getHighlightedText('Quickstart', 'foobar')).toBe('Quickstart');
|
|
13
|
+
});
|
|
14
|
+
test('wraps a case-insensitive match in mark/b tags and preserves original casing', () => {
|
|
15
|
+
expect(getHighlightedText('Quickstart', 'quickstart')).toBe(`${PRE}Quickstart${POST}`);
|
|
16
|
+
});
|
|
17
|
+
test('highlights every occurrence of the query', () => {
|
|
18
|
+
expect(getHighlightedText('search and search again', 'search')).toBe(`${PRE}search${POST} and ${PRE}search${POST} again`);
|
|
19
|
+
});
|
|
20
|
+
test('escapes HTML in the surrounding text', () => {
|
|
21
|
+
expect(getHighlightedText('<script>quickstart</script>', 'quickstart')).toBe(`<script>${PRE}quickstart${POST}</script>`);
|
|
22
|
+
});
|
|
23
|
+
test('highlights matches across a full chunk without truncating', () => {
|
|
24
|
+
const content = Array.from({ length: 200 }, (_, i) => (i === 100 ? 'target' : `w${i}`)).join(' ');
|
|
25
|
+
const result = getHighlightedText(content, 'target');
|
|
26
|
+
expect(result).toContain(`${PRE}target${POST}`);
|
|
27
|
+
expect(result).toContain('w0');
|
|
28
|
+
expect(result).toContain('w199');
|
|
29
|
+
});
|
|
30
|
+
test('highlights multi-word query terms', () => {
|
|
31
|
+
const content = 'Learn how to configure webhooks for your application and receive events';
|
|
32
|
+
const result = getHighlightedText(content, 'configure webhooks');
|
|
33
|
+
expect(result).toContain(`${PRE}configure webhooks${POST}`);
|
|
34
|
+
});
|
|
35
|
+
test('preserves original case in highlighted text', () => {
|
|
36
|
+
const content = 'The TypeScript SDK provides type safety';
|
|
37
|
+
const result = getHighlightedText(content, 'typescript sdk');
|
|
38
|
+
expect(result).toContain(`${PRE}TypeScript SDK${POST}`);
|
|
39
|
+
});
|
|
40
|
+
test('escapes HTML in non-highlighted content to prevent XSS', () => {
|
|
41
|
+
const content = 'Use <script>alert("xss")</script> to test the API endpoint';
|
|
42
|
+
const result = getHighlightedText(content, 'API');
|
|
43
|
+
expect(result).not.toContain('<script>');
|
|
44
|
+
expect(result).toContain('<script>');
|
|
45
|
+
expect(result).toContain(`${PRE}API${POST}`);
|
|
46
|
+
});
|
|
47
|
+
test('respects word boundaries when only padded variations match', () => {
|
|
48
|
+
const result = getHighlightedText('shellofoo and a stray foo here', 'foo bar');
|
|
49
|
+
expect(result).toContain(`stray ${PRE}foo${POST} here`);
|
|
50
|
+
expect(result).not.toContain(`shello${PRE}foo${POST}`);
|
|
51
|
+
});
|
|
52
|
+
test('highlights standalone words when a compound match also appears', () => {
|
|
53
|
+
const result = getHighlightedText('foo bar here, also foo standalone, and bar elsewhere', 'foo bar');
|
|
54
|
+
expect(result).toContain(`${PRE}foo bar${POST}`);
|
|
55
|
+
expect(result).toContain(`also ${PRE}foo${POST} standalone`);
|
|
56
|
+
expect(result).toContain(`and ${PRE}bar${POST} elsewhere`);
|
|
57
|
+
});
|
|
58
|
+
test('highlights an individual query word inside a short text segment', () => {
|
|
59
|
+
expect(getHighlightedText('Webhooks', 'configure webhooks')).toBe(`${PRE}Webhooks${POST}`);
|
|
60
|
+
});
|
|
61
|
+
test('highlights a word prefix when the query is a partial word', () => {
|
|
62
|
+
expect(getHighlightedText('Test the integration', 'tes')).toBe(`${PRE}Tes${POST}t the integration`);
|
|
63
|
+
expect(getHighlightedText('Testing your proxy setup', 'tes')).toBe(`${PRE}Tes${POST}ting your proxy setup`);
|
|
64
|
+
});
|
|
65
|
+
test('highlights the full word when the query matches it exactly', () => {
|
|
66
|
+
expect(getHighlightedText('Test the integration', 'test')).toBe(`${PRE}Test${POST} the integration`);
|
|
67
|
+
});
|
|
68
|
+
test('does not highlight a partial match in the middle of a word', () => {
|
|
69
|
+
expect(getHighlightedText('latest greatest', 'test')).toBe('latest greatest');
|
|
70
|
+
});
|
|
71
|
+
test('strips server-emitted <mark><b> highlight tags before re-highlighting', () => {
|
|
72
|
+
expect(getHighlightedText('<mark><b>Install</b></mark> the Mintlify CLI', 'install')).toBe(`${PRE}Install${POST} the Mintlify CLI`);
|
|
73
|
+
});
|
|
74
|
+
test('strips server highlight tags even when query has no match', () => {
|
|
75
|
+
expect(getHighlightedText('<mark><b>Install</b></mark> the CLI', 'foobar')).toBe('Install the CLI');
|
|
76
|
+
});
|
|
77
|
+
test('strips server highlight tags when query is empty', () => {
|
|
78
|
+
expect(getHighlightedText('<mark><b>Install</b></mark> the CLI', '')).toBe('Install the CLI');
|
|
79
|
+
});
|
|
80
|
+
test('completes within timeout for very long queries', () => {
|
|
81
|
+
const content = 'A '.repeat(1000) + 'needle ' + 'B '.repeat(1000);
|
|
82
|
+
const longQuery = Array.from({ length: 30 }, (_, i) => `word${i}`).join(' ');
|
|
83
|
+
const start = Date.now();
|
|
84
|
+
getHighlightedText(content, longQuery);
|
|
85
|
+
const elapsed = Date.now() - start;
|
|
86
|
+
expect(elapsed).toBeLessThan(500);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe('getHighlightedSnippet', () => {
|
|
90
|
+
test('returns escaped text unchanged when query is empty', () => {
|
|
91
|
+
expect(getHighlightedSnippet('Quickstart', '')).toBe('Quickstart');
|
|
92
|
+
expect(getHighlightedSnippet('', 'foo')).toBe('');
|
|
93
|
+
});
|
|
94
|
+
test('returns escaped text unchanged when no match is found', () => {
|
|
95
|
+
expect(getHighlightedSnippet('Quickstart guide', 'foobar')).toBe('Quickstart guide');
|
|
96
|
+
});
|
|
97
|
+
test('does not window when the match is near the start', () => {
|
|
98
|
+
expect(getHighlightedSnippet('Quickstart guide for the API', 'quickstart')).toBe(`${PRE}Quickstart${POST} guide for the API`);
|
|
99
|
+
});
|
|
100
|
+
test('windows around a match buried deep in the content', () => {
|
|
101
|
+
const boilerplate = 'REST Endpoint POST /v1/resources Summary Creates a resource Description This endpoint ';
|
|
102
|
+
const content = `${boilerplate}accepts a customerEmail field in the request body`;
|
|
103
|
+
const result = getHighlightedSnippet(content, 'customerEmail');
|
|
104
|
+
expect(result.startsWith(`${ELLIPSIS} `)).toBe(true);
|
|
105
|
+
expect(result).toContain(`${PRE}customerEmail${POST}`);
|
|
106
|
+
// Leading boilerplate is windowed out of the snippet.
|
|
107
|
+
expect(result).not.toContain('REST Endpoint POST');
|
|
108
|
+
});
|
|
109
|
+
test('keeps preceding context before the match within the leading window', () => {
|
|
110
|
+
const content = 'word '.repeat(40) + 'before target needle after';
|
|
111
|
+
const result = getHighlightedSnippet(content, 'needle');
|
|
112
|
+
expect(result).toContain(`${PRE}needle${POST}`);
|
|
113
|
+
expect(result).toContain('target');
|
|
114
|
+
});
|
|
115
|
+
test('adds a trailing ellipsis when content is windowed at both ends', () => {
|
|
116
|
+
const lead = 'lorem ipsum dolor sit amet '.repeat(5);
|
|
117
|
+
const tail = ' consectetur adipiscing elit sed do eiusmod tempor incididunt'.repeat(10);
|
|
118
|
+
const content = `${lead}MATCHWORD${tail}`;
|
|
119
|
+
const result = getHighlightedSnippet(content, 'matchword');
|
|
120
|
+
expect(result.startsWith(`${ELLIPSIS} `)).toBe(true);
|
|
121
|
+
expect(result.endsWith(` ${ELLIPSIS}`)).toBe(true);
|
|
122
|
+
expect(result).toContain(`${PRE}MATCHWORD${POST}`);
|
|
123
|
+
});
|
|
124
|
+
test('does not add a trailing ellipsis when the match is at the very end', () => {
|
|
125
|
+
const content = 'word '.repeat(40) + 'finalToken';
|
|
126
|
+
const result = getHighlightedSnippet(content, 'finalToken');
|
|
127
|
+
expect(result.startsWith(`${ELLIPSIS} `)).toBe(true);
|
|
128
|
+
expect(result.endsWith(`${PRE}finalToken${POST}`)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
test('escapes HTML in the windowed snippet', () => {
|
|
131
|
+
const content = 'intro text '.repeat(10) + '<script>danger</script> targetField value';
|
|
132
|
+
const result = getHighlightedSnippet(content, 'targetField');
|
|
133
|
+
expect(result).not.toContain('<script>');
|
|
134
|
+
expect(result).toContain('<script>');
|
|
135
|
+
expect(result).toContain(`${PRE}targetField${POST}`);
|
|
136
|
+
});
|
|
137
|
+
test('caps output length even when the match is near the start', () => {
|
|
138
|
+
const content = 'match ' + 'word '.repeat(400);
|
|
139
|
+
const result = getHighlightedSnippet(content, 'match', { maxLength: 40 });
|
|
140
|
+
expect(result.startsWith(`${PRE}match${POST}`)).toBe(true);
|
|
141
|
+
expect(result.endsWith(` ${ELLIPSIS}`)).toBe(true);
|
|
142
|
+
// No leading ellipsis since the window starts at the beginning.
|
|
143
|
+
expect(result.startsWith(`${ELLIPSIS}`)).toBe(false);
|
|
144
|
+
// Bounded output rather than the full ~2k-char chunk.
|
|
145
|
+
expect(result.length).toBeLessThan(120);
|
|
146
|
+
});
|
|
147
|
+
test('respects a custom leading context', () => {
|
|
148
|
+
const content = 'aaaa bbbb cccc dddd eeee ffff gggg needle tail';
|
|
149
|
+
const result = getHighlightedSnippet(content, 'needle', { leadingContext: 5 });
|
|
150
|
+
expect(result.startsWith(`${ELLIPSIS} `)).toBe(true);
|
|
151
|
+
expect(result).toContain(`${PRE}needle${POST}`);
|
|
152
|
+
expect(result).not.toContain('aaaa');
|
|
153
|
+
});
|
|
154
|
+
test('caps the leading window when there is no query or no match', () => {
|
|
155
|
+
const content = 'word '.repeat(100).trim();
|
|
156
|
+
const noQuery = getHighlightedSnippet(content, '', { maxLength: 40 });
|
|
157
|
+
const noMatch = getHighlightedSnippet(content, 'zzz', { maxLength: 40 });
|
|
158
|
+
for (const result of [noQuery, noMatch]) {
|
|
159
|
+
expect(result.endsWith(` ${ELLIPSIS}`)).toBe(true);
|
|
160
|
+
expect(result.length).toBeLessThan(60);
|
|
161
|
+
expect(result).not.toContain(PRE);
|
|
162
|
+
}
|
|
163
|
+
expect(getHighlightedSnippet('short text', 'zzz', { maxLength: 40 })).toBe('short text');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('stripDuplicatedTitleLine', () => {
|
|
167
|
+
test('drops a first line that repeats the header', () => {
|
|
168
|
+
expect(stripDuplicatedTitleLine('Install\nRun npm install', 'Install')).toBe('Run npm install');
|
|
169
|
+
expect(stripDuplicatedTitleLine('<mark>Install</mark>\nRun it', 'Install')).toBe('Run it');
|
|
170
|
+
});
|
|
171
|
+
test('leaves content alone otherwise', () => {
|
|
172
|
+
expect(stripDuplicatedTitleLine('Overview\nRun it', 'Install')).toBe('Overview\nRun it');
|
|
173
|
+
expect(stripDuplicatedTitleLine('Install', 'Install')).toBe('Install');
|
|
174
|
+
expect(stripDuplicatedTitleLine('Install\nRun it', '')).toBe('Install\nRun it');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './highlight.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './highlight.js';
|