@alliance-droid/svelte-docs-system 0.0.2 → 0.1.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 +155 -23
- package/dist/components/APITable.svelte.d.ts +21 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +14 -0
- package/dist/components/Callout.svelte.d.ts +15 -0
- package/dist/components/CodeBlock.svelte.d.ts +12 -0
- package/{src/lib → dist}/components/DocLayout.svelte +18 -6
- package/dist/components/DocLayout.svelte.d.ts +18 -0
- package/dist/components/DocsPage.svelte +39 -0
- package/dist/components/DocsPage.svelte.d.ts +8 -0
- package/dist/components/Documentation.svelte +639 -0
- package/dist/components/Footer.svelte.d.ts +10 -0
- package/dist/components/Image.svelte.d.ts +15 -0
- package/{src/lib → dist}/components/Navbar.svelte +4 -4
- package/dist/components/Navbar.svelte.d.ts +10 -0
- package/{src/lib → dist}/components/Search.svelte +2 -2
- package/dist/components/Search.svelte.d.ts +6 -0
- package/{template-starter/src/lib → dist}/components/Sidebar.svelte +2 -2
- package/dist/components/Sidebar.svelte.d.ts +9 -0
- package/dist/components/Tabs.svelte.d.ts +16 -0
- package/dist/config.d.ts +93 -0
- package/dist/config.js +89 -0
- package/dist/configLoader.d.ts +48 -0
- package/dist/configLoader.js +187 -0
- package/dist/configParser.d.ts +27 -0
- package/dist/configParser.js +208 -0
- package/{template-starter/src/lib/index.ts → dist/index.d.ts} +6 -7
- package/dist/index.js +18 -0
- package/dist/navigationBuilder.d.ts +64 -0
- package/dist/navigationBuilder.js +225 -0
- package/dist/plugin.d.ts +30 -0
- package/dist/plugin.js +172 -0
- package/dist/routing.d.ts +48 -0
- package/dist/routing.js +92 -0
- package/dist/stores/i18n.d.ts +20 -0
- package/dist/stores/i18n.js +119 -0
- package/dist/stores/nav.d.ts +20 -0
- package/dist/stores/nav.js +15 -0
- package/dist/stores/search.d.ts +49 -0
- package/dist/stores/search.js +127 -0
- package/dist/stores/theme.d.ts +7 -0
- package/dist/stores/theme.js +152 -0
- package/dist/stores/version.d.ts +18 -0
- package/dist/stores/version.js +93 -0
- package/dist/themeCustomization.d.ts +46 -0
- package/dist/themeCustomization.js +188 -0
- package/dist/utils/highlight.d.ts +13 -0
- package/dist/utils/highlight.js +83 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/markdown.d.ts +40 -0
- package/dist/utils/markdown.js +165 -0
- package/package.json +44 -23
- package/COMPONENTS.md +0 -365
- package/COVERAGE_REPORT.md +0 -663
- package/SEARCH_VERIFICATION.md +0 -229
- package/TEST_SUMMARY.md +0 -344
- package/bin/init.js +0 -821
- package/docs/COMPONENT_LIBRARY_INTEGRATION_REPORT.md +0 -153
- package/docs/DARK_MODE_AUDIT_REPORT.md +0 -403
- package/docs/E2E_TESTS.md +0 -354
- package/docs/TESTING.md +0 -754
- package/docs/THEME_INHERITANCE.md +0 -192
- package/docs/de/index.md +0 -41
- package/docs/en/COMPONENTS.md +0 -443
- package/docs/en/api/examples.md +0 -100
- package/docs/en/api/overview.md +0 -69
- package/docs/en/components/index.md +0 -622
- package/docs/en/config/navigation.md +0 -505
- package/docs/en/config/theme-and-colors.md +0 -395
- package/docs/en/getting-started/integration.md +0 -406
- package/docs/en/guides/common-setups.md +0 -651
- package/docs/en/index.md +0 -243
- package/docs/en/markdown.md +0 -102
- package/docs/en/routing.md +0 -64
- package/docs/en/setup.md +0 -52
- package/docs/en/troubleshooting.md +0 -704
- package/docs/es/index.md +0 -41
- package/docs/fr/index.md +0 -41
- package/docs/ja/index.md +0 -41
- package/pagefind.toml +0 -8
- package/postcss.config.js +0 -5
- package/src/app.css +0 -119
- package/src/app.d.ts +0 -13
- package/src/app.html +0 -11
- package/src/lib/components/APITable.test.ts +0 -153
- package/src/lib/components/Breadcrumbs.test.ts +0 -148
- package/src/lib/components/Callout.test.ts +0 -100
- package/src/lib/components/CodeBlock.test.ts +0 -133
- package/src/lib/components/Image.test.ts +0 -163
- package/src/lib/components/Sidebar.svelte +0 -110
- package/src/lib/components/Tabs.test.ts +0 -102
- package/src/lib/config.test.ts +0 -140
- package/src/lib/config.ts +0 -179
- package/src/lib/configIntegration.test.ts +0 -272
- package/src/lib/configLoader.ts +0 -231
- package/src/lib/configParser.test.ts +0 -217
- package/src/lib/configParser.ts +0 -234
- package/src/lib/index.ts +0 -37
- package/src/lib/integration.test.ts +0 -426
- package/src/lib/navigationBuilder.test.ts +0 -338
- package/src/lib/navigationBuilder.ts +0 -268
- package/src/lib/performance.test.ts +0 -369
- package/src/lib/routing.test.ts +0 -202
- package/src/lib/routing.ts +0 -127
- package/src/lib/search-functionality.test.ts +0 -493
- package/src/lib/stores/i18n.test.ts +0 -180
- package/src/lib/stores/i18n.ts +0 -143
- package/src/lib/stores/nav.ts +0 -36
- package/src/lib/stores/search.test.ts +0 -140
- package/src/lib/stores/search.ts +0 -162
- package/src/lib/stores/theme.test.ts +0 -117
- package/src/lib/stores/theme.ts +0 -167
- package/src/lib/stores/version.test.ts +0 -139
- package/src/lib/stores/version.ts +0 -111
- package/src/lib/themeCustomization.test.ts +0 -223
- package/src/lib/themeCustomization.ts +0 -212
- package/src/lib/utils/highlight.test.ts +0 -136
- package/src/lib/utils/highlight.ts +0 -100
- package/src/lib/utils/index.ts +0 -7
- package/src/lib/utils/markdown.test.ts +0 -357
- package/src/lib/utils/markdown.ts +0 -77
- package/src/routes/+layout.server.ts +0 -1
- package/src/routes/+layout.svelte +0 -29
- package/src/routes/+page.svelte +0 -165
- package/src/routes/quote-demo/+page.svelte +0 -141
- package/static/robots.txt +0 -3
- package/svelte.config.js +0 -15
- package/tailwind.config.ts +0 -55
- package/template-starter/.github/workflows/build.yml +0 -40
- package/template-starter/.github/workflows/deploy-github-pages.yml +0 -47
- package/template-starter/.github/workflows/deploy-netlify.yml +0 -41
- package/template-starter/.github/workflows/deploy-vercel.yml +0 -64
- package/template-starter/NPM-PACKAGE-SETUP.md +0 -233
- package/template-starter/README.md +0 -320
- package/template-starter/docs/_config.json +0 -39
- package/template-starter/docs/api/components.md +0 -257
- package/template-starter/docs/api/overview.md +0 -169
- package/template-starter/docs/guides/configuration.md +0 -145
- package/template-starter/docs/guides/github-pages-deployment.md +0 -254
- package/template-starter/docs/guides/netlify-deployment.md +0 -159
- package/template-starter/docs/guides/vercel-deployment.md +0 -131
- package/template-starter/docs/index.md +0 -49
- package/template-starter/docs/setup.md +0 -149
- package/template-starter/package.json +0 -31
- package/template-starter/pagefind.toml +0 -3
- package/template-starter/postcss.config.js +0 -5
- package/template-starter/src/app.css +0 -34
- package/template-starter/src/app.d.ts +0 -13
- package/template-starter/src/app.html +0 -11
- package/template-starter/src/lib/components/APITable.svelte +0 -120
- package/template-starter/src/lib/components/APITable.test.ts +0 -96
- package/template-starter/src/lib/components/Breadcrumbs.svelte +0 -85
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +0 -82
- package/template-starter/src/lib/components/Callout.svelte +0 -60
- package/template-starter/src/lib/components/Callout.test.ts +0 -91
- package/template-starter/src/lib/components/CodeBlock.svelte +0 -68
- package/template-starter/src/lib/components/CodeBlock.test.ts +0 -62
- package/template-starter/src/lib/components/DocLayout.svelte +0 -84
- package/template-starter/src/lib/components/Footer.svelte +0 -78
- package/template-starter/src/lib/components/Image.svelte +0 -100
- package/template-starter/src/lib/components/Image.test.ts +0 -81
- package/template-starter/src/lib/components/Navbar.svelte +0 -141
- package/template-starter/src/lib/components/Search.svelte +0 -248
- package/template-starter/src/lib/components/Tabs.svelte +0 -48
- package/template-starter/src/lib/components/Tabs.test.ts +0 -89
- package/template-starter/src/routes/+layout.svelte +0 -28
- package/template-starter/src/routes/+page.svelte +0 -92
- package/template-starter/svelte.config.js +0 -17
- package/template-starter/tailwind.config.ts +0 -17
- package/template-starter/tsconfig.json +0 -13
- package/template-starter/vite.config.ts +0 -6
- package/tests/e2e/example.spec.ts +0 -345
- package/tsconfig.json +0 -20
- package/vite.config.ts +0 -6
- package/vitest.config.ts +0 -33
- package/vitest.setup.ts +0 -21
- /package/{src/lib → dist}/assets/favicon.svg +0 -0
- /package/{src/lib → dist}/components/APITable.svelte +0 -0
- /package/{src/lib → dist}/components/Breadcrumbs.svelte +0 -0
- /package/{src/lib → dist}/components/Callout.svelte +0 -0
- /package/{src/lib → dist}/components/CodeBlock.svelte +0 -0
- /package/{src/lib → dist}/components/Footer.svelte +0 -0
- /package/{src/lib → dist}/components/Image.svelte +0 -0
- /package/{src/lib → dist}/components/Tabs.svelte +0 -0
- /package/{src/lib → dist}/svelte-component-library.d.ts +0 -0
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme customization system
|
|
3
|
-
* Generates CSS variables from theme configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ThemeConfig } from './config';
|
|
7
|
-
|
|
8
|
-
export interface CSSVariables {
|
|
9
|
-
[key: string]: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generate CSS custom properties (variables) from theme config
|
|
14
|
-
* These can be injected into the document or stylesheet
|
|
15
|
-
*/
|
|
16
|
-
export function generateCSSVariables(theme: ThemeConfig): CSSVariables {
|
|
17
|
-
const vars: CSSVariables = {};
|
|
18
|
-
|
|
19
|
-
// Color variables
|
|
20
|
-
if (theme.primary) vars['--color-primary'] = theme.primary;
|
|
21
|
-
if (theme.secondary) vars['--color-secondary'] = theme.secondary;
|
|
22
|
-
if (theme.textLight) vars['--color-text-light'] = theme.textLight;
|
|
23
|
-
if (theme.textDark) vars['--color-text-dark'] = theme.textDark;
|
|
24
|
-
if (theme.bgLight) vars['--color-bg-light'] = theme.bgLight;
|
|
25
|
-
if (theme.bgDark) vars['--color-bg-dark'] = theme.bgDark;
|
|
26
|
-
if (theme.sidebarBg) vars['--color-sidebar-bg'] = theme.sidebarBg;
|
|
27
|
-
if (theme.navbarBg) vars['--color-navbar-bg'] = theme.navbarBg;
|
|
28
|
-
if (theme.codeBg) vars['--color-code-bg'] = theme.codeBg;
|
|
29
|
-
|
|
30
|
-
// Font variables
|
|
31
|
-
if (theme.fontFamily) vars['--font-family-body'] = theme.fontFamily;
|
|
32
|
-
if (theme.headingFont) vars['--font-family-heading'] = theme.headingFont;
|
|
33
|
-
|
|
34
|
-
return vars;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Apply CSS variables to the document root
|
|
39
|
-
*/
|
|
40
|
-
export function applyCSSVariables(vars: CSSVariables): void {
|
|
41
|
-
if (typeof window === 'undefined') {
|
|
42
|
-
return; // Skip in SSR
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const root = document.documentElement;
|
|
46
|
-
Object.entries(vars).forEach(([key, value]) => {
|
|
47
|
-
root.style.setProperty(key, value);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Create a stylesheet string from CSS variables
|
|
53
|
-
* Useful for injecting into <style> tags
|
|
54
|
-
*/
|
|
55
|
-
export function createCSSVariablesStylesheet(vars: CSSVariables): string {
|
|
56
|
-
const entries = Object.entries(vars)
|
|
57
|
-
.map(([key, value]) => ` ${key}: ${value};`)
|
|
58
|
-
.join('\n');
|
|
59
|
-
|
|
60
|
-
return `:root {\n${entries}\n}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Parse a color value and validate it
|
|
65
|
-
*/
|
|
66
|
-
export function parseColor(color: string): { valid: boolean; value?: string; error?: string } {
|
|
67
|
-
if (!color || typeof color !== 'string') {
|
|
68
|
-
return { valid: false, error: 'Color must be a non-empty string' };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const trimmed = color.trim();
|
|
72
|
-
|
|
73
|
-
// Hex colors
|
|
74
|
-
if (/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?([0-9a-fA-F]{2})?$/.test(trimmed)) {
|
|
75
|
-
return { valid: true, value: trimmed };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// RGB/RGBA
|
|
79
|
-
if (/^rgba?\s*\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(\s*,\s*[\d.]+)?\s*\)$/.test(trimmed)) {
|
|
80
|
-
return { valid: true, value: trimmed };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Named colors (basic list)
|
|
84
|
-
const namedColors = [
|
|
85
|
-
'red',
|
|
86
|
-
'blue',
|
|
87
|
-
'green',
|
|
88
|
-
'white',
|
|
89
|
-
'black',
|
|
90
|
-
'gray',
|
|
91
|
-
'grey',
|
|
92
|
-
'transparent',
|
|
93
|
-
'currentColor',
|
|
94
|
-
];
|
|
95
|
-
if (namedColors.includes(trimmed.toLowerCase())) {
|
|
96
|
-
return { valid: true, value: trimmed };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// CSS custom property
|
|
100
|
-
if (/^var\s*\(\s*--[a-zA-Z0-9-]+\s*\)$/.test(trimmed)) {
|
|
101
|
-
return { valid: true, value: trimmed };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return { valid: false, error: `Invalid color format: ${color}` };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Validate theme configuration
|
|
109
|
-
*/
|
|
110
|
-
export function validateTheme(theme: ThemeConfig): { valid: boolean; errors: string[] } {
|
|
111
|
-
const errors: string[] = [];
|
|
112
|
-
|
|
113
|
-
if (!theme || typeof theme !== 'object') {
|
|
114
|
-
return { valid: false, errors: ['Theme must be an object'] };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Validate colors
|
|
118
|
-
const colorFields = ['primary', 'secondary', 'textLight', 'textDark', 'bgLight', 'bgDark', 'sidebarBg', 'navbarBg', 'codeBg'] as const;
|
|
119
|
-
colorFields.forEach((field) => {
|
|
120
|
-
if (theme[field] !== undefined) {
|
|
121
|
-
const validation = parseColor(theme[field]!);
|
|
122
|
-
if (!validation.valid) {
|
|
123
|
-
errors.push(`${field}: ${validation.error}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// Validate fonts
|
|
129
|
-
if (theme.fontFamily !== undefined && typeof theme.fontFamily !== 'string') {
|
|
130
|
-
errors.push('fontFamily must be a string');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (theme.headingFont !== undefined && typeof theme.headingFont !== 'string') {
|
|
134
|
-
errors.push('headingFont must be a string');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { valid: errors.length === 0, errors };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Merge theme configs, with priority to the first argument
|
|
142
|
-
*/
|
|
143
|
-
export function mergeThemes(primary: ThemeConfig, secondary: ThemeConfig): ThemeConfig {
|
|
144
|
-
return {
|
|
145
|
-
...secondary,
|
|
146
|
-
...primary,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get a predefined theme template
|
|
152
|
-
*/
|
|
153
|
-
export type ThemeTemplate = 'default' | 'dark' | 'minimal' | 'colorful';
|
|
154
|
-
|
|
155
|
-
export function getThemeTemplate(template: ThemeTemplate = 'default'): ThemeConfig {
|
|
156
|
-
const templates: Record<ThemeTemplate, ThemeConfig> = {
|
|
157
|
-
default: {
|
|
158
|
-
primary: '#0066cc',
|
|
159
|
-
secondary: '#ff6b6b',
|
|
160
|
-
textLight: '#333333',
|
|
161
|
-
textDark: '#f0f0f0',
|
|
162
|
-
bgLight: '#ffffff',
|
|
163
|
-
bgDark: '#1a1a1a',
|
|
164
|
-
sidebarBg: '#f5f5f5',
|
|
165
|
-
navbarBg: '#ffffff',
|
|
166
|
-
codeBg: '#f4f4f4',
|
|
167
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
168
|
-
headingFont: 'system-ui, -apple-system, sans-serif',
|
|
169
|
-
},
|
|
170
|
-
dark: {
|
|
171
|
-
primary: '#4da6ff',
|
|
172
|
-
secondary: '#ff9999',
|
|
173
|
-
textLight: '#e0e0e0',
|
|
174
|
-
textDark: '#1a1a1a',
|
|
175
|
-
bgLight: '#1a1a1a',
|
|
176
|
-
bgDark: '#0d0d0d',
|
|
177
|
-
sidebarBg: '#262626',
|
|
178
|
-
navbarBg: '#1a1a1a',
|
|
179
|
-
codeBg: '#2d2d2d',
|
|
180
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
181
|
-
headingFont: 'system-ui, -apple-system, sans-serif',
|
|
182
|
-
},
|
|
183
|
-
minimal: {
|
|
184
|
-
primary: '#000000',
|
|
185
|
-
secondary: '#666666',
|
|
186
|
-
textLight: '#333333',
|
|
187
|
-
textDark: '#cccccc',
|
|
188
|
-
bgLight: '#ffffff',
|
|
189
|
-
bgDark: '#181818',
|
|
190
|
-
sidebarBg: '#fafafa',
|
|
191
|
-
navbarBg: '#ffffff',
|
|
192
|
-
codeBg: '#eeeeee',
|
|
193
|
-
fontFamily: 'Georgia, serif',
|
|
194
|
-
headingFont: 'Georgia, serif',
|
|
195
|
-
},
|
|
196
|
-
colorful: {
|
|
197
|
-
primary: '#ff006e',
|
|
198
|
-
secondary: '#00d9ff',
|
|
199
|
-
textLight: '#2d3142',
|
|
200
|
-
textDark: '#e0e0e0',
|
|
201
|
-
bgLight: '#fafafa',
|
|
202
|
-
bgDark: '#0d1b2a',
|
|
203
|
-
sidebarBg: '#f0f3ff',
|
|
204
|
-
navbarBg: '#ffffff',
|
|
205
|
-
codeBg: '#f5f5f5',
|
|
206
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
207
|
-
headingFont: '"Segoe UI", Tahoma, sans-serif',
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
return templates[template] || templates.default;
|
|
212
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { highlightSearchTerms, truncateText, extractExcerpt } from './highlight';
|
|
3
|
-
|
|
4
|
-
describe('Highlight Utilities', () => {
|
|
5
|
-
describe('highlightSearchTerms', () => {
|
|
6
|
-
it('should highlight single search term', () => {
|
|
7
|
-
const text = 'This is a test sentence';
|
|
8
|
-
const result = highlightSearchTerms(text, 'test');
|
|
9
|
-
expect(result).toContain('<mark>test</mark>');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should highlight multiple occurrences', () => {
|
|
13
|
-
const text = 'test case with test words';
|
|
14
|
-
const result = highlightSearchTerms(text, 'test');
|
|
15
|
-
const matches = (result.match(/<mark>test<\/mark>/g) || []).length;
|
|
16
|
-
expect(matches).toBe(2);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should handle multiple search terms', () => {
|
|
20
|
-
const text = 'This is a test case';
|
|
21
|
-
const result = highlightSearchTerms(text, 'test case');
|
|
22
|
-
expect(result).toContain('<mark>test</mark>');
|
|
23
|
-
expect(result).toContain('<mark>case</mark>');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should be case insensitive', () => {
|
|
27
|
-
const text = 'TEST and Test and test';
|
|
28
|
-
const result = highlightSearchTerms(text, 'test');
|
|
29
|
-
const matches = (result.match(/<mark>/g) || []).length;
|
|
30
|
-
expect(matches).toBe(3);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should handle special regex characters', () => {
|
|
34
|
-
const text = 'This (test) has [special] {chars}';
|
|
35
|
-
const result = highlightSearchTerms(text, 'test');
|
|
36
|
-
expect(result).toContain('<mark>test</mark>');
|
|
37
|
-
expect(result).not.toThrow;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should return original text if search query is empty', () => {
|
|
41
|
-
const text = 'This is a test';
|
|
42
|
-
const result = highlightSearchTerms(text, '');
|
|
43
|
-
expect(result).toBe(text);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should return empty string if text is empty', () => {
|
|
47
|
-
const result = highlightSearchTerms('', 'test');
|
|
48
|
-
expect(result).toBe('');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should use word boundaries', () => {
|
|
52
|
-
const text = 'testing test';
|
|
53
|
-
const result = highlightSearchTerms(text, 'test');
|
|
54
|
-
// Should highlight 'test' in 'testing' as part of word, and 'test' as standalone
|
|
55
|
-
// Actually, \b word boundary means 'test' won't match in 'testing'
|
|
56
|
-
const matches = (result.match(/<mark>test<\/mark>/g) || []).length;
|
|
57
|
-
expect(matches).toBe(1); // Only the standalone 'test'
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
describe('truncateText', () => {
|
|
62
|
-
it('should not truncate short text', () => {
|
|
63
|
-
const text = 'Short text';
|
|
64
|
-
const result = truncateText(text, 50);
|
|
65
|
-
expect(result).toBe(text);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should truncate long text with ellipsis', () => {
|
|
69
|
-
const text = 'This is a very long text that should be truncated';
|
|
70
|
-
const result = truncateText(text, 20);
|
|
71
|
-
expect(result).toContain('...');
|
|
72
|
-
expect(result.length).toBeLessThan(text.length);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should truncate at word boundary if possible', () => {
|
|
76
|
-
const text = 'This is a test sentence for truncation';
|
|
77
|
-
const result = truncateText(text, 15);
|
|
78
|
-
expect(result.endsWith('...')).toBe(true);
|
|
79
|
-
// Result should have ellipsis and be shorter
|
|
80
|
-
expect(result.length).toBeLessThan(text.length);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should use default max length', () => {
|
|
84
|
-
const longText = 'a'.repeat(200);
|
|
85
|
-
const result = truncateText(longText);
|
|
86
|
-
expect(result).toContain('...');
|
|
87
|
-
expect(result.length).toBeLessThan(longText.length);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('extractExcerpt', () => {
|
|
92
|
-
it('should extract text around search term', () => {
|
|
93
|
-
const text = 'The quick brown fox jumps over the lazy dog';
|
|
94
|
-
const result = extractExcerpt(text, 'brown', 20);
|
|
95
|
-
expect(result).toContain('brown');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should return truncated text if no search query', () => {
|
|
99
|
-
const text = 'a'.repeat(300);
|
|
100
|
-
const result = extractExcerpt(text, '', 50);
|
|
101
|
-
expect(result).toContain('...');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should handle short excerpts', () => {
|
|
105
|
-
const text = 'The quick brown fox jumps';
|
|
106
|
-
const result = extractExcerpt(text, 'brown', 30);
|
|
107
|
-
expect(result).toContain('brown');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should handle longer text with excerpt', () => {
|
|
111
|
-
const text = 'The quick brown fox jumps over lazy dog';
|
|
112
|
-
const result = extractExcerpt(text, 'brown', 50);
|
|
113
|
-
expect(result).toContain('brown');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should handle search term at start', () => {
|
|
117
|
-
const text = 'brown fox jumps';
|
|
118
|
-
const result = extractExcerpt(text, 'brown', 50);
|
|
119
|
-
expect(result).toContain('brown');
|
|
120
|
-
expect(result.startsWith('...')).toBe(false);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should handle search term at end', () => {
|
|
124
|
-
const text = 'the quick brown';
|
|
125
|
-
const result = extractExcerpt(text, 'brown', 50);
|
|
126
|
-
expect(result).toContain('brown');
|
|
127
|
-
expect(result.endsWith('...')).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should be case insensitive', () => {
|
|
131
|
-
const text = 'The Brown fox';
|
|
132
|
-
const result = extractExcerpt(text, 'brown', 50);
|
|
133
|
-
expect(result.toLowerCase()).toContain('brown');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Highlight search terms in text
|
|
3
|
-
* Returns HTML with highlighted terms wrapped in <mark> tags
|
|
4
|
-
*/
|
|
5
|
-
export function highlightSearchTerms(text: string, searchQuery: string): string {
|
|
6
|
-
if (!text || !searchQuery.trim()) {
|
|
7
|
-
return text;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// Split search query into individual terms and escape special regex characters
|
|
11
|
-
const terms = searchQuery
|
|
12
|
-
.trim()
|
|
13
|
-
.split(/\s+/)
|
|
14
|
-
.filter((term) => term.length > 0)
|
|
15
|
-
.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
16
|
-
|
|
17
|
-
if (terms.length === 0) {
|
|
18
|
-
return text;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Create a regex that matches any of the terms (case-insensitive)
|
|
22
|
-
const regex = new RegExp(`\\b(${terms.join('|')})\\b`, 'gi');
|
|
23
|
-
|
|
24
|
-
// Replace matches with highlighted version
|
|
25
|
-
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Truncate text to a maximum length and ensure it ends at a word boundary
|
|
30
|
-
*/
|
|
31
|
-
export function truncateText(text: string, maxLength: number = 150): string {
|
|
32
|
-
if (text.length <= maxLength) {
|
|
33
|
-
return text;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Truncate and find the last space
|
|
37
|
-
const truncated = text.substring(0, maxLength);
|
|
38
|
-
const lastSpace = truncated.lastIndexOf(' ');
|
|
39
|
-
|
|
40
|
-
if (lastSpace > maxLength * 0.7) {
|
|
41
|
-
// If the last space is relatively close to the end, use it
|
|
42
|
-
return truncated.substring(0, lastSpace) + '...';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Otherwise just truncate and add ellipsis
|
|
46
|
-
return truncated + '...';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Extract excerpt around search terms
|
|
51
|
-
*/
|
|
52
|
-
export function extractExcerpt(text: string, searchQuery: string, maxLength: number = 200): string {
|
|
53
|
-
if (!text || !searchQuery.trim()) {
|
|
54
|
-
return truncateText(text, maxLength);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const searchTerms = searchQuery
|
|
58
|
-
.trim()
|
|
59
|
-
.split(/\s+/)
|
|
60
|
-
.filter((term) => term.length > 0)
|
|
61
|
-
.map((term) => term.toLowerCase());
|
|
62
|
-
|
|
63
|
-
// Find the position of the first search term in the text
|
|
64
|
-
let earliestIndex = text.length;
|
|
65
|
-
for (const term of searchTerms) {
|
|
66
|
-
const index = text.toLowerCase().indexOf(term);
|
|
67
|
-
if (index !== -1 && index < earliestIndex) {
|
|
68
|
-
earliestIndex = index;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Extract context around the search term
|
|
73
|
-
let startIndex = Math.max(0, earliestIndex - 50);
|
|
74
|
-
let endIndex = Math.min(text.length, startIndex + maxLength);
|
|
75
|
-
|
|
76
|
-
// Adjust if we're too close to the end
|
|
77
|
-
if (endIndex === text.length && startIndex > 0) {
|
|
78
|
-
startIndex = Math.max(0, endIndex - maxLength);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let excerpt = text.substring(startIndex, endIndex);
|
|
82
|
-
|
|
83
|
-
// Remove partial words at the beginning
|
|
84
|
-
if (startIndex > 0) {
|
|
85
|
-
const firstSpace = excerpt.indexOf(' ');
|
|
86
|
-
if (firstSpace !== -1 && firstSpace < 20) {
|
|
87
|
-
excerpt = excerpt.substring(firstSpace + 1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Add ellipsis if needed
|
|
92
|
-
if (startIndex > 0) {
|
|
93
|
-
excerpt = '...' + excerpt;
|
|
94
|
-
}
|
|
95
|
-
if (endIndex < text.length) {
|
|
96
|
-
excerpt = excerpt + '...';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return excerpt;
|
|
100
|
-
}
|