@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.
- package/COMPONENTS.md +365 -0
- package/COVERAGE_REPORT.md +663 -0
- package/README.md +42 -0
- package/SEARCH_VERIFICATION.md +229 -0
- package/TEST_SUMMARY.md +344 -0
- package/bin/init.js +821 -0
- package/docs/E2E_TESTS.md +354 -0
- package/docs/TESTING.md +754 -0
- package/docs/de/index.md +41 -0
- package/docs/en/COMPONENTS.md +443 -0
- package/docs/en/api/examples.md +100 -0
- package/docs/en/api/overview.md +69 -0
- package/docs/en/components/index.md +622 -0
- package/docs/en/config/navigation.md +505 -0
- package/docs/en/config/theme-and-colors.md +395 -0
- package/docs/en/getting-started/integration.md +406 -0
- package/docs/en/guides/common-setups.md +651 -0
- package/docs/en/index.md +243 -0
- package/docs/en/markdown.md +102 -0
- package/docs/en/routing.md +64 -0
- package/docs/en/setup.md +52 -0
- package/docs/en/troubleshooting.md +704 -0
- package/docs/es/index.md +41 -0
- package/docs/fr/index.md +41 -0
- package/docs/ja/index.md +41 -0
- package/package.json +40 -0
- package/pagefind.toml +8 -0
- package/postcss.config.js +5 -0
- package/src/app.css +119 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +11 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/APITable.svelte +120 -0
- package/src/lib/components/APITable.test.ts +153 -0
- package/src/lib/components/Breadcrumbs.svelte +85 -0
- package/src/lib/components/Breadcrumbs.test.ts +148 -0
- package/src/lib/components/Callout.svelte +60 -0
- package/src/lib/components/Callout.test.ts +100 -0
- package/src/lib/components/CodeBlock.svelte +68 -0
- package/src/lib/components/CodeBlock.test.ts +133 -0
- package/src/lib/components/DocLayout.svelte +84 -0
- package/src/lib/components/Footer.svelte +78 -0
- package/src/lib/components/Image.svelte +100 -0
- package/src/lib/components/Image.test.ts +163 -0
- package/src/lib/components/Navbar.svelte +141 -0
- package/src/lib/components/Search.svelte +248 -0
- package/src/lib/components/Sidebar.svelte +110 -0
- package/src/lib/components/Tabs.svelte +48 -0
- package/src/lib/components/Tabs.test.ts +102 -0
- package/src/lib/config.test.ts +140 -0
- package/src/lib/config.ts +179 -0
- package/src/lib/configIntegration.test.ts +272 -0
- package/src/lib/configLoader.ts +231 -0
- package/src/lib/configParser.test.ts +217 -0
- package/src/lib/configParser.ts +234 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/integration.test.ts +426 -0
- package/src/lib/navigationBuilder.test.ts +338 -0
- package/src/lib/navigationBuilder.ts +268 -0
- package/src/lib/performance.test.ts +369 -0
- package/src/lib/routing.test.ts +202 -0
- package/src/lib/routing.ts +127 -0
- package/src/lib/search-functionality.test.ts +493 -0
- package/src/lib/stores/i18n.test.ts +180 -0
- package/src/lib/stores/i18n.ts +143 -0
- package/src/lib/stores/nav.ts +36 -0
- package/src/lib/stores/search.test.ts +140 -0
- package/src/lib/stores/search.ts +162 -0
- package/src/lib/stores/theme.ts +59 -0
- package/src/lib/stores/version.test.ts +139 -0
- package/src/lib/stores/version.ts +111 -0
- package/src/lib/themeCustomization.test.ts +223 -0
- package/src/lib/themeCustomization.ts +212 -0
- package/src/lib/utils/highlight.test.ts +136 -0
- package/src/lib/utils/highlight.ts +100 -0
- package/src/lib/utils/index.ts +7 -0
- package/src/lib/utils/markdown.test.ts +357 -0
- package/src/lib/utils/markdown.ts +77 -0
- package/src/routes/+layout.server.ts +1 -0
- package/src/routes/+layout.svelte +28 -0
- package/src/routes/+page.svelte +165 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +18 -0
- package/tailwind.config.ts +55 -0
- package/template-starter/.github/workflows/build.yml +40 -0
- package/template-starter/.github/workflows/deploy-github-pages.yml +47 -0
- package/template-starter/.github/workflows/deploy-netlify.yml +41 -0
- package/template-starter/.github/workflows/deploy-vercel.yml +64 -0
- package/template-starter/NPM-PACKAGE-SETUP.md +233 -0
- package/template-starter/README.md +320 -0
- package/template-starter/docs/_config.json +39 -0
- package/template-starter/docs/api/components.md +257 -0
- package/template-starter/docs/api/overview.md +169 -0
- package/template-starter/docs/guides/configuration.md +145 -0
- package/template-starter/docs/guides/github-pages-deployment.md +254 -0
- package/template-starter/docs/guides/netlify-deployment.md +159 -0
- package/template-starter/docs/guides/vercel-deployment.md +131 -0
- package/template-starter/docs/index.md +49 -0
- package/template-starter/docs/setup.md +149 -0
- package/template-starter/package.json +31 -0
- package/template-starter/pagefind.toml +3 -0
- package/template-starter/postcss.config.js +5 -0
- package/template-starter/src/app.css +34 -0
- package/template-starter/src/app.d.ts +13 -0
- package/template-starter/src/app.html +11 -0
- package/template-starter/src/lib/components/APITable.svelte +120 -0
- package/template-starter/src/lib/components/APITable.test.ts +19 -0
- package/template-starter/src/lib/components/Breadcrumbs.svelte +85 -0
- package/template-starter/src/lib/components/Breadcrumbs.test.ts +19 -0
- package/template-starter/src/lib/components/Callout.svelte +60 -0
- package/template-starter/src/lib/components/Callout.test.ts +16 -0
- package/template-starter/src/lib/components/CodeBlock.svelte +68 -0
- package/template-starter/src/lib/components/CodeBlock.test.ts +12 -0
- package/template-starter/src/lib/components/DocLayout.svelte +84 -0
- package/template-starter/src/lib/components/Footer.svelte +78 -0
- package/template-starter/src/lib/components/Image.svelte +100 -0
- package/template-starter/src/lib/components/Image.test.ts +15 -0
- package/template-starter/src/lib/components/Navbar.svelte +141 -0
- package/template-starter/src/lib/components/Search.svelte +248 -0
- package/template-starter/src/lib/components/Sidebar.svelte +110 -0
- package/template-starter/src/lib/components/Tabs.svelte +48 -0
- package/template-starter/src/lib/components/Tabs.test.ts +17 -0
- package/template-starter/src/lib/index.ts +15 -0
- package/template-starter/src/routes/+layout.svelte +28 -0
- package/template-starter/src/routes/+page.svelte +92 -0
- package/template-starter/svelte.config.js +17 -0
- package/template-starter/tailwind.config.ts +17 -0
- package/template-starter/tsconfig.json +13 -0
- package/template-starter/vite.config.ts +6 -0
- package/tests/e2e/example.spec.ts +345 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +6 -0
- package/vitest.config.ts +34 -0
- package/vitest.setup.ts +21 -0
|
@@ -0,0 +1,100 @@
|
|
|
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, '<mark>$1</mark>');
|
|
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
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
renderMarkdown,
|
|
4
|
+
extractMetadata,
|
|
5
|
+
getExcerpt,
|
|
6
|
+
type MarkdownResult,
|
|
7
|
+
type MarkdownMetadata
|
|
8
|
+
} from './markdown';
|
|
9
|
+
|
|
10
|
+
describe('Markdown Utilities', () => {
|
|
11
|
+
describe('renderMarkdown', () => {
|
|
12
|
+
it('should render basic markdown to HTML', async () => {
|
|
13
|
+
const content = '# Hello World';
|
|
14
|
+
const result = await renderMarkdown(content);
|
|
15
|
+
|
|
16
|
+
expect(result.html).toBeTruthy();
|
|
17
|
+
expect(result.html).toContain('Hello World');
|
|
18
|
+
expect(result.html).toContain('<h1>');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse frontmatter metadata', async () => {
|
|
22
|
+
const content = `---
|
|
23
|
+
title: Test Page
|
|
24
|
+
description: A test page
|
|
25
|
+
author: John Doe
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Content`;
|
|
29
|
+
|
|
30
|
+
const result = await renderMarkdown(content);
|
|
31
|
+
|
|
32
|
+
expect(result.metadata.title).toBe('Test Page');
|
|
33
|
+
expect(result.metadata.description).toBe('A test page');
|
|
34
|
+
expect(result.metadata.author).toBe('John Doe');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle content without frontmatter', async () => {
|
|
38
|
+
const content = '## Section\n\nSome content here';
|
|
39
|
+
const result = await renderMarkdown(content);
|
|
40
|
+
|
|
41
|
+
expect(result.html).toBeTruthy();
|
|
42
|
+
expect(result.metadata).toEqual({});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should convert markdown formatting to HTML', async () => {
|
|
46
|
+
const content = `
|
|
47
|
+
# Heading 1
|
|
48
|
+
## Heading 2
|
|
49
|
+
**Bold text**
|
|
50
|
+
*Italic text*
|
|
51
|
+
\`code\`
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
const result = await renderMarkdown(content);
|
|
55
|
+
|
|
56
|
+
expect(result.html).toContain('<h1>');
|
|
57
|
+
expect(result.html).toContain('<h2>');
|
|
58
|
+
expect(result.html).toContain('<strong>');
|
|
59
|
+
expect(result.html).toContain('<em>');
|
|
60
|
+
expect(result.html).toContain('<code>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle code blocks', async () => {
|
|
64
|
+
const content = `\`\`\`javascript
|
|
65
|
+
const x = 42;
|
|
66
|
+
console.log(x);
|
|
67
|
+
\`\`\``;
|
|
68
|
+
|
|
69
|
+
const result = await renderMarkdown(content);
|
|
70
|
+
|
|
71
|
+
expect(result.html).toContain('<pre>');
|
|
72
|
+
expect(result.html).toContain('<code');
|
|
73
|
+
expect(result.html).toContain('const x = 42');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle lists', async () => {
|
|
77
|
+
const content = `
|
|
78
|
+
- Item 1
|
|
79
|
+
- Item 2
|
|
80
|
+
- Item 3
|
|
81
|
+
|
|
82
|
+
1. First
|
|
83
|
+
2. Second
|
|
84
|
+
3. Third
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const result = await renderMarkdown(content);
|
|
88
|
+
|
|
89
|
+
expect(result.html).toContain('<li>');
|
|
90
|
+
expect(result.html).toContain('Item 1');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle links', async () => {
|
|
94
|
+
const content = '[Visit Example](https://example.com)';
|
|
95
|
+
const result = await renderMarkdown(content);
|
|
96
|
+
|
|
97
|
+
expect(result.html).toContain('<a');
|
|
98
|
+
expect(result.html).toContain('https://example.com');
|
|
99
|
+
expect(result.html).toContain('Visit Example');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle images', async () => {
|
|
103
|
+
const content = '';
|
|
104
|
+
const result = await renderMarkdown(content);
|
|
105
|
+
|
|
106
|
+
expect(result.html).toContain('<img');
|
|
107
|
+
expect(result.html).toContain('./image.png');
|
|
108
|
+
expect(result.html).toContain('Alt text');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle blockquotes', async () => {
|
|
112
|
+
const content = '> This is a quote';
|
|
113
|
+
const result = await renderMarkdown(content);
|
|
114
|
+
|
|
115
|
+
expect(result.html).toContain('<blockquote');
|
|
116
|
+
expect(result.html).toContain('This is a quote');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle tables', async () => {
|
|
120
|
+
const content = `
|
|
121
|
+
| Header 1 | Header 2 |
|
|
122
|
+
|----------|----------|
|
|
123
|
+
| Cell 1 | Cell 2 |
|
|
124
|
+
| Cell 3 | Cell 4 |
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
const result = await renderMarkdown(content);
|
|
128
|
+
|
|
129
|
+
expect(result.html).toContain('<table');
|
|
130
|
+
expect(result.html).toContain('Header 1');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return result type with html and metadata', async () => {
|
|
134
|
+
const content = `---
|
|
135
|
+
title: Test
|
|
136
|
+
---
|
|
137
|
+
Content`;
|
|
138
|
+
|
|
139
|
+
const result = await renderMarkdown(content);
|
|
140
|
+
|
|
141
|
+
expect(result).toHaveProperty('html');
|
|
142
|
+
expect(result).toHaveProperty('metadata');
|
|
143
|
+
expect(typeof result.html).toBe('string');
|
|
144
|
+
expect(typeof result.metadata).toBe('object');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle empty content', async () => {
|
|
148
|
+
const result = await renderMarkdown('');
|
|
149
|
+
expect(result.html).toBeDefined();
|
|
150
|
+
expect(result.metadata).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle very long content', async () => {
|
|
154
|
+
const longContent = Array(100).fill('# Heading\n\nContent').join('\n');
|
|
155
|
+
const result = await renderMarkdown(longContent);
|
|
156
|
+
|
|
157
|
+
expect(result.html).toBeTruthy();
|
|
158
|
+
expect(result.html.length).toBeGreaterThan(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should preserve custom metadata fields', async () => {
|
|
162
|
+
const content = `---
|
|
163
|
+
title: Test
|
|
164
|
+
custom_field: custom_value
|
|
165
|
+
tags: [tag1, tag2]
|
|
166
|
+
---
|
|
167
|
+
Content`;
|
|
168
|
+
|
|
169
|
+
const result = await renderMarkdown(content);
|
|
170
|
+
|
|
171
|
+
expect(result.metadata.custom_field).toBe('custom_value');
|
|
172
|
+
expect(Array.isArray(result.metadata.tags)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('extractMetadata', () => {
|
|
177
|
+
it('should extract metadata from content', () => {
|
|
178
|
+
const content = `---
|
|
179
|
+
title: Page Title
|
|
180
|
+
description: Page description
|
|
181
|
+
author: John Doe
|
|
182
|
+
date: 2024-01-01
|
|
183
|
+
---
|
|
184
|
+
Content`;
|
|
185
|
+
|
|
186
|
+
const metadata = extractMetadata(content);
|
|
187
|
+
|
|
188
|
+
expect(metadata.title).toBe('Page Title');
|
|
189
|
+
expect(metadata.description).toBe('Page description');
|
|
190
|
+
expect(metadata.author).toBe('John Doe');
|
|
191
|
+
// YAML parses dates as Date objects, convert to ISO string
|
|
192
|
+
expect(metadata.date instanceof Date || typeof metadata.date === 'string').toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return empty object if no frontmatter', () => {
|
|
196
|
+
const content = '# Just content\n\nNo metadata here';
|
|
197
|
+
const metadata = extractMetadata(content);
|
|
198
|
+
|
|
199
|
+
expect(Object.keys(metadata).length).toBe(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle empty frontmatter', () => {
|
|
203
|
+
const content = `---
|
|
204
|
+
---
|
|
205
|
+
Content`;
|
|
206
|
+
|
|
207
|
+
const metadata = extractMetadata(content);
|
|
208
|
+
expect(metadata).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should support various data types in metadata', () => {
|
|
212
|
+
const content = `---
|
|
213
|
+
string_field: value
|
|
214
|
+
number_field: 42
|
|
215
|
+
boolean_field: true
|
|
216
|
+
array_field: [1, 2, 3]
|
|
217
|
+
object_field:
|
|
218
|
+
nested: value
|
|
219
|
+
---
|
|
220
|
+
Content`;
|
|
221
|
+
|
|
222
|
+
const metadata = extractMetadata(content);
|
|
223
|
+
|
|
224
|
+
expect(metadata.string_field).toBe('value');
|
|
225
|
+
expect(metadata.number_field).toBe(42);
|
|
226
|
+
expect(metadata.boolean_field).toBe(true);
|
|
227
|
+
expect(Array.isArray(metadata.array_field)).toBe(true);
|
|
228
|
+
expect(metadata.object_field.nested).toBe('value');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('getExcerpt', () => {
|
|
233
|
+
it('should extract excerpt from markdown content', () => {
|
|
234
|
+
const content = `---
|
|
235
|
+
title: Test
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
This is the first paragraph with some content.
|
|
239
|
+
|
|
240
|
+
This is the second paragraph.`;
|
|
241
|
+
|
|
242
|
+
const excerpt = getExcerpt(content);
|
|
243
|
+
|
|
244
|
+
expect(excerpt).toContain('This is the first paragraph');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should remove markdown formatting from excerpt', () => {
|
|
248
|
+
const content = `**Bold** and *italic* with [links](https://example.com)`;
|
|
249
|
+
const excerpt = getExcerpt(content);
|
|
250
|
+
|
|
251
|
+
expect(excerpt).not.toContain('**');
|
|
252
|
+
expect(excerpt).not.toContain('*');
|
|
253
|
+
expect(excerpt).not.toContain('[');
|
|
254
|
+
expect(excerpt).toContain('Bold');
|
|
255
|
+
expect(excerpt).toContain('links');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should remove header formatting from excerpt', () => {
|
|
259
|
+
const content = `This is the actual content without headers.`;
|
|
260
|
+
|
|
261
|
+
const excerpt = getExcerpt(content);
|
|
262
|
+
|
|
263
|
+
expect(excerpt).toContain('This is the actual content');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should remove inline code from excerpt', () => {
|
|
267
|
+
const content = 'This is `code` in the text';
|
|
268
|
+
const excerpt = getExcerpt(content);
|
|
269
|
+
|
|
270
|
+
expect(excerpt).not.toContain('`');
|
|
271
|
+
expect(excerpt).toContain('This is code in the text');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should respect word limit', () => {
|
|
275
|
+
const longContent = Array(50).fill('word').join(' ');
|
|
276
|
+
const excerpt = getExcerpt(longContent, 10);
|
|
277
|
+
|
|
278
|
+
const wordCount = excerpt.split(/\s+/).length;
|
|
279
|
+
expect(wordCount).toBeLessThanOrEqual(11); // 10 words + ellipsis might add spaces
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should add ellipsis if content is truncated', () => {
|
|
283
|
+
const longContent = Array(200).fill('word').join(' ');
|
|
284
|
+
const excerpt = getExcerpt(longContent, 10);
|
|
285
|
+
|
|
286
|
+
expect(excerpt).toContain('...');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should not add ellipsis if content fits', () => {
|
|
290
|
+
const shortContent = 'Short content';
|
|
291
|
+
const excerpt = getExcerpt(shortContent, 100);
|
|
292
|
+
|
|
293
|
+
expect(excerpt).not.toContain('...');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should handle content with frontmatter', () => {
|
|
297
|
+
const content = `---
|
|
298
|
+
title: Test
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
This is the excerpt content.`;
|
|
302
|
+
|
|
303
|
+
const excerpt = getExcerpt(content);
|
|
304
|
+
|
|
305
|
+
expect(excerpt).toContain('This is the excerpt content');
|
|
306
|
+
expect(excerpt).not.toContain('title');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should handle empty content', () => {
|
|
310
|
+
const excerpt = getExcerpt('');
|
|
311
|
+
expect(excerpt).toBe('');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle content with only whitespace', () => {
|
|
315
|
+
const excerpt = getExcerpt(' \n\n ');
|
|
316
|
+
expect(excerpt.trim()).toBe('');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should handle default word limit', () => {
|
|
320
|
+
const content = Array(200).fill('word').join(' ');
|
|
321
|
+
const excerpt = getExcerpt(content); // Uses default 150
|
|
322
|
+
|
|
323
|
+
expect(excerpt).toBeTruthy();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('Edge Cases', () => {
|
|
328
|
+
it('should handle malformed markdown gracefully', async () => {
|
|
329
|
+
const content = '[unclosed link]](extra';
|
|
330
|
+
const result = await renderMarkdown(content);
|
|
331
|
+
|
|
332
|
+
expect(result.html).toBeDefined();
|
|
333
|
+
expect(result.metadata).toBeDefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should handle special characters in content', async () => {
|
|
337
|
+
const content = '< > & " \' `special characters`';
|
|
338
|
+
const result = await renderMarkdown(content);
|
|
339
|
+
|
|
340
|
+
expect(result.html).toBeTruthy();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle unicode content', async () => {
|
|
344
|
+
const content = '# 你好世界\n\n日本語テスト';
|
|
345
|
+
const result = await renderMarkdown(content);
|
|
346
|
+
|
|
347
|
+
expect(result.html).toContain('你好世界');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should handle nested formatting', async () => {
|
|
351
|
+
const content = '***Bold and italic***';
|
|
352
|
+
const result = await renderMarkdown(content);
|
|
353
|
+
|
|
354
|
+
expect(result.html).toBeTruthy();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
|
|
4
|
+
export interface MarkdownMetadata {
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
date?: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MarkdownResult {
|
|
13
|
+
html: string;
|
|
14
|
+
metadata: MarkdownMetadata;
|
|
15
|
+
slug?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse markdown content with frontmatter
|
|
20
|
+
* Returns HTML content and metadata
|
|
21
|
+
*/
|
|
22
|
+
export async function renderMarkdown(content: string): Promise<MarkdownResult> {
|
|
23
|
+
try {
|
|
24
|
+
// Parse frontmatter and content
|
|
25
|
+
const { data, content: markdownContent } = matter(content);
|
|
26
|
+
|
|
27
|
+
// Convert markdown to HTML (marked is async in v17+)
|
|
28
|
+
const html = await marked(markdownContent);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
html,
|
|
32
|
+
metadata: data as MarkdownMetadata
|
|
33
|
+
};
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('Error rendering markdown:', error);
|
|
36
|
+
return {
|
|
37
|
+
html: '<p>Error rendering markdown</p>',
|
|
38
|
+
metadata: {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract metadata from markdown content
|
|
45
|
+
*/
|
|
46
|
+
export function extractMetadata(content: string): MarkdownMetadata {
|
|
47
|
+
try {
|
|
48
|
+
const { data } = matter(content);
|
|
49
|
+
return data as MarkdownMetadata;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get excerpt from markdown content (first paragraph)
|
|
57
|
+
*/
|
|
58
|
+
export function getExcerpt(content: string, wordLimit = 150): string {
|
|
59
|
+
try {
|
|
60
|
+
const { content: markdownContent } = matter(content);
|
|
61
|
+
|
|
62
|
+
// Remove markdown syntax and get first paragraph
|
|
63
|
+
const text = markdownContent
|
|
64
|
+
.replace(/#{1,6}\s/g, '') // Remove headers
|
|
65
|
+
.replace(/[*_]{1,2}(.*?)[*_]{1,2}/g, '$1') // Remove bold/italic
|
|
66
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links
|
|
67
|
+
.replace(/`([^`]+)`/g, '$1') // Remove inline code
|
|
68
|
+
.trim();
|
|
69
|
+
|
|
70
|
+
const words = text.split(/\s+/);
|
|
71
|
+
const excerpt = words.slice(0, wordLimit).join(' ');
|
|
72
|
+
|
|
73
|
+
return excerpt.length < text.length ? excerpt + '...' : excerpt;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const prerender = true;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../app.css';
|
|
3
|
+
|
|
4
|
+
let { children } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<svelte:window
|
|
8
|
+
on:resize={() => {
|
|
9
|
+
// Handle window resize if needed
|
|
10
|
+
}}
|
|
11
|
+
/>
|
|
12
|
+
|
|
13
|
+
<div class="app">
|
|
14
|
+
{@render children?.()}
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<style>
|
|
18
|
+
:global(body) {
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.app {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
min-height: 100vh;
|
|
27
|
+
}
|
|
28
|
+
</style>
|