@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/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 ![alt](url) - 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.substring(0, 197) + '...';
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/sia/sia), a simple and powerful static site generator.
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/sia/sia), a simple static site generator that supports:
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/sia/sia).
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
- const d = new Date(date);
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
- // Default templates from the selected theme
208
- const themeName = config.theme || 'main';
209
- const themeDir = join(__dirname, '..', 'themes', themeName);
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(__dirname, '..', 'themes', '_shared', 'includes');
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
+