@terrymooreii/sia 1.0.2 → 2.0.0

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 (61) hide show
  1. package/_config.yml +33 -0
  2. package/bin/cli.js +51 -0
  3. package/defaults/includes/footer.njk +14 -0
  4. package/defaults/includes/header.njk +71 -0
  5. package/defaults/includes/pagination.njk +26 -0
  6. package/defaults/includes/tag-list.njk +11 -0
  7. package/defaults/layouts/base.njk +41 -0
  8. package/defaults/layouts/note.njk +25 -0
  9. package/defaults/layouts/page.njk +14 -0
  10. package/defaults/layouts/post.njk +43 -0
  11. package/defaults/pages/blog.njk +36 -0
  12. package/defaults/pages/feed.njk +28 -0
  13. package/defaults/pages/index.njk +60 -0
  14. package/defaults/pages/notes.njk +34 -0
  15. package/defaults/pages/tag.njk +41 -0
  16. package/defaults/pages/tags.njk +39 -0
  17. package/defaults/styles/main.css +1074 -0
  18. package/lib/assets.js +234 -0
  19. package/lib/build.js +260 -19
  20. package/lib/collections.js +191 -0
  21. package/lib/config.js +114 -0
  22. package/lib/content.js +323 -0
  23. package/lib/index.js +53 -18
  24. package/lib/init.js +555 -6
  25. package/lib/new.js +379 -41
  26. package/lib/server.js +257 -0
  27. package/lib/templates.js +249 -0
  28. package/package.json +30 -15
  29. package/readme.md +212 -63
  30. package/src/images/.gitkeep +3 -0
  31. package/src/notes/2024-12-17-first-note.md +6 -0
  32. package/src/pages/about.md +29 -0
  33. package/src/posts/2024-12-16-markdown-features.md +76 -0
  34. package/src/posts/2024-12-17-welcome-to-sia.md +78 -0
  35. package/src/posts/2024-12-17-welcome-to-static-forge.md +78 -0
  36. package/.prettierignore +0 -3
  37. package/.prettierrc +0 -8
  38. package/lib/helpers.js +0 -37
  39. package/lib/markdown.js +0 -33
  40. package/lib/parse.js +0 -100
  41. package/lib/readconfig.js +0 -18
  42. package/lib/rss.js +0 -63
  43. package/templates/siarc-template.js +0 -53
  44. package/templates/src/_partials/_footer.njk +0 -1
  45. package/templates/src/_partials/_head.njk +0 -35
  46. package/templates/src/_partials/_header.njk +0 -1
  47. package/templates/src/_partials/_layout.njk +0 -12
  48. package/templates/src/_partials/_nav.njk +0 -12
  49. package/templates/src/_partials/page.njk +0 -5
  50. package/templates/src/_partials/post.njk +0 -13
  51. package/templates/src/_partials/posts.njk +0 -19
  52. package/templates/src/assets/android-chrome-192x192.png +0 -0
  53. package/templates/src/assets/android-chrome-512x512.png +0 -0
  54. package/templates/src/assets/apple-touch-icon.png +0 -0
  55. package/templates/src/assets/favicon-16x16.png +0 -0
  56. package/templates/src/assets/favicon-32x32.png +0 -0
  57. package/templates/src/assets/favicon.ico +0 -0
  58. package/templates/src/assets/site.webmanifest +0 -19
  59. package/templates/src/content/index.md +0 -7
  60. package/templates/src/css/markdown.css +0 -1210
  61. package/templates/src/css/theme.css +0 -120
package/lib/config.js ADDED
@@ -0,0 +1,114 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import yaml from 'js-yaml';
4
+
5
+ // Default configuration
6
+ const defaultConfig = {
7
+ site: {
8
+ title: 'My Site',
9
+ description: 'A static site built with Static Forge',
10
+ url: 'http://localhost:3000',
11
+ author: 'Anonymous'
12
+ },
13
+ input: 'src',
14
+ output: 'dist',
15
+ layouts: '_layouts',
16
+ includes: '_includes',
17
+ collections: {
18
+ posts: {
19
+ path: 'posts',
20
+ layout: 'post',
21
+ permalink: '/blog/:slug/',
22
+ sortBy: 'date',
23
+ sortOrder: 'desc'
24
+ },
25
+ pages: {
26
+ path: 'pages',
27
+ layout: 'page',
28
+ permalink: '/:slug/'
29
+ },
30
+ notes: {
31
+ path: 'notes',
32
+ layout: 'note',
33
+ permalink: '/notes/:slug/',
34
+ sortBy: 'date',
35
+ sortOrder: 'desc'
36
+ }
37
+ },
38
+ pagination: {
39
+ size: 10
40
+ },
41
+ server: {
42
+ port: 3000
43
+ }
44
+ };
45
+
46
+ /**
47
+ * Deep merge two objects
48
+ */
49
+ function deepMerge(target, source) {
50
+ const result = { ...target };
51
+
52
+ for (const key in source) {
53
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
54
+ result[key] = deepMerge(result[key] || {}, source[key]);
55
+ } else {
56
+ result[key] = source[key];
57
+ }
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ /**
64
+ * Load configuration from YAML or JSON file
65
+ */
66
+ export function loadConfig(rootDir = process.cwd()) {
67
+ let userConfig = {};
68
+
69
+ // Try to load _config.yml first, then _config.json
70
+ const yamlPath = join(rootDir, '_config.yml');
71
+ const jsonPath = join(rootDir, '_config.json');
72
+
73
+ if (existsSync(yamlPath)) {
74
+ try {
75
+ const content = readFileSync(yamlPath, 'utf-8');
76
+ userConfig = yaml.load(content) || {};
77
+ console.log('📄 Loaded configuration from _config.yml');
78
+ } catch (err) {
79
+ console.error('Error parsing _config.yml:', err.message);
80
+ }
81
+ } else if (existsSync(jsonPath)) {
82
+ try {
83
+ const content = readFileSync(jsonPath, 'utf-8');
84
+ userConfig = JSON.parse(content);
85
+ console.log('📄 Loaded configuration from _config.json');
86
+ } catch (err) {
87
+ console.error('Error parsing _config.json:', err.message);
88
+ }
89
+ } else {
90
+ console.log('⚠️ No config file found, using defaults');
91
+ }
92
+
93
+ // Merge user config with defaults
94
+ const config = deepMerge(defaultConfig, userConfig);
95
+
96
+ // Add computed paths
97
+ config.rootDir = rootDir;
98
+ config.inputDir = join(rootDir, config.input);
99
+ config.outputDir = join(rootDir, config.output);
100
+ config.layoutsDir = join(rootDir, config.layouts);
101
+ config.includesDir = join(rootDir, config.includes);
102
+
103
+ return config;
104
+ }
105
+
106
+ /**
107
+ * Get the default configuration
108
+ */
109
+ export function getDefaultConfig() {
110
+ return { ...defaultConfig };
111
+ }
112
+
113
+ export default { loadConfig, getDefaultConfig };
114
+
package/lib/content.js ADDED
@@ -0,0 +1,323 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
2
+ import { join, basename, extname, relative } from 'path';
3
+ import matter from 'gray-matter';
4
+ import { Marked } from 'marked';
5
+ import { markedHighlight } from 'marked-highlight';
6
+ import { markedEmoji } from 'marked-emoji';
7
+ import hljs from 'highlight.js';
8
+
9
+ /**
10
+ * Default emoji map for shortcode support
11
+ * Common emojis - extend as needed
12
+ */
13
+ const emojis = {
14
+ smile: '😄',
15
+ grinning: '😀',
16
+ joy: '😂',
17
+ heart: '❤️',
18
+ thumbsup: '👍',
19
+ thumbsdown: '👎',
20
+ clap: '👏',
21
+ fire: '🔥',
22
+ rocket: '🚀',
23
+ star: '⭐',
24
+ sparkles: '✨',
25
+ check: '✅',
26
+ x: '❌',
27
+ warning: '⚠️',
28
+ bulb: '💡',
29
+ memo: '📝',
30
+ book: '📖',
31
+ link: '🔗',
32
+ eyes: '👀',
33
+ thinking: '🤔',
34
+ wave: '👋',
35
+ pray: '🙏',
36
+ muscle: '💪',
37
+ tada: '🎉',
38
+ party: '🥳',
39
+ coffee: '☕',
40
+ bug: '🐛',
41
+ wrench: '🔧',
42
+ hammer: '🔨',
43
+ gear: '⚙️',
44
+ lock: '🔒',
45
+ key: '🔑',
46
+ zap: '⚡',
47
+ bomb: '💣',
48
+ gem: '💎',
49
+ trophy: '🏆',
50
+ medal: '🏅',
51
+ crown: '👑',
52
+ sun: '☀️',
53
+ moon: '🌙',
54
+ cloud: '☁️',
55
+ rain: '🌧️',
56
+ snow: '❄️',
57
+ earth: '🌍',
58
+ tree: '🌳',
59
+ flower: '🌸',
60
+ apple: '🍎',
61
+ pizza: '🍕',
62
+ beer: '🍺',
63
+ wine: '🍷',
64
+ cat: '🐱',
65
+ dog: '🐶',
66
+ bird: '🐦',
67
+ fish: '🐟',
68
+ whale: '🐳',
69
+ snake: '🐍',
70
+ turtle: '🐢',
71
+ octopus: '🐙',
72
+ crab: '🦀',
73
+ shrimp: '🦐',
74
+ 100: '💯',
75
+ '+1': '👍',
76
+ '-1': '👎',
77
+ };
78
+
79
+ /**
80
+ * Configure marked with syntax highlighting and emoji support
81
+ */
82
+ const marked = new Marked(
83
+ markedHighlight({
84
+ langPrefix: 'hljs language-',
85
+ highlight(code, lang) {
86
+ if (lang && hljs.getLanguage(lang)) {
87
+ try {
88
+ return hljs.highlight(code, { language: lang }).value;
89
+ } catch (err) {
90
+ console.warn(`Highlight.js error for language "${lang}":`, err.message);
91
+ }
92
+ }
93
+ // Auto-detect language if not specified
94
+ try {
95
+ return hljs.highlightAuto(code).value;
96
+ } catch (err) {
97
+ return code;
98
+ }
99
+ }
100
+ }),
101
+ markedEmoji({
102
+ emojis,
103
+ renderer: (token) => token.emoji
104
+ })
105
+ );
106
+
107
+ // Configure marked options
108
+ marked.setOptions({
109
+ gfm: true,
110
+ breaks: false
111
+ });
112
+
113
+ /**
114
+ * Generate a URL-friendly slug from a string
115
+ */
116
+ export function slugify(str) {
117
+ return str
118
+ .toLowerCase()
119
+ .replace(/[^\w\s-]/g, '')
120
+ .replace(/[\s_-]+/g, '-')
121
+ .replace(/^-+|-+$/g, '');
122
+ }
123
+
124
+ /**
125
+ * Extract slug from filename (removes date prefix if present)
126
+ */
127
+ export function getSlugFromFilename(filename) {
128
+ // Remove extension
129
+ const name = basename(filename, extname(filename));
130
+
131
+ // Check for date prefix pattern: YYYY-MM-DD-slug
132
+ const datePattern = /^\d{4}-\d{2}-\d{2}-(.+)$/;
133
+ const match = name.match(datePattern);
134
+
135
+ if (match) {
136
+ return match[1];
137
+ }
138
+
139
+ return slugify(name);
140
+ }
141
+
142
+ /**
143
+ * Extract date from filename if present
144
+ */
145
+ export function getDateFromFilename(filename) {
146
+ const name = basename(filename, extname(filename));
147
+ const datePattern = /^(\d{4}-\d{2}-\d{2})/;
148
+ const match = name.match(datePattern);
149
+
150
+ if (match) {
151
+ return new Date(match[1]);
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * Parse a markdown file with front matter
159
+ */
160
+ export function parseContent(filePath) {
161
+ const content = readFileSync(filePath, 'utf-8');
162
+ const { data: frontMatter, content: markdown } = matter(content);
163
+
164
+ // Parse markdown to HTML
165
+ const html = marked.parse(markdown);
166
+
167
+ // Get slug from front matter or filename
168
+ const slug = frontMatter.slug || getSlugFromFilename(filePath);
169
+
170
+ // Get date from front matter or filename
171
+ let date = frontMatter.date;
172
+ if (date) {
173
+ date = new Date(date);
174
+ } else {
175
+ date = getDateFromFilename(filePath) || new Date();
176
+ }
177
+
178
+ // Extract excerpt (first paragraph or custom excerpt)
179
+ let excerpt = frontMatter.excerpt;
180
+ if (!excerpt) {
181
+ const firstParagraph = markdown.split('\n\n')[0];
182
+ excerpt = firstParagraph.replace(/^#+\s+.+\n?/, '').trim();
183
+ // Limit excerpt length
184
+ if (excerpt.length > 200) {
185
+ excerpt = excerpt.substring(0, 197) + '...';
186
+ }
187
+ }
188
+
189
+ // Normalize tags to array
190
+ let tags = frontMatter.tags || [];
191
+ if (typeof tags === 'string') {
192
+ tags = tags.split(',').map(t => t.trim());
193
+ }
194
+
195
+ return {
196
+ ...frontMatter,
197
+ slug,
198
+ date,
199
+ excerpt,
200
+ tags,
201
+ content: html,
202
+ rawContent: markdown,
203
+ filePath,
204
+ draft: frontMatter.draft || false
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Recursively get all markdown files in a directory
210
+ */
211
+ export function getMarkdownFiles(dir) {
212
+ const files = [];
213
+
214
+ if (!existsSync(dir)) {
215
+ return files;
216
+ }
217
+
218
+ const items = readdirSync(dir);
219
+
220
+ for (const item of items) {
221
+ const fullPath = join(dir, item);
222
+ const stat = statSync(fullPath);
223
+
224
+ if (stat.isDirectory()) {
225
+ files.push(...getMarkdownFiles(fullPath));
226
+ } else if (item.endsWith('.md') || item.endsWith('.markdown')) {
227
+ files.push(fullPath);
228
+ }
229
+ }
230
+
231
+ return files;
232
+ }
233
+
234
+ /**
235
+ * Load all content from a collection directory
236
+ */
237
+ export function loadCollection(config, collectionName) {
238
+ const collectionConfig = config.collections[collectionName];
239
+
240
+ if (!collectionConfig) {
241
+ console.warn(`Collection "${collectionName}" not found in config`);
242
+ return [];
243
+ }
244
+
245
+ const collectionDir = join(config.inputDir, collectionConfig.path);
246
+ const files = getMarkdownFiles(collectionDir);
247
+
248
+ const items = files
249
+ .map(filePath => {
250
+ try {
251
+ const item = parseContent(filePath);
252
+
253
+ // Add collection-specific metadata
254
+ item.collection = collectionName;
255
+ item.layout = item.layout || collectionConfig.layout;
256
+
257
+ // Generate permalink
258
+ let permalink = item.permalink || collectionConfig.permalink || '/:slug/';
259
+ permalink = permalink
260
+ .replace(':slug', item.slug)
261
+ .replace(':year', item.date.getFullYear())
262
+ .replace(':month', String(item.date.getMonth() + 1).padStart(2, '0'))
263
+ .replace(':day', String(item.date.getDate()).padStart(2, '0'));
264
+
265
+ item.url = permalink;
266
+ item.outputPath = join(config.outputDir, permalink, 'index.html');
267
+
268
+ return item;
269
+ } catch (err) {
270
+ console.error(`Error parsing ${filePath}:`, err.message);
271
+ return null;
272
+ }
273
+ })
274
+ .filter(item => item !== null && !item.draft);
275
+
276
+ // Sort items
277
+ const sortBy = collectionConfig.sortBy || 'date';
278
+ const sortOrder = collectionConfig.sortOrder || 'desc';
279
+
280
+ items.sort((a, b) => {
281
+ const aVal = a[sortBy];
282
+ const bVal = b[sortBy];
283
+
284
+ if (aVal instanceof Date && bVal instanceof Date) {
285
+ return sortOrder === 'desc' ? bVal - aVal : aVal - bVal;
286
+ }
287
+
288
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
289
+ return sortOrder === 'desc'
290
+ ? bVal.localeCompare(aVal)
291
+ : aVal.localeCompare(bVal);
292
+ }
293
+
294
+ return 0;
295
+ });
296
+
297
+ return items;
298
+ }
299
+
300
+ /**
301
+ * Load all collections defined in config
302
+ */
303
+ export function loadAllCollections(config) {
304
+ const collections = {};
305
+
306
+ for (const name of Object.keys(config.collections)) {
307
+ collections[name] = loadCollection(config, name);
308
+ console.log(`📚 Loaded ${collections[name].length} items from "${name}" collection`);
309
+ }
310
+
311
+ return collections;
312
+ }
313
+
314
+ export default {
315
+ parseContent,
316
+ slugify,
317
+ getSlugFromFilename,
318
+ getDateFromFilename,
319
+ getMarkdownFiles,
320
+ loadCollection,
321
+ loadAllCollections
322
+ };
323
+
package/lib/index.js CHANGED
@@ -1,22 +1,57 @@
1
- #!/usr/bin/env node
1
+ /**
2
+ * Sia - A simple, powerful static site generator
3
+ */
2
4
 
3
- import { build } from './build.js'
4
- import { create } from './new.js'
5
- import { init } from './init.js'
5
+ export { loadConfig, getDefaultConfig } from './config.js';
6
+ export {
7
+ parseContent,
8
+ slugify,
9
+ getSlugFromFilename,
10
+ loadCollection,
11
+ loadAllCollections
12
+ } from './content.js';
13
+ export {
14
+ buildTagCollections,
15
+ getAllTags,
16
+ paginate,
17
+ buildSiteData,
18
+ getRecentItems,
19
+ getRelatedItems
20
+ } from './collections.js';
21
+ export {
22
+ createTemplateEngine,
23
+ renderTemplate,
24
+ renderString
25
+ } from './templates.js';
26
+ export {
27
+ copyImages,
28
+ copyAssets,
29
+ copyStaticAssets,
30
+ writeFile,
31
+ ensureDir
32
+ } from './assets.js';
33
+ export { build, buildCommand } from './build.js';
34
+ export { startServer, devCommand } from './server.js';
35
+ export { newCommand } from './new.js';
36
+ export { initSite, initCommand } from './init.js';
6
37
 
7
- const [, , cmd, type, folder] = process.argv
38
+ // Default export with version
39
+ import { readFileSync } from 'fs';
40
+ import { join, dirname } from 'path';
41
+ import { fileURLToPath } from 'url';
8
42
 
9
- switch (cmd) {
10
- case init:
11
- init()
12
- break
13
- case 'build':
14
- build()
15
- break
16
- case 'new':
17
- create(type, folder)
18
- break
19
- default:
20
- console.log('Missing command')
21
- break
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const __dirname = dirname(__filename);
45
+
46
+ let version = '1.0.0';
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
49
+ version = pkg.version;
50
+ } catch (e) {
51
+ // Ignore error
22
52
  }
53
+
54
+ export default {
55
+ version
56
+ };
57
+