@terrymooreii/sia 2.1.5 → 2.1.7
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/_config.yml +3 -1
- package/bin/cli.js +7 -0
- package/docs/README.md +135 -0
- package/docs/creating-themes.md +987 -0
- package/docs/front-matter.md +557 -0
- package/docs/markdown-guide.md +536 -0
- package/docs/template-reference.md +581 -0
- package/lib/assets.js +15 -8
- package/lib/build.js +8 -4
- package/lib/config.js +3 -1
- package/lib/content.js +74 -2
- package/lib/init.js +3 -3
- package/lib/templates.js +14 -6
- package/lib/theme-resolver.js +175 -0
- package/lib/theme.js +1524 -0
- package/package.json +1 -1
- package/readme.md +51 -2
- package/themes/developer/includes/hero.njk +6 -0
- package/themes/developer/pages/index.njk +2 -5
- package/themes/magazine/includes/hero.njk +8 -0
- package/themes/magazine/pages/index.njk +4 -9
- package/themes/main/includes/footer.njk +1 -1
- package/themes/main/includes/hero.njk +6 -0
- package/themes/main/pages/index.njk +2 -5
- package/themes/minimal/includes/footer.njk +1 -1
- package/themes/minimal/includes/hero.njk +6 -0
- package/themes/minimal/pages/index.njk +2 -5
package/lib/content.js
CHANGED
|
@@ -247,6 +247,72 @@ marked.use({
|
|
|
247
247
|
}
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Safely truncate markdown text without breaking inline formatting
|
|
252
|
+
* Avoids cutting in the middle of links, bold, italic, code, images, etc.
|
|
253
|
+
*/
|
|
254
|
+
function truncateMarkdownSafely(text, maxLength) {
|
|
255
|
+
if (text.length <= maxLength) {
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Find all inline markdown element ranges to avoid cutting inside them
|
|
260
|
+
const inlinePatterns = [
|
|
261
|
+
/!\[([^\]]*)\]\([^)]*\)/g, // Images  - must come before links
|
|
262
|
+
/\[([^\]]*)\]\([^)]*\)/g, // Links [text](url)
|
|
263
|
+
/\[([^\]]*)\]\[[^\]]*\]/g, // Reference links [text][ref]
|
|
264
|
+
/\*\*([^*]+)\*\*/g, // Bold **text**
|
|
265
|
+
/__([^_]+)__/g, // Bold __text__
|
|
266
|
+
/\*([^*\n]+)\*/g, // Italic *text*
|
|
267
|
+
/_([^_\n]+)_/g, // Italic _text_
|
|
268
|
+
/`([^`]+)`/g, // Inline code `code`
|
|
269
|
+
/~~([^~]+)~~/g, // Strikethrough ~~text~~
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
// Collect all ranges where inline elements exist
|
|
273
|
+
const ranges = [];
|
|
274
|
+
for (const pattern of inlinePatterns) {
|
|
275
|
+
let match;
|
|
276
|
+
// Reset lastIndex for each pattern
|
|
277
|
+
pattern.lastIndex = 0;
|
|
278
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
279
|
+
ranges.push({ start: match.index, end: match.index + match[0].length });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Sort ranges by start position
|
|
284
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
285
|
+
|
|
286
|
+
// Find the best truncation point
|
|
287
|
+
let truncateAt = maxLength;
|
|
288
|
+
|
|
289
|
+
// Check if our target position is inside any markdown element
|
|
290
|
+
for (const range of ranges) {
|
|
291
|
+
if (truncateAt > range.start && truncateAt < range.end) {
|
|
292
|
+
// We're inside this element - decide whether to include it or exclude it
|
|
293
|
+
if (range.end <= maxLength + 50) {
|
|
294
|
+
// Include the whole element if it doesn't extend too far past our limit
|
|
295
|
+
truncateAt = range.end;
|
|
296
|
+
} else {
|
|
297
|
+
// Otherwise, truncate before this element starts
|
|
298
|
+
truncateAt = range.start;
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Try to break at a word boundary for cleaner excerpts
|
|
305
|
+
if (truncateAt > 0) {
|
|
306
|
+
const lastSpace = text.lastIndexOf(' ', truncateAt);
|
|
307
|
+
// Only use word boundary if it's reasonably close to our target
|
|
308
|
+
if (lastSpace > truncateAt - 30 && lastSpace > maxLength * 0.5) {
|
|
309
|
+
truncateAt = lastSpace;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return text.substring(0, truncateAt).trim() + '...';
|
|
314
|
+
}
|
|
315
|
+
|
|
250
316
|
/**
|
|
251
317
|
* Generate a URL-friendly slug from a string
|
|
252
318
|
*/
|
|
@@ -321,12 +387,17 @@ export function parseContent(filePath) {
|
|
|
321
387
|
if (!excerpt) {
|
|
322
388
|
const firstParagraph = markdown.split('\n\n')[0];
|
|
323
389
|
excerpt = firstParagraph.replace(/^#+\s+.+\n?/, '').trim();
|
|
324
|
-
// Limit excerpt length
|
|
390
|
+
// Limit excerpt length using safe truncation that preserves markdown syntax
|
|
325
391
|
if (excerpt.length > 200) {
|
|
326
|
-
excerpt = excerpt
|
|
392
|
+
excerpt = truncateMarkdownSafely(excerpt, 200);
|
|
327
393
|
}
|
|
328
394
|
}
|
|
329
395
|
|
|
396
|
+
// Create HTML version of excerpt for templates that need rendered output
|
|
397
|
+
let excerptHtml = marked.parse(excerpt);
|
|
398
|
+
// Clean up the HTML (remove wrapping <p> tags for inline use)
|
|
399
|
+
excerptHtml = excerptHtml.replace(/^<p>/, '').replace(/<\/p>\n?$/, '');
|
|
400
|
+
|
|
330
401
|
// Normalize tags to array
|
|
331
402
|
let tags = frontMatter.tags || [];
|
|
332
403
|
if (typeof tags === 'string') {
|
|
@@ -338,6 +409,7 @@ export function parseContent(filePath) {
|
|
|
338
409
|
slug,
|
|
339
410
|
date,
|
|
340
411
|
excerpt,
|
|
412
|
+
excerptHtml,
|
|
341
413
|
tags,
|
|
342
414
|
content: html,
|
|
343
415
|
rawContent: markdown,
|
package/lib/init.js
CHANGED
|
@@ -87,7 +87,7 @@ Welcome to my new blog! This is my first post.
|
|
|
87
87
|
|
|
88
88
|
## About This Site
|
|
89
89
|
|
|
90
|
-
This site is built with [Sia](https://github.com/
|
|
90
|
+
This site is built with [Sia](https://github.com/terrymooreii/sia), a simple and powerful static site generator.
|
|
91
91
|
|
|
92
92
|
## What's Next?
|
|
93
93
|
|
|
@@ -114,7 +114,7 @@ Hello! I'm ${author}. Welcome to my corner of the internet.
|
|
|
114
114
|
|
|
115
115
|
## About This Site
|
|
116
116
|
|
|
117
|
-
This site is built with [Sia](https://github.com/
|
|
117
|
+
This site is built with [Sia](https://github.com/terrymooreii/sia), a simple static site generator that supports:
|
|
118
118
|
|
|
119
119
|
- Markdown with front matter
|
|
120
120
|
- Blog posts, pages, and notes
|
|
@@ -216,7 +216,7 @@ Then upload the \`dist/\` folder to any static hosting.
|
|
|
216
216
|
|
|
217
217
|
return `# ${title}
|
|
218
218
|
|
|
219
|
-
A static site built with [Sia](https://github.com/
|
|
219
|
+
A static site built with [Sia](https://github.com/terrymooreii/sia).
|
|
220
220
|
|
|
221
221
|
## Getting Started
|
|
222
222
|
|
package/lib/templates.js
CHANGED
|
@@ -2,6 +2,7 @@ import nunjucks from 'nunjucks';
|
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import { resolveTheme, getBuiltInThemesDir } from './theme-resolver.js';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
const __dirname = dirname(__filename);
|
|
@@ -12,7 +13,8 @@ const __dirname = dirname(__filename);
|
|
|
12
13
|
function dateFilter(date, format = 'long') {
|
|
13
14
|
if (!date) return '';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// Handle special "now" keyword for current date/time
|
|
17
|
+
const d = (date === 'now') ? new Date() : new Date(date);
|
|
16
18
|
|
|
17
19
|
if (isNaN(d.getTime())) return '';
|
|
18
20
|
|
|
@@ -189,8 +191,11 @@ function createUrlFilter(basePath) {
|
|
|
189
191
|
|
|
190
192
|
/**
|
|
191
193
|
* Create and configure the Nunjucks environment
|
|
194
|
+
*
|
|
195
|
+
* @param {object} config - Site configuration
|
|
196
|
+
* @param {object} [resolvedTheme] - Pre-resolved theme info from resolveTheme()
|
|
192
197
|
*/
|
|
193
|
-
export function createTemplateEngine(config) {
|
|
198
|
+
export function createTemplateEngine(config, resolvedTheme = null) {
|
|
194
199
|
// Set up template paths - user layouts first, then defaults
|
|
195
200
|
const templatePaths = [];
|
|
196
201
|
|
|
@@ -204,15 +209,18 @@ export function createTemplateEngine(config) {
|
|
|
204
209
|
templatePaths.push(config.includesDir);
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
//
|
|
208
|
-
const themeName = config.theme || 'main';
|
|
209
|
-
const
|
|
212
|
+
// Resolve theme if not already resolved
|
|
213
|
+
const themeName = config.theme?.name || 'main';
|
|
214
|
+
const theme = resolvedTheme || resolveTheme(themeName, config.rootDir);
|
|
215
|
+
const themeDir = theme.themeDir;
|
|
216
|
+
|
|
217
|
+
// Add theme template paths
|
|
210
218
|
templatePaths.push(join(themeDir, 'layouts'));
|
|
211
219
|
templatePaths.push(join(themeDir, 'includes'));
|
|
212
220
|
templatePaths.push(join(themeDir, 'pages'));
|
|
213
221
|
|
|
214
222
|
// Shared includes available to all themes
|
|
215
|
-
const sharedIncludesDir = join(
|
|
223
|
+
const sharedIncludesDir = join(getBuiltInThemesDir(), '_shared', 'includes');
|
|
216
224
|
templatePaths.push(sharedIncludesDir);
|
|
217
225
|
|
|
218
226
|
// Create the environment
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
|
|
9
|
+
// Built-in themes directory
|
|
10
|
+
const builtInThemesDir = join(__dirname, '..', 'themes');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the path to a theme directory
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. Check built-in themes folder (sia's themes/)
|
|
17
|
+
* 2. Check for npm package sia-theme-{name}
|
|
18
|
+
* 3. Fall back to 'main' theme
|
|
19
|
+
*
|
|
20
|
+
* @param {string} themeName - The theme name from config
|
|
21
|
+
* @param {string} rootDir - The user's project root directory
|
|
22
|
+
* @returns {{ themeDir: string, themeName: string, isExternal: boolean }}
|
|
23
|
+
*/
|
|
24
|
+
export function resolveTheme(themeName, rootDir = process.cwd()) {
|
|
25
|
+
// 1. Check built-in themes folder
|
|
26
|
+
const builtInThemeDir = join(builtInThemesDir, themeName);
|
|
27
|
+
if (existsSync(builtInThemeDir)) {
|
|
28
|
+
return {
|
|
29
|
+
themeDir: builtInThemeDir,
|
|
30
|
+
themeName,
|
|
31
|
+
isExternal: false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Check for npm package sia-theme-{name}
|
|
36
|
+
const packageName = `sia-theme-${themeName}`;
|
|
37
|
+
const externalThemeDir = resolveExternalTheme(packageName, rootDir);
|
|
38
|
+
|
|
39
|
+
if (externalThemeDir) {
|
|
40
|
+
console.log(`🎨 Using external theme package: ${packageName}`);
|
|
41
|
+
return {
|
|
42
|
+
themeDir: externalThemeDir,
|
|
43
|
+
themeName,
|
|
44
|
+
isExternal: true
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. Fall back to 'main' theme
|
|
49
|
+
if (themeName !== 'main') {
|
|
50
|
+
console.log(`⚠️ Theme "${themeName}" not found, falling back to "main" theme`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
themeDir: join(builtInThemesDir, 'main'),
|
|
55
|
+
themeName: 'main',
|
|
56
|
+
isExternal: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Attempt to resolve an external theme package
|
|
62
|
+
*
|
|
63
|
+
* @param {string} packageName - The npm package name (sia-theme-{name})
|
|
64
|
+
* @param {string} rootDir - The user's project root directory
|
|
65
|
+
* @returns {string|null} The theme directory path or null if not found
|
|
66
|
+
*/
|
|
67
|
+
function resolveExternalTheme(packageName, rootDir) {
|
|
68
|
+
// First, check if the package is in the user's package.json
|
|
69
|
+
const packageJsonPath = join(rootDir, 'package.json');
|
|
70
|
+
|
|
71
|
+
if (!existsSync(packageJsonPath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
77
|
+
const allDeps = {
|
|
78
|
+
...packageJson.dependencies,
|
|
79
|
+
...packageJson.devDependencies
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Check if the theme package is listed as a dependency
|
|
83
|
+
if (!allDeps[packageName]) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Try to resolve the package path
|
|
88
|
+
// Use createRequire to resolve from the user's project directory
|
|
89
|
+
const require = createRequire(join(rootDir, 'package.json'));
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Try to resolve the package's main entry point
|
|
93
|
+
const packageMainPath = require.resolve(packageName);
|
|
94
|
+
const packageDir = dirname(packageMainPath);
|
|
95
|
+
|
|
96
|
+
// The theme directory is typically the package root
|
|
97
|
+
// Check if it has the expected theme structure
|
|
98
|
+
if (isValidThemeDirectory(packageDir)) {
|
|
99
|
+
return packageDir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Sometimes the main entry is in a subdirectory, try parent
|
|
103
|
+
const parentDir = dirname(packageDir);
|
|
104
|
+
if (isValidThemeDirectory(parentDir)) {
|
|
105
|
+
return parentDir;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Try node_modules directly
|
|
109
|
+
const nodeModulesPath = join(rootDir, 'node_modules', packageName);
|
|
110
|
+
if (existsSync(nodeModulesPath) && isValidThemeDirectory(nodeModulesPath)) {
|
|
111
|
+
return nodeModulesPath;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return null;
|
|
115
|
+
} catch (resolveErr) {
|
|
116
|
+
// Package might not be installed yet
|
|
117
|
+
// Try node_modules directly as fallback
|
|
118
|
+
const nodeModulesPath = join(rootDir, 'node_modules', packageName);
|
|
119
|
+
if (existsSync(nodeModulesPath) && isValidThemeDirectory(nodeModulesPath)) {
|
|
120
|
+
return nodeModulesPath;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a directory has the expected theme structure
|
|
131
|
+
*
|
|
132
|
+
* @param {string} dir - Directory to check
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
function isValidThemeDirectory(dir) {
|
|
136
|
+
if (!existsSync(dir)) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A valid theme must have at least layouts and pages directories
|
|
141
|
+
const hasLayouts = existsSync(join(dir, 'layouts'));
|
|
142
|
+
const hasPages = existsSync(join(dir, 'pages'));
|
|
143
|
+
|
|
144
|
+
return hasLayouts && hasPages;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get the built-in themes directory path
|
|
149
|
+
*
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
export function getBuiltInThemesDir() {
|
|
153
|
+
return builtInThemesDir;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get list of available built-in themes
|
|
158
|
+
*
|
|
159
|
+
* @returns {string[]}
|
|
160
|
+
*/
|
|
161
|
+
export function getBuiltInThemes() {
|
|
162
|
+
return readdirSync(builtInThemesDir)
|
|
163
|
+
.filter(name => {
|
|
164
|
+
if (name.startsWith('_')) return false; // Skip _shared
|
|
165
|
+
const themePath = join(builtInThemesDir, name);
|
|
166
|
+
return statSync(themePath).isDirectory();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default {
|
|
171
|
+
resolveTheme,
|
|
172
|
+
getBuiltInThemesDir,
|
|
173
|
+
getBuiltInThemes
|
|
174
|
+
};
|
|
175
|
+
|