-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathcontent-processor.js
More file actions
525 lines (438 loc) · 16.1 KB
/
Copy pathcontent-processor.js
File metadata and controls
525 lines (438 loc) · 16.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
// Node.js specific modules should only run on the server side
// This file uses mdsvex for markdown processing instead of marked
// Node.js modules
import fs from 'fs';
import path from 'path';
import { compile } from 'mdsvex';
import matter from 'gray-matter';
import rehypeSlug from 'rehype-slug';
import remarkGfm from 'remark-gfm';
import remarkXCard from './remark-x-card.js';
// This error check is to provide an early warning when this module is attempted to be used in the browser
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
if (isBrowser) {
console.error('content-processor-mdsvex.js should only be used on the server side!');
throw new Error('Content processor cannot run on the client side!');
}
// Try to load site config, but don't fail if not available
let siteConfig = {};
try {
const configPath = path.resolve('site.config.json');
if (fs.existsSync(configPath)) {
siteConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
} catch {
// siteConfig not available, will use empty object
}
// Configure mdsvex options
const mdsvexOptions = {
extensions: ['.md'],
remarkPlugins: [remarkGfm, remarkXCard],
rehypePlugins: [rehypeSlug],
layout: null // We'll handle layout in Svelte components
};
/**
* Process markdown with mdsvex and extract HTML content
* Since mdsvex produces Svelte component code, we need to extract just the HTML part
*/
const processMarkdownWithMDSvex = async (markdown) => {
try {
const { code } = await compile(markdown, mdsvexOptions);
// Extract HTML from mdsvex output
let html = code;
// Remove script tags and their content
html = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
// Remove Svelte {@html `...`} wrappers and keep just the inner HTML
// mdsvex wraps code blocks in {@html `...`} which we need to unwrap
html = html.replace(/\{@html\s+`([^`]*(?:`[^`]*`)*)`\}/g, '$1');
// Remove remaining Svelte-specific wrappers
html = html.replace(/\{[\s\S]*?\}/g, (match) => {
// Keep if it's not a Svelte directive (like {@html} already handled)
if (match.startsWith('{@') || match.includes('=>')) {
return '';
}
return match;
});
html = html.trim();
return html;
} catch (error) {
console.error('MDSvex processing error:', error);
return `<p>Error processing markdown: ${error.message}</p>`;
}
};
// Function to remove the first h1 heading from HTML content
const removeFirstH1 = (html) => {
return html.replace(/<h1[^>]*>(.*?)<\/h1>/, '');
};
/**
* Creates a custom marked-like renderer for link transformation
* We'll handle this with post-processing since mdsvex uses rehype/remark
*/
const transformLinks = (html, currentDirectory) => {
// Transform internal .md links
return html.replace(/href="([^"]+)"/g, (match, href) => {
// Skip external links and anchors
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('mailto:')) {
return match;
}
// Remove .md extension
let transformedHref = href;
if (transformedHref.endsWith('.md')) {
transformedHref = transformedHref.slice(0, -3);
}
// Handle relative paths
if (transformedHref.startsWith('./') || transformedHref.startsWith('../')) {
const resolvedPath = path.join('/', currentDirectory, transformedHref);
transformedHref = resolvedPath.replace(/\\/g, '/').replace(/\/$/, '');
} else if (!transformedHref.startsWith('/')) {
transformedHref = path.join('/', currentDirectory, transformedHref).replace(/\\/g, '/');
}
return `href="${transformedHref}"`;
});
};
// Scans all markdown files and folders in the content directory
const scanContentDirectory = async () => {
const contentPath = path.resolve('content');
const contentEntries = [];
if (!fs.existsSync(contentPath)) {
console.warn('Content folder not found!');
return contentEntries;
}
// Recursively scan the content folder
async function scanDir(dirPath, relativePath = '') {
const entries = fs.readdirSync(dirPath);
for (const entry of entries) {
const fullPath = path.join(dirPath, entry);
const entryRelativePath = path.join(relativePath, entry);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
// If it's a folder, scan its contents
await scanDir(fullPath, entryRelativePath);
} else if (stats.isFile() && (entry.endsWith('.md') || entry.endsWith('.mdx'))) {
// Add markdown and mdx files to the list
const isMdx = entry.endsWith('.mdx');
const slug = entry.replace(/\.mdx?$/, '');
const url = relativePath
? `/${relativePath}/${slug}`.replace(/\\/g, '/')
: `/${slug}`;
const content = fs.readFileSync(fullPath, 'utf-8');
const { data, content: markdownContent } = matter(content);
// Process template variables (both in markdown content and metadata)
const processedMarkdownContent = processTemplateVariables(markdownContent);
const processedMetadata = {};
// Process string values in metadata through template processing
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string') {
processedMetadata[key] = processTemplateVariables(value);
} else {
processedMetadata[key] = value;
}
}
// Add default values and process them through template processing
const finalMetadata = {
title: processedMetadata.title || formatTitle(slug),
description: processedMetadata.description || '',
date: processedMetadata.date || null,
author: processedMetadata.author || null,
...processedMetadata
};
// Fix directory - use full path
let directory = relativePath.replace(/\\/g, '/');
// Process content: MDX files are rendered as Svelte components, MD files as HTML
let html = '';
if (!isMdx) {
html = await processMarkdownWithMDSvex(processedMarkdownContent);
html = removeFirstH1(html);
html = transformLinks(html, directory);
}
// Add main directory information to create content tree
const mainDirectory = directory.split('/')[0] || 'root';
contentEntries.push({
slug,
path: entryRelativePath,
url,
directory,
mainDirectory,
depth: directory === '' ? 0 : directory.split('/').length,
content: html,
metadata: finalMetadata,
isMdx // Flag for MDX files - rendered client-side as Svelte components
});
}
}
}
// Start scanning the content folder
await scanDir(contentPath);
return contentEntries;
};
// Function that detects folders in the content directory
const getContentDirectories = () => {
const contentPath = path.resolve('content');
const directories = [];
if (!fs.existsSync(contentPath)) {
console.warn('Content folder not found!');
return directories;
}
const entries = fs.readdirSync(contentPath);
for (const entry of entries) {
const fullPath = path.join(contentPath, entry);
if (fs.statSync(fullPath).isDirectory()) {
directories.push({
name: entry,
path: `content/${entry}`,
title: formatTitle(entry),
url: `/${entry}`
});
}
}
return directories;
};
// Function to create a title from a slug
const formatTitle = (slug) => {
return slug
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
// To scan all content once and cache it
let cachedContent = null;
// Get all content (using cache)
const getAllContent = async () => {
// Check for development mode to skip caching
const isDev = process.env.NODE_ENV === 'development' || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV);
if (!isDev && cachedContent) return cachedContent;
// In development, we want to scan every time to pick up changes
if (isDev) {
// Clear cache to be safe
cachedContent = null;
}
const content = await scanContentDirectory();
// Only cache in production
if (!isDev) {
cachedContent = content;
}
return content;
};
// Get content for a specific URL
const getContentByUrl = async (url) => {
const allContent = await getAllContent();
// Remove trailing slash (/) from URL
const normalizedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
console.log('Normalized URL for lookup:', normalizedUrl);
// Check content URLs and find matching content
const result = allContent.find(entry => {
// Remove trailing slash from content URL as well
const entryUrl = entry.url.endsWith('/') ? entry.url.slice(0, -1) : entry.url;
console.log(`Comparing: "${entryUrl}" vs "${normalizedUrl}"`);
return entryUrl === normalizedUrl;
});
console.log('Match result:', result ? `Found: ${result.url}` : 'Not found');
return result;
};
// Get content from a specific directory
const getContentByDirectory = async (directory) => {
const allContent = await getAllContent();
// Direct matching for main directories
if (directory === 'root') {
return allContent.filter(entry => entry.directory === 'root');
}
// Get all content that starts with the specified directory, including subdirectories
return allContent.filter(entry => {
// 1. Exact match case (e.g., 'blog' directory for 'blog')
// 2. Subdirectory match (e.g., 'blog/category' directory for 'blog')
return entry.directory === directory || entry.directory.startsWith(directory + '/');
});
};
// Clear cache (might be necessary in development mode)
const clearContentCache = () => {
cachedContent = null;
};
// Function to find subdirectories - returns subdirectories for a specific directory
const getSubDirectories = async (directory) => {
const allContent = await getAllContent();
const subdirs = new Set();
// If not the main directory, filter relevant content
const contents = allContent.filter(entry =>
entry.directory !== 'root' &&
(entry.directory === directory || entry.directory.startsWith(directory + '/'))
);
// Extract subdirectories from contents
contents.forEach(entry => {
// Get only subdirectories by skipping the main directory
const relativePath = entry.directory.replace(directory + '/', '');
if (relativePath && relativePath.includes('/')) {
// Get the first subdirectory level (e.g., 'blog/category/js' -> 'category')
const firstLevel = relativePath.split('/')[0];
subdirs.add(firstLevel);
}
});
return Array.from(subdirs).map(subdir => ({
name: subdir,
path: `${directory}/${subdir}`,
title: formatTitle(subdir),
url: `/${directory}/${subdir}`
}));
};
// Function to process template variables
const processTemplateVariables = (content) => {
// Get variables from configuration (with safe defaults)
const site = siteConfig.site || {};
const contact = siteConfig.contact || {};
const social = siteConfig.social || {};
const legal = siteConfig.legal || {};
const variables = {
// Site information
'site.name': site.name,
'site.description': site.description,
'site.url': site.url,
'site.author': site.author,
// Contact information
'contact.email': contact.email,
'contact.privacyEmail': contact.privacyEmail,
'contact.supportEmail': contact.supportEmail,
'contact.phone': contact.phone,
'contact.address.street': contact.address?.street,
'contact.address.city': contact.address?.city,
'contact.address.state': contact.address?.state,
'contact.address.zipCode': contact.address?.zipCode,
'contact.address.country': contact.address?.country,
'contact.address.full': contact.address
? `${contact.address.street || ''}, ${contact.address.city || ''}, ${contact.address.state || ''} ${contact.address.zipCode || ''}`.trim()
: '',
// Social media
'social.twitter': social.twitter,
'social.github': social.github,
'social.linkedin': social.linkedin,
'social.facebook': social.facebook,
'social.instagram': social.instagram,
'social.youtube': social.youtube,
'social.discord': social.discord,
'social.reddit': social.reddit,
// Legal information
'legal.privacyPolicyLastUpdated': legal.privacyPolicyLastUpdated,
'legal.termsLastUpdated': legal.termsLastUpdated,
'legal.doNotSell.processingTime': legal.doNotSell?.processingTime,
// Dynamic date functions
'date.now': new Date().toLocaleDateString('en-US'),
'date.year': new Date().getFullYear().toString(),
'date.month': new Date().toLocaleDateString('en-US', { month: 'long' }),
'date.day': new Date().getDate().toString()
};
// Replace template variables
// Support {{variable.name}} format variables
let processedContent = content;
// Process {{variable}} format variables
processedContent = processedContent.replace(/\{\{([^}]+)\}\}/g, (match, variableName) => {
const trimmedName = variableName.trim();
if (Object.hasOwn(variables, trimmedName)) {
return variables[trimmedName];
}
console.warn(`Template variable not found: ${trimmedName}`);
return match; // Leave unfound variables as they are
});
return processedContent;
};
// Function to build sidebar navigation tree for a directory
const getSidebarTree = async (directory) => {
const allContent = await getAllContent();
// Filter content for this directory
const directoryContent = allContent.filter(entry =>
entry.directory === directory || entry.directory.startsWith(directory + '/')
);
// Group by subdirectory
const groups = {};
directoryContent.forEach(entry => {
// Get relative path from the main directory
const relativePath = entry.directory === directory
? ''
: entry.directory.replace(directory + '/', '');
const parts = relativePath.split('/').filter(Boolean);
const groupKey = parts[0] || '_root';
if (!groups[groupKey]) {
groups[groupKey] = {
title: groupKey === '_root' ? formatTitle(directory) : formatTitle(groupKey),
items: []
};
}
groups[groupKey].items.push({
title: entry.metadata.title,
url: entry.url,
order: entry.metadata.order || 999
});
});
// Sort items within each group
Object.values(groups).forEach(group => {
group.items.sort((a, b) => a.order - b.order);
});
// Convert to sidebar format
const result = [];
// Add root items first
if (groups._root) {
groups._root.items.forEach(item => {
result.push(item);
});
delete groups._root;
}
// Add grouped items
Object.entries(groups).forEach(([key, group]) => {
result.push({
title: group.title,
children: group.items
});
});
return result;
};
// Function to get all directories as sidebar navigation
const getAllDirectoriesSidebar = async () => {
const directories = getContentDirectories();
const result = [];
directories.forEach(dir => {
const dirContent = getSidebarTree(dir.name);
if (dirContent.length > 0) {
result.push({
title: dir.title,
url: dir.url,
children: dirContent
});
}
});
return result;
};
// Get all unique tags from all content
const getAllTags = async () => {
const allContent = await getAllContent();
const tagsSet = new Set();
allContent.forEach(entry => {
if (entry.metadata.tags && Array.isArray(entry.metadata.tags)) {
entry.metadata.tags.forEach(tag => tagsSet.add(tag.toLowerCase()));
}
});
return Array.from(tagsSet).sort();
};
// Get all posts that have a specific tag
const getPostsByTag = async (tag) => {
const allContent = await getAllContent();
const normalizedTag = tag.toLowerCase();
return allContent.filter(entry => {
if (!entry.metadata.tags || !Array.isArray(entry.metadata.tags)) {
return false;
}
return entry.metadata.tags.some(t => t.toLowerCase() === normalizedTag);
});
};
// Export functions
export {
scanContentDirectory,
getContentDirectories,
formatTitle,
getAllContent,
getContentByUrl,
getContentByDirectory,
clearContentCache,
getSubDirectories,
processTemplateVariables,
getSidebarTree,
getAllDirectoriesSidebar,
getAllTags,
getPostsByTag
};