@alliance-droid/svelte-docs-system 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/COMPONENTS.md +365 -0
  2. package/COVERAGE_REPORT.md +663 -0
  3. package/README.md +42 -0
  4. package/SEARCH_VERIFICATION.md +229 -0
  5. package/TEST_SUMMARY.md +344 -0
  6. package/bin/init.js +821 -0
  7. package/docs/E2E_TESTS.md +354 -0
  8. package/docs/TESTING.md +754 -0
  9. package/docs/de/index.md +41 -0
  10. package/docs/en/COMPONENTS.md +443 -0
  11. package/docs/en/api/examples.md +100 -0
  12. package/docs/en/api/overview.md +69 -0
  13. package/docs/en/components/index.md +622 -0
  14. package/docs/en/config/navigation.md +505 -0
  15. package/docs/en/config/theme-and-colors.md +395 -0
  16. package/docs/en/getting-started/integration.md +406 -0
  17. package/docs/en/guides/common-setups.md +651 -0
  18. package/docs/en/index.md +243 -0
  19. package/docs/en/markdown.md +102 -0
  20. package/docs/en/routing.md +64 -0
  21. package/docs/en/setup.md +52 -0
  22. package/docs/en/troubleshooting.md +704 -0
  23. package/docs/es/index.md +41 -0
  24. package/docs/fr/index.md +41 -0
  25. package/docs/ja/index.md +41 -0
  26. package/package.json +40 -0
  27. package/pagefind.toml +8 -0
  28. package/postcss.config.js +5 -0
  29. package/src/app.css +119 -0
  30. package/src/app.d.ts +13 -0
  31. package/src/app.html +11 -0
  32. package/src/lib/assets/favicon.svg +1 -0
  33. package/src/lib/components/APITable.svelte +120 -0
  34. package/src/lib/components/APITable.test.ts +153 -0
  35. package/src/lib/components/Breadcrumbs.svelte +85 -0
  36. package/src/lib/components/Breadcrumbs.test.ts +148 -0
  37. package/src/lib/components/Callout.svelte +60 -0
  38. package/src/lib/components/Callout.test.ts +100 -0
  39. package/src/lib/components/CodeBlock.svelte +68 -0
  40. package/src/lib/components/CodeBlock.test.ts +133 -0
  41. package/src/lib/components/DocLayout.svelte +84 -0
  42. package/src/lib/components/Footer.svelte +78 -0
  43. package/src/lib/components/Image.svelte +100 -0
  44. package/src/lib/components/Image.test.ts +163 -0
  45. package/src/lib/components/Navbar.svelte +141 -0
  46. package/src/lib/components/Search.svelte +248 -0
  47. package/src/lib/components/Sidebar.svelte +110 -0
  48. package/src/lib/components/Tabs.svelte +48 -0
  49. package/src/lib/components/Tabs.test.ts +102 -0
  50. package/src/lib/config.test.ts +140 -0
  51. package/src/lib/config.ts +179 -0
  52. package/src/lib/configIntegration.test.ts +272 -0
  53. package/src/lib/configLoader.ts +231 -0
  54. package/src/lib/configParser.test.ts +217 -0
  55. package/src/lib/configParser.ts +234 -0
  56. package/src/lib/index.ts +34 -0
  57. package/src/lib/integration.test.ts +426 -0
  58. package/src/lib/navigationBuilder.test.ts +338 -0
  59. package/src/lib/navigationBuilder.ts +268 -0
  60. package/src/lib/performance.test.ts +369 -0
  61. package/src/lib/routing.test.ts +202 -0
  62. package/src/lib/routing.ts +127 -0
  63. package/src/lib/search-functionality.test.ts +493 -0
  64. package/src/lib/stores/i18n.test.ts +180 -0
  65. package/src/lib/stores/i18n.ts +143 -0
  66. package/src/lib/stores/nav.ts +36 -0
  67. package/src/lib/stores/search.test.ts +140 -0
  68. package/src/lib/stores/search.ts +162 -0
  69. package/src/lib/stores/theme.ts +59 -0
  70. package/src/lib/stores/version.test.ts +139 -0
  71. package/src/lib/stores/version.ts +111 -0
  72. package/src/lib/themeCustomization.test.ts +223 -0
  73. package/src/lib/themeCustomization.ts +212 -0
  74. package/src/lib/utils/highlight.test.ts +136 -0
  75. package/src/lib/utils/highlight.ts +100 -0
  76. package/src/lib/utils/index.ts +7 -0
  77. package/src/lib/utils/markdown.test.ts +357 -0
  78. package/src/lib/utils/markdown.ts +77 -0
  79. package/src/routes/+layout.server.ts +1 -0
  80. package/src/routes/+layout.svelte +28 -0
  81. package/src/routes/+page.svelte +165 -0
  82. package/static/robots.txt +3 -0
  83. package/svelte.config.js +18 -0
  84. package/tailwind.config.ts +55 -0
  85. package/template-starter/.github/workflows/build.yml +40 -0
  86. package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
  87. package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
  88. package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
  89. package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
  90. package/template-starter/README.md +320 -0
  91. package/template-starter/docs/_config.json +39 -0
  92. package/template-starter/docs/api/components.md +257 -0
  93. package/template-starter/docs/api/overview.md +169 -0
  94. package/template-starter/docs/guides/configuration.md +145 -0
  95. package/template-starter/docs/guides/github-pages-deployment.md +254 -0
  96. package/template-starter/docs/guides/netlify-deployment.md +159 -0
  97. package/template-starter/docs/guides/vercel-deployment.md +131 -0
  98. package/template-starter/docs/index.md +49 -0
  99. package/template-starter/docs/setup.md +149 -0
  100. package/template-starter/package.json +31 -0
  101. package/template-starter/pagefind.toml +3 -0
  102. package/template-starter/postcss.config.js +5 -0
  103. package/template-starter/src/app.css +34 -0
  104. package/template-starter/src/app.d.ts +13 -0
  105. package/template-starter/src/app.html +11 -0
  106. package/template-starter/src/lib/components/APITable.svelte +120 -0
  107. package/template-starter/src/lib/components/APITable.test.ts +19 -0
  108. package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
  109. package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
  110. package/template-starter/src/lib/components/Callout.svelte +60 -0
  111. package/template-starter/src/lib/components/Callout.test.ts +16 -0
  112. package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
  113. package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
  114. package/template-starter/src/lib/components/DocLayout.svelte +84 -0
  115. package/template-starter/src/lib/components/Footer.svelte +78 -0
  116. package/template-starter/src/lib/components/Image.svelte +100 -0
  117. package/template-starter/src/lib/components/Image.test.ts +15 -0
  118. package/template-starter/src/lib/components/Navbar.svelte +141 -0
  119. package/template-starter/src/lib/components/Search.svelte +248 -0
  120. package/template-starter/src/lib/components/Sidebar.svelte +110 -0
  121. package/template-starter/src/lib/components/Tabs.svelte +48 -0
  122. package/template-starter/src/lib/components/Tabs.test.ts +17 -0
  123. package/template-starter/src/lib/index.ts +15 -0
  124. package/template-starter/src/routes/+layout.svelte +28 -0
  125. package/template-starter/src/routes/+page.svelte +92 -0
  126. package/template-starter/svelte.config.js +17 -0
  127. package/template-starter/tailwind.config.ts +17 -0
  128. package/template-starter/tsconfig.json +13 -0
  129. package/template-starter/vite.config.ts +6 -0
  130. package/tests/e2e/example.spec.ts +345 -0
  131. package/tsconfig.json +20 -0
  132. package/vite.config.ts +6 -0
  133. package/vitest.config.ts +34 -0
  134. package/vitest.setup.ts +21 -0
@@ -0,0 +1,111 @@
1
+ import { writable } from 'svelte/store';
2
+
3
+ export interface VersionMetadata {
4
+ version: string;
5
+ label: string;
6
+ isLatest: boolean;
7
+ releaseDate?: string;
8
+ status?: 'stable' | 'beta' | 'deprecated';
9
+ }
10
+
11
+ export interface VersionConfig {
12
+ current: string;
13
+ availableVersions: VersionMetadata[];
14
+ }
15
+
16
+ function createVersionStore() {
17
+ // Default version configuration
18
+ const defaultConfig: VersionConfig = {
19
+ current: '2.0',
20
+ availableVersions: [
21
+ {
22
+ version: '2.0',
23
+ label: 'v2.0 (Latest)',
24
+ isLatest: true,
25
+ releaseDate: '2026-02-04',
26
+ status: 'stable',
27
+ },
28
+ {
29
+ version: '1.5',
30
+ label: 'v1.5',
31
+ isLatest: false,
32
+ releaseDate: '2025-12-01',
33
+ status: 'stable',
34
+ },
35
+ {
36
+ version: '1.0',
37
+ label: 'v1.0 (Archive)',
38
+ isLatest: false,
39
+ releaseDate: '2025-10-01',
40
+ status: 'deprecated',
41
+ },
42
+ ],
43
+ };
44
+
45
+ const getInitialVersion = (): string => {
46
+ if (typeof window === 'undefined') {
47
+ return defaultConfig.current;
48
+ }
49
+
50
+ try {
51
+ // Check localStorage for stored version preference
52
+ const stored = localStorage?.getItem?.('docs-version');
53
+ if (stored) {
54
+ return stored;
55
+ }
56
+ } catch (e) {
57
+ // localStorage may not be available in some environments
58
+ }
59
+
60
+ return defaultConfig.current;
61
+ };
62
+
63
+ const { subscribe, set, update } = writable<VersionConfig>({
64
+ ...defaultConfig,
65
+ current: getInitialVersion(),
66
+ });
67
+
68
+ return {
69
+ subscribe,
70
+ setVersion: (version: string) => {
71
+ update((config) => {
72
+ if (config.availableVersions.some((v) => v.version === version)) {
73
+ if (typeof window !== 'undefined') {
74
+ try {
75
+ localStorage?.setItem?.('docs-version', version);
76
+ } catch (e) {
77
+ // localStorage may not be available
78
+ }
79
+ }
80
+ return { ...config, current: version };
81
+ }
82
+ return config;
83
+ });
84
+ },
85
+ addVersion: (metadata: VersionMetadata) => {
86
+ update((config) => ({
87
+ ...config,
88
+ availableVersions: [
89
+ ...config.availableVersions.filter((v) => v.version !== metadata.version),
90
+ metadata,
91
+ ],
92
+ }));
93
+ },
94
+ removeVersion: (version: string) => {
95
+ update((config) => ({
96
+ ...config,
97
+ availableVersions: config.availableVersions.filter((v) => v.version !== version),
98
+ }));
99
+ },
100
+ getVersionMetadata: (version: string): VersionMetadata | undefined => {
101
+ let metadata: VersionMetadata | undefined;
102
+ const unsubscribe = subscribe((config) => {
103
+ metadata = config.availableVersions.find((v) => v.version === version);
104
+ });
105
+ unsubscribe();
106
+ return metadata;
107
+ },
108
+ };
109
+ }
110
+
111
+ export const version = createVersionStore();
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ generateCSSVariables,
4
+ createCSSVariablesStylesheet,
5
+ parseColor,
6
+ validateTheme,
7
+ mergeThemes,
8
+ getThemeTemplate,
9
+ } from './themeCustomization';
10
+
11
+ describe('Theme Customization', () => {
12
+ describe('generateCSSVariables', () => {
13
+ it('should generate CSS variables from theme config', () => {
14
+ const theme = {
15
+ primary: '#ff0000',
16
+ secondary: '#00ff00',
17
+ fontFamily: 'Arial',
18
+ };
19
+
20
+ const vars = generateCSSVariables(theme);
21
+ expect(vars['--color-primary']).toBe('#ff0000');
22
+ expect(vars['--color-secondary']).toBe('#00ff00');
23
+ expect(vars['--font-family-body']).toBe('Arial');
24
+ });
25
+
26
+ it('should skip undefined values', () => {
27
+ const theme = { primary: '#ff0000' };
28
+ const vars = generateCSSVariables(theme);
29
+
30
+ expect(Object.keys(vars)).toContain('--color-primary');
31
+ expect(Object.keys(vars)).not.toContain('--color-secondary');
32
+ });
33
+
34
+ it('should handle all color properties', () => {
35
+ const theme = {
36
+ primary: '#1',
37
+ secondary: '#2',
38
+ textLight: '#3',
39
+ textDark: '#4',
40
+ bgLight: '#5',
41
+ bgDark: '#6',
42
+ sidebarBg: '#7',
43
+ navbarBg: '#8',
44
+ codeBg: '#9',
45
+ };
46
+
47
+ const vars = generateCSSVariables(theme);
48
+ expect(Object.keys(vars)).toHaveLength(9);
49
+ });
50
+
51
+ it('should handle all font properties', () => {
52
+ const theme = {
53
+ fontFamily: 'sans-serif',
54
+ headingFont: 'serif',
55
+ };
56
+
57
+ const vars = generateCSSVariables(theme);
58
+ expect(vars['--font-family-body']).toBe('sans-serif');
59
+ expect(vars['--font-family-heading']).toBe('serif');
60
+ });
61
+ });
62
+
63
+ describe('createCSSVariablesStylesheet', () => {
64
+ it('should create valid CSS string', () => {
65
+ const vars = { '--color-primary': '#ff0000' };
66
+ const css = createCSSVariablesStylesheet(vars);
67
+
68
+ expect(css).toContain(':root');
69
+ expect(css).toContain('--color-primary: #ff0000');
70
+ });
71
+
72
+ it('should include multiple variables', () => {
73
+ const vars = {
74
+ '--color-primary': '#ff0000',
75
+ '--color-secondary': '#00ff00',
76
+ };
77
+
78
+ const css = createCSSVariablesStylesheet(vars);
79
+ expect(css).toContain('--color-primary');
80
+ expect(css).toContain('--color-secondary');
81
+ });
82
+ });
83
+
84
+ describe('parseColor', () => {
85
+ it('should parse hex colors', () => {
86
+ expect(parseColor('#ff0000').valid).toBe(true);
87
+ expect(parseColor('#f00').valid).toBe(true);
88
+ expect(parseColor('#ff0000aa').valid).toBe(true);
89
+ });
90
+
91
+ it('should parse RGB colors', () => {
92
+ expect(parseColor('rgb(255, 0, 0)').valid).toBe(true);
93
+ expect(parseColor('rgba(255, 0, 0, 0.5)').valid).toBe(true);
94
+ });
95
+
96
+ it('should parse named colors', () => {
97
+ expect(parseColor('red').valid).toBe(true);
98
+ expect(parseColor('blue').valid).toBe(true);
99
+ expect(parseColor('transparent').valid).toBe(true);
100
+ });
101
+
102
+ it('should parse CSS variables', () => {
103
+ expect(parseColor('var(--color-primary)').valid).toBe(true);
104
+ });
105
+
106
+ it('should reject invalid colors', () => {
107
+ expect(parseColor('invalid').valid).toBe(false);
108
+ expect(parseColor('#').valid).toBe(false);
109
+ expect(parseColor('').valid).toBe(false);
110
+ });
111
+
112
+ it('should return error message for invalid colors', () => {
113
+ const result = parseColor('not-a-color');
114
+ expect(result.valid).toBe(false);
115
+ expect(result.error).toBeDefined();
116
+ });
117
+ });
118
+
119
+ describe('validateTheme', () => {
120
+ it('should validate valid theme', () => {
121
+ const theme = {
122
+ primary: '#ff0000',
123
+ secondary: 'blue',
124
+ fontFamily: 'Arial',
125
+ };
126
+
127
+ const result = validateTheme(theme);
128
+ expect(result.valid).toBe(true);
129
+ expect(result.errors).toHaveLength(0);
130
+ });
131
+
132
+ it('should reject non-object theme', () => {
133
+ const result = validateTheme('not-object' as any);
134
+ expect(result.valid).toBe(false);
135
+ expect(result.errors.length).toBeGreaterThan(0);
136
+ });
137
+
138
+ it('should validate all color fields', () => {
139
+ const theme = {
140
+ primary: 'invalid-color',
141
+ };
142
+
143
+ const result = validateTheme(theme);
144
+ expect(result.valid).toBe(false);
145
+ expect(result.errors.some((e) => e.includes('primary'))).toBe(true);
146
+ });
147
+
148
+ it('should validate font fields are strings', () => {
149
+ const theme = {
150
+ fontFamily: 123 as any,
151
+ };
152
+
153
+ const result = validateTheme(theme);
154
+ expect(result.valid).toBe(false);
155
+ expect(result.errors.some((e) => e.includes('fontFamily'))).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe('mergeThemes', () => {
160
+ it('should merge two themes with priority to first', () => {
161
+ const primary = { primary: '#ff0000' };
162
+ const secondary = { primary: '#00ff00', secondary: '#0000ff' };
163
+
164
+ const merged = mergeThemes(primary, secondary);
165
+ expect(merged.primary).toBe('#ff0000');
166
+ expect(merged.secondary).toBe('#0000ff');
167
+ });
168
+
169
+ it('should handle undefined values', () => {
170
+ const primary = { primary: '#ff0000' };
171
+ const secondary = { secondary: '#00ff00' };
172
+
173
+ const merged = mergeThemes(primary, secondary);
174
+ expect(merged.primary).toBe('#ff0000');
175
+ expect(merged.secondary).toBe('#00ff00');
176
+ });
177
+ });
178
+
179
+ describe('getThemeTemplate', () => {
180
+ it('should return default theme', () => {
181
+ const theme = getThemeTemplate('default');
182
+ expect(theme.primary).toBeDefined();
183
+ expect(theme.fontFamily).toBeDefined();
184
+ });
185
+
186
+ it('should return dark theme', () => {
187
+ const theme = getThemeTemplate('dark');
188
+ expect(theme.primary).toBeDefined();
189
+ // Dark theme might have different colors
190
+ expect(theme.bgDark).toBeDefined();
191
+ });
192
+
193
+ it('should return minimal theme', () => {
194
+ const theme = getThemeTemplate('minimal');
195
+ expect(theme.primary).toBe('#000000');
196
+ });
197
+
198
+ it('should return colorful theme', () => {
199
+ const theme = getThemeTemplate('colorful');
200
+ expect(theme.primary).toBe('#ff006e');
201
+ });
202
+
203
+ it('should default to default theme if invalid', () => {
204
+ const theme = getThemeTemplate('invalid' as any);
205
+ expect(theme).toEqual(getThemeTemplate('default'));
206
+ });
207
+
208
+ it('should return default if not specified', () => {
209
+ const theme = getThemeTemplate();
210
+ expect(theme).toEqual(getThemeTemplate('default'));
211
+ });
212
+
213
+ it('all theme templates should be valid', () => {
214
+ const templates = ['default', 'dark', 'minimal', 'colorful'] as const;
215
+
216
+ templates.forEach((templateName) => {
217
+ const theme = getThemeTemplate(templateName);
218
+ const result = validateTheme(theme);
219
+ expect(result.valid).toBe(true);
220
+ });
221
+ });
222
+ });
223
+ });
@@ -0,0 +1,212 @@
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
+ }
@@ -0,0 +1,136 @@
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
+ });