@daz4126/swifty 1.12.1 → 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.
package/package.json CHANGED
@@ -1,21 +1,20 @@
1
1
  {
2
2
  "name": "@daz4126/swifty",
3
- "version": "1.12.1",
3
+ "version": "2.0.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "bin": {
7
- "swifty": "./cli.js"
7
+ "swifty": "./src/cli.js"
8
8
  },
9
9
  "files": [
10
- "swifty.js",
11
- "cli.js",
12
- "init.js"
10
+ "src/"
13
11
  ],
14
12
  "scripts": {
15
13
  "test": "mocha",
16
- "build": "node swifty.js",
17
- "start": "node swifty.js && serve dist",
18
- "init": "node init.js"
14
+ "build": "node src/build.js",
15
+ "start": "node src/build.js && serve dist",
16
+ "init": "node src/init.js",
17
+ "watch": "node src/watcher.js"
19
18
  },
20
19
  "keywords": [
21
20
  "static",
@@ -36,7 +35,9 @@
36
35
  "sharp": "^0.33.5"
37
36
  },
38
37
  "devDependencies": {
39
- "mocha": "^11.1.0"
38
+ "chokidar-cli": "^3.0.0",
39
+ "mocha": "^11.1.0",
40
+ "npm-run-all": "^4.1.5"
40
41
  },
41
42
  "directories": {
42
43
  "test": "test"
package/src/assets.js ADDED
@@ -0,0 +1,80 @@
1
+ import fs from "fs/promises";
2
+ import fsExtra from "fs-extra";
3
+ import path from "path";
4
+ import sharp from "sharp";
5
+ import { dirs, defaultConfig } from "./config.js";
6
+
7
+ const validExtensions = {
8
+ css: [".css"],
9
+ js: [".js"],
10
+ images: [".png", ".jpg", ".jpeg", ".gif", ".svg"," .webp"],
11
+ };
12
+
13
+ const ensureAndCopy = async (source, destination, validExts) => {
14
+ if (await fsExtra.pathExists(source)) {
15
+ await fsExtra.ensureDir(destination);
16
+
17
+ const files = await fs.readdir(source);
18
+ await Promise.all(
19
+ files
20
+ .filter(file => validExts.includes(path.extname(file).toLowerCase()))
21
+ .map(file => fsExtra.copy(path.join(source, file), path.join(destination, file)))
22
+ );
23
+ console.log(`Copied valid files from ${source} to ${destination}`);
24
+ } else {
25
+ console.log(`No ${path.basename(source)} found in ${source}`);
26
+ }
27
+ };
28
+ const copyAssets = async () => {
29
+ await ensureAndCopy(dirs.css, path.join(dirs.dist, 'css'), validExtensions.css);
30
+ await ensureAndCopy(dirs.js, path.join(dirs.dist, 'js'), validExtensions.js);
31
+ await ensureAndCopy(dirs.images, path.join(dirs.dist, 'images'), validExtensions.images);
32
+ };
33
+ async function optimizeImages() {
34
+ try {
35
+ const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png'];
36
+ const images_folder = path.join(dirs.dist, "images");
37
+ const files = await fs.readdir(images_folder);
38
+
39
+ await Promise.all(files.map(async (file) => {
40
+ const filePath = path.join(images_folder, file);
41
+ const ext = path.extname(file).toLowerCase();
42
+
43
+ if (!IMAGE_EXTENSIONS.includes(ext)) return;
44
+
45
+ const optimizedPath = path.join(images_folder, `${path.basename(file, ext)}.webp`);
46
+
47
+ if (filePath !== optimizedPath) {
48
+ const image = sharp(filePath);
49
+ const metadata = await image.metadata();
50
+ const originalWidth = metadata.width || 0;
51
+ const maxWidth = defaultConfig.max_image_size || 800;
52
+ const resizeWidth = Math.min(originalWidth, maxWidth);
53
+
54
+ await image
55
+ .resize({ width: resizeWidth })
56
+ .toFormat('webp', { quality: 80 })
57
+ .toFile(optimizedPath);
58
+
59
+ await fs.unlink(filePath);
60
+
61
+ console.log(`Optimized ${file} -> ${optimizedPath}`);
62
+ }
63
+ }));
64
+ } catch (error) {
65
+ console.error('Error optimizing images:', error);
66
+ }
67
+ };
68
+ const generateAssetImports = async (dir, tagTemplate, validExts) => {
69
+ if (!(await fsExtra.pathExists(dir))) return '';
70
+ const files = await fs.readdir(dir);
71
+ return files
72
+ .filter(file => validExts.includes(path.extname(file).toLowerCase()))
73
+ .sort()
74
+ .map(file => tagTemplate(file))
75
+ .join('\n');
76
+ };
77
+ const getCssImports = () => generateAssetImports(dirs.css, (file) => `<link rel="stylesheet" href="/css/${file}" />`, validExtensions.css);
78
+ const getJsImports = () => generateAssetImports(dirs.js, (file) => `<script src="/js/${file}"></script>`, validExtensions.js);
79
+
80
+ export { copyAssets, optimizeImages, getCssImports, getJsImports };
package/src/build.js ADDED
@@ -0,0 +1,13 @@
1
+ import { copyAssets, optimizeImages } from "./assets.js";
2
+ import { generatePages, createPages, addLinks } from "./pages.js";
3
+ import { dirs } from "./config.js";
4
+
5
+ async function buildSite() {
6
+ await copyAssets();
7
+ await optimizeImages();
8
+ const pages = await generatePages(dirs.pages);
9
+ await addLinks(pages);
10
+ await createPages(pages);
11
+ }
12
+
13
+ buildSite();
package/src/cli.js ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args[0];
5
+ let outDir = 'dist'; // default
6
+
7
+ // Look for --out [folder]
8
+ const outIndex = args.indexOf('--out');
9
+ if (outIndex !== -1 && args[outIndex + 1]) {
10
+ outDir = args[outIndex + 1];
11
+ }
12
+
13
+ // Pass outDir as an environment variable
14
+ process.env.OUT_DIR = outDir;
15
+
16
+ switch (command) {
17
+ case "init":
18
+ import('./init.js');
19
+ break;
20
+ case "build":
21
+ import('./build.js');
22
+ break;
23
+ case "start":
24
+ import('./build.js').then(async () => {
25
+ const { execSync } = await import('child_process');
26
+ execSync(`npx serve ${process.env.OUT_DIR}`, { stdio: 'inherit' });
27
+ });
28
+ break;
29
+ default:
30
+ console.log(`Unknown command: ${command}`);
31
+ console.log(`Usage: swifty [init|build|start] [--out folder]`);
32
+ }
package/src/config.js ADDED
@@ -0,0 +1,36 @@
1
+ import path from "path";
2
+ import { fileURLToPath } from "url";
3
+ import fs from "fs/promises";
4
+ import yaml from "js-yaml";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const isInstalled = process.cwd() !== __dirname;
9
+ const baseDir = isInstalled ? process.cwd() : __dirname;
10
+
11
+ const dirs = {
12
+ pages: path.join(baseDir, "pages"),
13
+ images: path.join(baseDir, "images"),
14
+ dist: path.join(baseDir, "dist"),
15
+ layouts: path.join(baseDir, "layouts"),
16
+ css: path.join(baseDir, "css"),
17
+ js: path.join(baseDir, "js"),
18
+ partials: path.join(baseDir, "partials"),
19
+ };
20
+
21
+ async function loadConfig(dir) {
22
+ const configFiles = ['config.yaml', 'config.yml', 'config.json'];
23
+ for (const file of configFiles) {
24
+ const filePath = path.join(dir, file);
25
+ try {
26
+ await fs.access(filePath);
27
+ const content = await fs.readFile(filePath, 'utf-8');
28
+ return file.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
29
+ } catch {}
30
+ }
31
+ return {};
32
+ }
33
+
34
+ const defaultConfig = await loadConfig(baseDir);
35
+
36
+ export { baseDir, dirs, defaultConfig, loadConfig };
package/src/layout.js ADDED
@@ -0,0 +1,43 @@
1
+ import fs from "fs/promises";
2
+ import fsExtra from "fs-extra";
3
+ import path from "path";
4
+ import { dirs, baseDir } from "./config.js";
5
+ import { getCssImports, getJsImports } from "./assets.js";
6
+
7
+ const layoutCache = new Map();
8
+ const getLayout = async (layoutName) => {
9
+ if (!layoutName) return null;
10
+ if (!layoutCache.has(layoutName)) {
11
+ const layoutPath = path.join(dirs.layouts, `${layoutName}.html`);
12
+ if (await fsExtra.pathExists(layoutPath)) {
13
+ const layoutContent = await fs.readFile(layoutPath, 'utf-8');
14
+ layoutCache.set(layoutName, layoutContent);
15
+ } else {
16
+ return null;
17
+ }
18
+ }
19
+ return layoutCache.get(layoutName);
20
+ };
21
+
22
+ const createTemplate = async () => {
23
+ // Read the template from pages folder
24
+ const templatePath = path.join(baseDir, 'template.html');
25
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
26
+ const turboScript = `<script type="module">import * as Turbo from 'https://esm.sh/@hotwired/turbo';</script>`;
27
+ const css = await getCssImports();
28
+ const js = await getJsImports();
29
+ const imports = css + js;
30
+ const highlightCSS = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/monokai-sublime.min.css">`;
31
+ const template = templateContent.replace('</head>', `${turboMetaTag}\n${turboScript}\n${highlightCSS}\n${imports}\n</head>`);
32
+ return template;
33
+ };
34
+
35
+ const applyLayoutAndWrapContent = async (page,content) => {
36
+ const layoutContent = await getLayout(page.data.layout !== undefined ? page.data.layout : page.layout);
37
+ if (!layoutContent) return content;
38
+ return layoutContent.replace(/\{\{\s*content\s*\}\}/g, content);
39
+ };
40
+
41
+ const template = await createTemplate();
42
+
43
+ export { getLayout, applyLayoutAndWrapContent, template };
package/src/pages.js ADDED
@@ -0,0 +1,210 @@
1
+ // pages.js
2
+ import { replacePlaceholders } from "./partials.js";
3
+ import { dirs, defaultConfig, loadConfig } from "./config.js";
4
+ import { template, applyLayoutAndWrapContent } from "./layout.js";
5
+ import { marked } from "marked";
6
+ import matter from "gray-matter";
7
+ import fs from "fs/promises";
8
+ import fsExtra from "fs-extra";
9
+ import path from "path";
10
+
11
+ const isValid = async (filePath) => {
12
+ try {
13
+ const stats = await fs.stat(filePath);
14
+ return stats.isDirectory() || path.extname(filePath) === '.md';
15
+ } catch (err) {
16
+ return false; // Handle errors like file not found
17
+ }
18
+ };
19
+
20
+ const capitalize = (str) => str.replace(/\b\w/g, (char) => char.toUpperCase());
21
+
22
+ const tagsMap = new Map();
23
+ const pageIndex = [];
24
+
25
+ const addToTagMap = (tag, page) => {
26
+ if (!tagsMap.has(tag)) tagsMap.set(tag, []);
27
+ tagsMap.get(tag).push({ title: page.title, url: page.url });
28
+ };
29
+
30
+ const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
31
+ const pages = [];
32
+ const folderConfig = await loadConfig(sourceDir);
33
+ const config = { ...defaultConfig, ...parent?.data, ...folderConfig };
34
+
35
+ try {
36
+ const files = await fs.readdir(sourceDir, { withFileTypes: true });
37
+
38
+ // Collect promises for processing all files
39
+ const filePromises = files.map(async (file) => {
40
+ const filePath = path.join(sourceDir, file.name);
41
+ if (!(await isValid(filePath))) return null;
42
+
43
+ const root = file.name === "index.md" && !parent;
44
+ const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/");
45
+ const finalPath = `/${relativePath.replace(/\.md$/, "")}`;
46
+ const name = root ? "Home" : capitalize(file.name.replace(/\.md$/, "").replace(/-/g, " "));
47
+ const stats = await fs.stat(filePath);
48
+ const isDirectory = file.isDirectory();
49
+ const layoutFileExists = parent && await fsExtra.pathExists(`${dirs.layouts}/${parent.filename}.html`);
50
+ const layout = layoutFileExists ? parent.filename : parent ? parent.layout : "pages";
51
+
52
+ const page = {
53
+ name, root, layout, filePath,
54
+ filename: file.name.replace(/\.md$/, ""),
55
+ url: root ? "/" : finalPath,
56
+ nav: !parent && !root,
57
+ parent: parent ? { title: parent.data.title, url: parent.url } : undefined,
58
+ folder: isDirectory,
59
+ title: name,
60
+ created_at: new Date(stats.birthtime).toLocaleDateString(undefined, config.dateFormat),
61
+ updated_at: new Date(stats.mtime).toLocaleDateString(undefined, config.dateFormat),
62
+ date: new Date(stats.mtime).toLocaleDateString(undefined, config.dateFormat),
63
+ data: root ? { ...defaultConfig } : { ...config }
64
+ };
65
+
66
+ if (path.extname(file.name) === ".md") {
67
+ const markdownContent = await fs.readFile(filePath, "utf-8");
68
+ const { data, content } = matter(markdownContent);
69
+ Object.assign(page, { data: { ...page.data, ...data }, content });
70
+ }
71
+
72
+ // For directories, we defer recursion separately
73
+ return { page, isDirectory };
74
+ });
75
+
76
+ // Await all file processing
77
+ const fileResults = await Promise.all(filePromises);
78
+
79
+ // Now handle directories recursively
80
+ const directoryPromises = fileResults.map(async (result) => {
81
+ if (!result) return;
82
+
83
+ const { page, isDirectory } = result;
84
+
85
+ if (isDirectory) {
86
+ page.pages = await generatePages(page.filePath, baseDir, page);
87
+
88
+ page.pages.sort((a, b) => {
89
+ if (a.data.position && b.data.position) {
90
+ return a.data.position - b.data.position;
91
+ }
92
+ return new Date(a.updated_at) - new Date(b.updated_at);
93
+ });
94
+
95
+ page.content = await generateLinkList(page.filename, page.pages);
96
+ }
97
+
98
+ // Add tags
99
+ if (page.data.tags) {
100
+ page.data.tags.forEach(tag => addToTagMap(tag, page));
101
+ }
102
+
103
+ pages.push(page);
104
+ pageIndex.push({ url: page.url, title: page.title, nav: page.nav });
105
+ });
106
+
107
+ // Await all directory recursion
108
+ await Promise.all(directoryPromises);
109
+
110
+ } catch (err) {
111
+ console.error("Error reading directory:", err);
112
+ }
113
+
114
+ // Make Tags page
115
+ if (!parent && tagsMap.size) {
116
+ const tagLayout = await fsExtra.pathExists(`${dirs.layouts}/tags.html`);
117
+ const tagPage = {
118
+ url: "/tags",
119
+ nav: false,
120
+ folder: true,
121
+ name: "Tags",
122
+ title: "All Tags",
123
+ layout: "pages",
124
+ updated_at: new Date().toLocaleDateString(undefined, defaultConfig.dateFormat),
125
+ data: { ...config },
126
+ pages: []
127
+ };
128
+
129
+ for (const [tag, pagesForTag] of tagsMap) {
130
+ const page = {
131
+ name: tag,
132
+ title: tag,
133
+ updated_at: new Date().toLocaleDateString(undefined, defaultConfig.dateFormat),
134
+ url: `/tags/${tag}`,
135
+ layout: tagLayout ? "tags" : "pages",
136
+ data: { ...config, title: `Pages tagged with ${capitalize(tag)}` },
137
+ };
138
+ page.content = pagesForTag
139
+ .map(p => `* <a href="${p.url}">${p.title}</a>`)
140
+ .join('\n');
141
+
142
+ tagPage.pages.push(page);
143
+ }
144
+
145
+ tagPage.content = await generateLinkList("tags", tagPage.pages);
146
+ pages.push(tagPage);
147
+ }
148
+ return pages;
149
+ };
150
+
151
+
152
+ const generateLinkList = async (name,pages) => {
153
+ const partial = `${name}.md`;
154
+ const partialPath = path.join(dirs.partials, partial);
155
+ const linksPath = path.join(dirs.partials, "links.md");
156
+ // Check if either file exists in the 'partials' folder
157
+ const fileExists = await fsExtra.pathExists(partialPath);
158
+ const defaultExists = await fsExtra.pathExists(linksPath);
159
+ if (fileExists || defaultExists) {
160
+ const partial = await fs.readFile(fileExists ? partialPath : linksPath, "utf-8");
161
+ const content = await Promise.all(pages.map(page => replacePlaceholders(partial, page)));
162
+ return content.join('\n');
163
+ } else {
164
+ return `${pages.map(page => `<li><a href="${page.url}" class="${defaultConfig.link_class}">${page.title}</a></li>`).join`\n`}`
165
+ }
166
+ };
167
+
168
+ const render = async page => {
169
+ const htmlContent = marked.parse(page.content); // Markdown processed once
170
+ const wrappedContent = await applyLayoutAndWrapContent(page, htmlContent);
171
+ const htmlWithTemplate = template.replace(/\{\{\s*content\s*\}\}/g, wrappedContent);
172
+ const finalContent = await replacePlaceholders(htmlWithTemplate, page);
173
+ return finalContent;
174
+ };
175
+
176
+ const createPages = async (pages, distDir = dirs.dist) => {
177
+ await Promise.all(pages.map(async (page) => {
178
+ const html = await render(page);
179
+ const pageDir = path.join(distDir, page.url);
180
+ const pagePath = path.join(distDir, page.url, "index.html");
181
+
182
+ await fsExtra.ensureDir(pageDir);
183
+ await fs.writeFile(pagePath, html);
184
+ console.log(`Created file: ${pagePath}`);
185
+
186
+ if (page.folder) {
187
+ await createPages(page.pages, distDir); // Recursive still needs to await
188
+ }
189
+ }));
190
+ };
191
+
192
+ const addLinks = async (pages, parent) => {
193
+ for (const page of pages) {
194
+ page.data ||= {};
195
+ page.data.links_to_tags = page?.data?.tags?.length
196
+ ? page.data.tags.map(tag => `<a class="${defaultConfig.tag_class}" href="/tags/${tag}">${tag}</a>`).join`` : "";
197
+ const crumb = page.root ? "" : ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}">${page.name}</a>`;
198
+ page.data.breadcrumbs = parent ? parent.data.breadcrumbs + crumb
199
+ : `<a class="${defaultConfig.breadcrumb_class}" href="/">Home</a>` + crumb;
200
+ page.data.links_to_children = page.pages ? await generateLinkList(page.filename, page.pages) : "";
201
+ page.data.links_to_siblings = await generateLinkList(page.parent?.filename || "pages", pages.filter(p => p.url !== page.url));
202
+ page.data.links_to_self_and_siblings = await generateLinkList(page.parent?.filename || "pages", pages);
203
+ page.data.nav_links = await generateLinkList("nav", pageIndex.filter(p => p.nav));
204
+ if (page.pages) {
205
+ await addLinks(page.pages, page); // Recursive call
206
+ }
207
+ }
208
+ };
209
+
210
+ export { generatePages, createPages, pageIndex, addLinks };
@@ -0,0 +1,63 @@
1
+ import fs from "fs/promises";
2
+ import fsExtra from "fs-extra";
3
+ import path from "path";
4
+ import { dirs } from "./config.js";
5
+ import { marked } from "marked";
6
+
7
+ const partialCache = new Map();
8
+
9
+
10
+ const loadPartial = async (partialName) => {
11
+ if (partialCache.has(partialName)) {
12
+ return partialCache.get(partialName);
13
+ }
14
+
15
+ const partialPath = path.join(dirs.partials, `${partialName}.md`);
16
+ if (await fsExtra.pathExists(partialPath)) {
17
+ const partialContent = await fs.readFile(partialPath, "utf-8");
18
+ partialCache.set(partialName, partialContent); // Store in cache
19
+ return partialContent;
20
+ } else {
21
+ console.warn(`Include "${partialName}" not found.`);
22
+ return `<p>Include "${partialName}" not found.</p>`;
23
+ }
24
+ };
25
+
26
+ const replacePlaceholders = async (template, values) => {
27
+ const partialRegex = /{{\s*partial:\s*([\w-]+)\s*}}/g;
28
+ const replaceAsync = async (str, regex, asyncFn) => {
29
+ const matches = [];
30
+ str.replace(regex, (match, ...args) => {
31
+ matches.push(asyncFn(match, ...args));
32
+ return match;
33
+ });
34
+ const results = await Promise.all(matches);
35
+ return str.replace(regex, () => results.shift());
36
+ };
37
+
38
+ // Replace partial includes
39
+ template = await replaceAsync(template, partialRegex, async (match, partialName) => {
40
+ let partialContent = await loadPartial(partialName);
41
+ partialContent = await replacePlaceholders(partialContent, values); // Recursive replacement
42
+ return marked(partialContent); // Convert Markdown to HTML
43
+ });
44
+ // replace image extensions with optimized extension
45
+ template = template.replace(/\.(png|jpe?g|webp)/gi, ".webp");
46
+ // Replace other placeholders **only outside of code blocks**
47
+ const codeBlockRegex = /```[\s\S]*?```|`[^`]+`|<(pre|code)[^>]*>[\s\S]*?<\/\1>/g;
48
+ const codeBlocks = [];
49
+ template = template.replace(codeBlockRegex, match => {
50
+ codeBlocks.push(match);
51
+ return `{{CODE_BLOCK_${codeBlocks.length - 1}}}`; // Temporary placeholder
52
+ });
53
+ // Replace placeholders outside of code blocks
54
+ template = template.replace(/{{\s*([^}\s]+)\s*}}/g, (match, key) => {
55
+ return(values.data && key in values?.data ? values.data[key] : key in values ? values[key] : match)
56
+ });
57
+ // Restore code blocks
58
+ template = template.replace(/{{CODE_BLOCK_(\d+)}}/g, (_, index) => codeBlocks[index]);
59
+
60
+ return template;
61
+ };
62
+
63
+ export { loadPartial, replacePlaceholders };
package/src/watcher.js ADDED
@@ -0,0 +1,39 @@
1
+ import chokidar from 'chokidar';
2
+ import { exec } from 'child_process';
3
+
4
+ // Define which files to watch (you can adjust based on your project structure)
5
+ const filesToWatch = [
6
+ 'pages/**/*.{md,html}', // Watch JavaScript and HTML files in pages directory
7
+ 'layouts/**/*.{html}', // Watch JavaScript and HTML files in layouts directory
8
+ 'images/**/*', // Watch all files in images directory
9
+ 'css/**/*.{css}', // Watch CSS files in css directory
10
+ 'js/**/*.{js}', // Watch JS files in js directory
11
+ 'partials/**/*.{md,html}', // Watch JavaScript and HTML files in partials directory
12
+ 'template.html', // Watch the template HTML file
13
+ 'config.yaml', 'config.yml', 'config.json' // Watch YAML and JSON config files
14
+ ];
15
+ const buildScript = 'npm run build'; // Your build script command
16
+
17
+ // Initialize watcher
18
+ const watcher = chokidar.watch(filesToWatch, {
19
+ persistent: true,
20
+ debounceDelay: 200 // Wait 200ms after the last change to trigger the build
21
+ })
22
+
23
+ // Event listener for file changes
24
+ watcher.on('change', path => {
25
+ console.log(`File ${path} has been changed. Running build...`);
26
+ exec(buildScript, (error, stdout, stderr) => {
27
+ if (error) {
28
+ console.error(`Error executing build: ${error.message}`);
29
+ return;
30
+ }
31
+ if (stderr) {
32
+ console.error(`stderr: ${stderr}`);
33
+ return;
34
+ }
35
+ console.log(stdout); // Output from build process
36
+ });
37
+ });
38
+
39
+ console.log(`Watching files for changes ...`);
package/cli.js DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const args = process.argv.slice(2);
4
-
5
- switch (args[0]) {
6
- case "init":
7
- import('./init.js');
8
- break;
9
- case "build":
10
- import('./swifty.js');
11
- break;
12
- case "start":
13
- // Optional: call build, then run a dev server
14
- import('./swifty.js').then(async () => {
15
- const { execSync } = await import('child_process');
16
- execSync('npx serve dist', { stdio: 'inherit' });
17
- });
18
- break;
19
- default:
20
- console.log(`Unknown command: ${args[0]}`);
21
- console.log(`Usage: swifty [init|build|start]`);
22
- }
package/swifty.js DELETED
@@ -1,504 +0,0 @@
1
- import fs from "fs/promises";
2
- import fsExtra from "fs-extra";
3
- import path from "path";
4
- import matter from "gray-matter";
5
- import yaml from "js-yaml";
6
- import sharp from "sharp";
7
- import { fileURLToPath } from "url";
8
- import { marked } from "marked";
9
- import { markedHighlight } from "marked-highlight";
10
- import hljs from 'highlight.js';
11
-
12
- // Directories
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
-
16
- // Determine the project root directory
17
- const isInstalled = process.cwd() !== __dirname;
18
- const baseDir = isInstalled ? process.cwd() : __dirname;
19
-
20
- const dirs = {
21
- pages: path.join(baseDir, 'pages'),
22
- images: path.join(baseDir, 'images'),
23
- dist: path.join(baseDir, 'dist'),
24
- layouts: path.join(baseDir, 'layouts'),
25
- css: path.join(baseDir, 'css'),
26
- js: path.join(baseDir, 'js'),
27
- partials: path.join(baseDir, 'partials'),
28
- };
29
-
30
- marked.use(
31
- markedHighlight({
32
- langPrefix: 'hljs language-',
33
- highlight: (code, lang) => {
34
- if (lang && hljs.getLanguage(lang)) {
35
- return hljs.highlight(code, { language: lang }).value;
36
- } else {
37
- return hljs.highlightAuto(code).value; // Auto-detect the language
38
- }
39
- },
40
- })
41
- );
42
-
43
- const tagsMap = new Map();
44
- const addToTagMap = (tag, page) => {
45
- if (!tagsMap.has(tag)) tagsMap.set(tag, []);
46
- tagsMap.get(tag).push({ title: page.title, url: page.url });
47
- };
48
- const pageIndex = [];
49
- const partialCache = new Map();
50
-
51
- const loadPartial = async (partialName) => {
52
- if (partialCache.has(partialName)) {
53
- return partialCache.get(partialName);
54
- }
55
-
56
- const partialPath = path.join(dirs.partials, `${partialName}.md`);
57
- if (await fsExtra.pathExists(partialPath)) {
58
- const partialContent = await fs.readFile(partialPath, "utf-8");
59
- partialCache.set(partialName, partialContent); // Store in cache
60
- return partialContent;
61
- } else {
62
- console.warn(`Include "${partialName}" not found.`);
63
- return `<p>Include "${partialName}" not found.</p>`;
64
- }
65
- };
66
-
67
- // Valid file extensions for assets
68
- const validExtensions = {
69
- css: ['.css'],
70
- js: ['.js'],
71
- images: ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'],
72
- };
73
-
74
- // Ensure and copy valid assets
75
- const ensureAndCopy = async (source, destination, validExts) => {
76
- if (await fsExtra.pathExists(source)) {
77
- await fsExtra.ensureDir(destination);
78
-
79
- const files = await fs.readdir(source);
80
- await Promise.all(
81
- files
82
- .filter(file => validExts.includes(path.extname(file).toLowerCase()))
83
- .map(file => fsExtra.copy(path.join(source, file), path.join(destination, file)))
84
- );
85
- console.log(`Copied valid files from ${source} to ${destination}`);
86
- } else {
87
- console.log(`No ${path.basename(source)} found in ${source}`);
88
- }
89
- };
90
-
91
- // Helper function to capitalize words
92
- const capitalize = (str) => str.replace(/\b\w/g, (char) => char.toUpperCase());
93
-
94
- // Copy assets with file type validation
95
- const copyAssets = async () => {
96
- await ensureAndCopy(dirs.css, path.join(dirs.dist, 'css'), validExtensions.css);
97
- await ensureAndCopy(dirs.js, path.join(dirs.dist, 'js'), validExtensions.js);
98
- await ensureAndCopy(dirs.images, path.join(dirs.dist, 'images'), validExtensions.images);
99
- };
100
-
101
- async function optimizeImages() {
102
- try {
103
- const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png'];
104
- const images_folder = path.join(dirs.dist, "images");
105
- const files = await fs.readdir(images_folder);
106
-
107
- await Promise.all(files.map(async (file) => {
108
- const filePath = path.join(images_folder, file);
109
- const ext = path.extname(file).toLowerCase();
110
-
111
- if (!IMAGE_EXTENSIONS.includes(ext)) return;
112
-
113
- const optimizedPath = path.join(images_folder, `${path.basename(file, ext)}.webp`);
114
-
115
- if (filePath !== optimizedPath) {
116
- const image = sharp(filePath);
117
- const metadata = await image.metadata();
118
- const originalWidth = metadata.width || 0;
119
- const maxWidth = defaultConfig.max_image_size || 800;
120
- const resizeWidth = Math.min(originalWidth, maxWidth);
121
-
122
- await image
123
- .resize({ width: resizeWidth })
124
- .toFormat('webp', { quality: 80 })
125
- .toFile(optimizedPath);
126
-
127
- await fs.unlink(filePath);
128
-
129
- console.log(`Optimized ${file} -> ${optimizedPath}`);
130
- }
131
- }));
132
- } catch (error) {
133
- console.error('Error optimizing images:', error);
134
- }
135
- };
136
-
137
- // Utility: Generate HTML imports for assets
138
- const generateAssetImports = async (dir, tagTemplate, validExts) => {
139
- if (!(await fsExtra.pathExists(dir))) return '';
140
- const files = await fs.readdir(dir);
141
- return files
142
- .filter(file => validExts.includes(path.extname(file).toLowerCase()))
143
- .sort()
144
- .map(file => tagTemplate(file))
145
- .join('\n');
146
- };
147
-
148
- // Generate CSS and JS imports
149
- const getCssImports = () => generateAssetImports(dirs.css, (file) => `<link rel="stylesheet" href="/css/${file}" />`,validExtensions.css);
150
- const getJsImports = () => generateAssetImports(dirs.js, (file) => `<script src="/js/${file}"></script>`,validExtensions.js);
151
-
152
- const loadConfig = async (dir) => {
153
- const configFiles = ['config.yaml', 'config.yml', 'config.json'];
154
- for (const file of configFiles) {
155
- const filePath = path.join(dir, file);
156
- try {
157
- await fs.access(filePath); // Check if file exists
158
- const content = await fs.readFile(filePath, 'utf-8');
159
- return file.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
160
- } catch (err) {
161
- // File not found, continue to next option
162
- }
163
- }
164
- return {}; // Return an empty object if no config file is found
165
- };
166
-
167
- // Default configuration
168
- const defaultConfig = await loadConfig(baseDir);
169
-
170
- // Utility: Cache and load layouts
171
- const layoutCache = new Map();
172
- const getLayout = async (layoutName) => {
173
- if (!layoutName) return null;
174
- if (!layoutCache.has(layoutName)) {
175
- const layoutPath = path.join(dirs.layouts, `${layoutName}.html`);
176
- if (await fsExtra.pathExists(layoutPath)) {
177
- const layoutContent = await fs.readFile(layoutPath, 'utf-8');
178
- layoutCache.set(layoutName, layoutContent);
179
- } else {
180
- return null;
181
- }
182
- }
183
- return layoutCache.get(layoutName);
184
- };
185
-
186
- // Apply layout content to a page
187
- const applyLayout = async (layoutContent, config) => {
188
- if (!layoutContent) return ['', ''];
189
- const [before, after] = layoutContent.split(/{{\s*content\s*}}/);
190
- return [
191
- await replacePlaceholders(before || '', config),
192
- await replacePlaceholders(after || '', config),
193
- ];
194
- };
195
-
196
- // Utility: Apply layout and wrap content in a Turbo Frame
197
- const applyLayoutAndWrapContent = async (page,content) => {
198
- const layoutContent = await getLayout(page.data.layout !== undefined ? page.data.layout || page.layout);
199
- const [beforeLayout, afterLayout] = await applyLayout(layoutContent, page);
200
- return `
201
- <turbo-frame id="content">
202
- <head><title>${page.title} || ${page.data.sitename}</title></head>
203
- ${beforeLayout}
204
- ${content}
205
- ${afterLayout}
206
- </turbo-frame>
207
- `;
208
- };
209
-
210
- const isValid = async (filePath) => {
211
- try {
212
- const stats = await fs.stat(filePath);
213
- return stats.isDirectory() || path.extname(filePath) === '.md';
214
- } catch (err) {
215
- return false; // Handle errors like file not found
216
- }
217
- };
218
-
219
- const generatePages = async (sourceDir, baseDir = sourceDir, parent) => {
220
- const pages = [];
221
- const folderConfig = await loadConfig(sourceDir);
222
- const config = {...defaultConfig,...parent?.data,...folderConfig};
223
- try {
224
- const files = await fs.readdir(sourceDir, { withFileTypes: true });
225
- for (const file of files) {
226
- const filePath = path.join(sourceDir, file.name);
227
- const valid = await isValid(filePath);
228
- if(!valid) continue;
229
- const root = file.name === "index.md" && !parent;
230
- const relativePath = path.relative(baseDir, filePath).replace(/\\/g, "/"); // Normalize slashes
231
- const finalPath = `/${relativePath.replace(/\.md$/, "")}`;
232
- const name = root ? "Home" : capitalize(file.name.replace(/\.md$/, "").replace(/-/g, " "));
233
- const stats = await fs.stat(filePath);
234
- const isDirectory = file.isDirectory();
235
- const layoutFileExists = parent && await fsExtra.pathExists(dirs.layouts + "/" + parent.filename + ".html");
236
- const layout = layoutFileExists ? parent.filename : parent ? parent.layout : "pages";
237
-
238
- const page = {
239
- name, root, layout,
240
- filename: file.name.replace(/\.md$/, ""),
241
- path: finalPath,
242
- filepath: filePath,
243
- url: root ? "/" : finalPath + ".html",
244
- nav: !parent && !root,
245
- parent: parent ? {title: parent.data.title, url: parent.url} : undefined,
246
- folder: isDirectory,
247
- title: name,
248
- created_at: new Date(stats.birthtime).toLocaleDateString(undefined,config.dateFormat),
249
- updated_at: new Date(stats.mtime).toLocaleDateString(undefined,config.dateFormat),
250
- date: new Date(stats.mtime).toLocaleDateString(undefined,config.dateFormat),
251
- data: root ? {...defaultConfig} : {...config}
252
- };
253
- if (path.extname(file.name) === ".md") {
254
- const markdownContent = await fs.readFile(filePath, "utf-8");
255
- const { data, content } = matter(markdownContent);
256
- const index = pages.findIndex(p => p.url === page.url);
257
- if (index !== -1) {
258
- Object.assign(pages[index], { data: { ...page.data, ...data }, content });
259
- continue;
260
- }
261
- else Object.assign(page, { data: { ...page.data, ...data }, content });
262
- }
263
- if (isDirectory) {
264
- page.pages = await generatePages(filePath, baseDir, page);
265
- page.pages.sort((a, b) => {
266
- if (a.data.position && b.data.position) {
267
- return a.data.position - b.data.position; // Sort by position first
268
- }
269
- return new Date(a.updated_at) - new Date(b.updated_at); // If position is the same, sort by date
270
- });
271
- const index = pages.findIndex(p => p.url === page.url);
272
- if (index !== -1) {
273
- page.content = pages[index].content;
274
- pages.splice(index, 1);
275
- }
276
- else page.content = await generateLinkList(page.filename,page.pages);
277
- }
278
-
279
- // add tags
280
- if (page.data.tags) page.data.tags.forEach(tag => addToTagMap(tag, page));
281
-
282
- pages.push(page);
283
- pageIndex.push({url: page.url, title: page.title || page.title, nav: page.nav})
284
- }
285
-
286
- } catch (err) {
287
- console.error("Error reading directory:", err);
288
- }
289
-
290
- // make Tags page
291
- if(!parent && tagsMap.size){
292
- const tagLayout = await fsExtra.pathExists(dirs.layouts + "/tags.html");
293
- const tagPage = {
294
- path: "/tags",
295
- url: "/tags.html",
296
- nav: false,
297
- folder: true,
298
- name: "Tags",
299
- title: "All Tags",
300
- layout: "pages",
301
- updated_at: new Date().toLocaleDateString(undefined,defaultConfig.dateFormat),
302
- data: {...config},
303
- }
304
- tagPage.pages = [];
305
- for (const [tag, pages] of tagsMap) {
306
- const page = {
307
- name: tag,
308
- title: tag,
309
- updated_at: new Date().toLocaleDateString(undefined,defaultConfig.dateFormat),
310
- path: `/tags/${tag}`,
311
- url: `/tags/${tag}.html`,
312
- layout: tagLayout ? "tags" : "pages",
313
- data: {...config, title: `Pages tagged with ${capitalize(tag)}`},
314
- };
315
- page.content = pages
316
- .map(page =>`* <a href="${page.url}" data-turbo-frame="content" data-turbo-action="advance">${page.title}</a>`)
317
- .join('\n');
318
- tagPage.pages.push(page);
319
- }
320
- tagPage.content = await generateLinkList("tags",tagPage.pages);
321
- pages.push(tagPage);
322
- }
323
- return pages;
324
- };
325
-
326
- const generateLinkList = async (name,pages) => {
327
- const partial = `${name}.md`;
328
- const partialPath = path.join(dirs.partials, partial);
329
- const linksPath = path.join(dirs.partials, "links.md");
330
- // Check if either file exists in the 'partials' folder
331
- const fileExists = await fsExtra.pathExists(partialPath);
332
- const defaultExists = await fsExtra.pathExists(linksPath);
333
- if (fileExists || defaultExists) {
334
- const partial = await fs.readFile(fileExists ? partialPath : linksPath, "utf-8");
335
- const content = await Promise.all(pages.map(page => replacePlaceholders(partial, page)));
336
- return content.join('\n');
337
- } else {
338
- return `${pages.map(page => `<li><a href="${page.url}" class="${defaultConfig.link_class}" data-turbo-frame="content">${page.title}</a></li>`).join`\n`}`
339
- }
340
- };
341
-
342
- const render = async page => {
343
- const replacedContent = await replacePlaceholders(page.content, page);
344
- const htmlContent = marked.parse(replacedContent); // Markdown processed once
345
-
346
- const wrappedContent = await applyLayoutAndWrapContent(page, htmlContent);
347
- const turboHTML = wrappedContent.replace(
348
- /<a\s+([^>]*?)href="(\/[^"#?]+?)"(.*?)>/g,
349
- (match, beforeHref, href, afterHref) => {
350
- // Don't double-add .html
351
- const fullHref = href.endsWith('.html') ? href : `${href}.html`;
352
-
353
- return `<a ${beforeHref}href="${fullHref}" data-turbo-frame="content" data-turbo-action="advance"${afterHref}>`;
354
- }
355
- );
356
- return turboHTML;
357
- };
358
-
359
- // Function to read and render the index template
360
- const renderIndexTemplate = async (content, config) => {
361
- // Read the template from pages folder
362
- const templatePath = path.join(baseDir, 'template.html');
363
- let templateContent = await fs.readFile(templatePath, 'utf-8');
364
-
365
- // Add the meta tag for Turbo refresh method
366
- const turboMetaTag = `<meta name="turbo-refresh-method" content="morph">`;
367
- const css = await getCssImports();
368
- const js = await getJsImports();
369
- const imports = css + js;
370
-
371
- const highlightCSS = `<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/monokai-sublime.min.css">`;
372
- templateContent = templateContent.replace('</head>', `${turboMetaTag}\n${imports}\n${highlightCSS}\n</head>`);
373
- templateContent = await replacePlaceholders(templateContent,{...defaultConfig,...config,content})
374
-
375
- // Add the missing script to the template
376
- const turboScript = `
377
- <script type="module">
378
- import * as Turbo from 'https://esm.sh/@hotwired/turbo';
379
- const turboFrame = document.querySelector("turbo-frame#content");
380
- function loadFrameContent() {
381
- const path = window.location.pathname;
382
- if(path !== "/") {
383
- turboFrame.style.visibility = "hidden";
384
- const pagePath = path.endsWith(".html") ? path : path + ".html";
385
- if (turboFrame) Turbo.visit(pagePath, { frame: "content" });
386
- }
387
- }
388
- loadFrameContent();
389
- window.addEventListener("popstate", loadFrameContent);
390
- document.addEventListener("turbo:frame-load", event => {
391
- turboFrame.style.visibility = "visible";
392
- window.scrollTo(0,0);
393
- const frameSrc = turboFrame.getAttribute("src");
394
- if (frameSrc && frameSrc.endsWith(".html")) {
395
- const newPath = frameSrc.replace(".html", "");
396
- if (window.location.pathname !== newPath) {
397
- window.history.pushState({}, "", newPath);
398
- }
399
- }
400
- });
401
- </script>
402
- `;
403
- // Inject the script at the end of the template
404
- templateContent = templateContent.replace('</body>', `${turboScript}</body>`);
405
- return templateContent;
406
- };
407
-
408
- const createPages = async (pages, distDir=dirs.dist) => {
409
- for (const page of pages) {
410
- let html = await render(page);
411
- if(page.root) html = await renderIndexTemplate(html,page.data);
412
- const pagePath = path.join(distDir, page.root ? "/index.html" : page.url);
413
- // If it's a folder, create the directory and recurse into its pages
414
- if (page.folder) {
415
- if (!(await fsExtra.pathExists(path.join(distDir, page.path)))) {
416
- await fs.mkdir(path.join(distDir, page.path), { recursive: true });
417
- }
418
- // Recurse into pages inside the directory
419
- await createPages(page.pages); // Process nested pages inside the folder
420
- }
421
- // create an HTML file
422
- try {
423
- await fs.writeFile(pagePath, html);
424
- console.log(`Created file: ${pagePath}`);
425
- } catch (err) {
426
- console.error(`Error writing file ${pagePath}:`, err);
427
- }
428
- }
429
- };
430
-
431
- const replacePlaceholders = async (template, values) => {
432
- const partialRegex = /{{\s*partial:\s*([\w-]+)\s*}}/g;
433
-
434
- // Async replace function
435
- const replaceAsync = async (str, regex, asyncFn) => {
436
- const matches = [];
437
- str.replace(regex, (match, ...args) => {
438
- matches.push(asyncFn(match, ...args));
439
- return match;
440
- });
441
-
442
- const results = await Promise.all(matches);
443
- return str.replace(regex, () => results.shift());
444
- };
445
-
446
- // Replace partial includes
447
- template = await replaceAsync(template, partialRegex, async (match, partialName) => {
448
- let partialContent = await loadPartial(partialName);
449
- partialContent = await replacePlaceholders(partialContent, values); // Recursive replacement
450
- return marked(partialContent); // Convert Markdown to HTML
451
- });
452
- // replace image extensions with optimized extension
453
- template = template.replace(/\.(png|jpe?g|webp)/gi, ".webp");
454
- // Replace other placeholders **only outside of code blocks**
455
- const codeBlockRegex = /```[\s\S]*?```|`[^`]+`|<(pre|code)[^>]*>[\s\S]*?<\/\1>/g;
456
- const codeBlocks = [];
457
- template = template.replace(codeBlockRegex, match => {
458
- codeBlocks.push(match);
459
- return `{{CODE_BLOCK_${codeBlocks.length - 1}}}`; // Temporary placeholder
460
- });
461
- // Replace placeholders outside of code blocks
462
- template = template.replace(/{{\s*([^}\s]+)\s*}}/g, (match, key) => {
463
- return(values.data && key in values?.data ? values.data[key] : key in values ? values[key] : match)
464
- });
465
- // Restore code blocks
466
- template = template.replace(/{{CODE_BLOCK_(\d+)}}/g, (_, index) => codeBlocks[index]);
467
-
468
- return template;
469
- };
470
-
471
- const addLinks = async (pages,parent) => {
472
- pages.forEach(async page => {
473
- page.data ||= {};
474
- page.data.links_to_tags = page?.data?.tags?.length
475
- ? page.data.tags.map(tag => `<a class="${defaultConfig.tag_class}" href="/tags/${tag}.html" data-turbo-frame="content" data-turbo-action="advance">${tag}</a>`).join`` : "";
476
- const crumb = page.root ? "" : ` ${defaultConfig.breadcrumb_separator} <a class="${defaultConfig.breadcrumb_class}" href="${page.url}" data-turbo-frame="content" data-turbo-action="advance">${page.name}</a>`;
477
- page.data.breadcrumbs = parent ? parent.data.breadcrumbs + crumb
478
- : `<a class="${defaultConfig.breadcrumb_class}" href="/" data-turbo-frame="content" data-turbo-action="advance">Home</a>` + crumb;
479
- page.data.links_to_children = page.pages ? await generateLinkList(page.filename,page.pages) : "";
480
- page.data.links_to_siblings = await generateLinkList(page.parent?.filename || "pages",pages.filter(p => p.url !== page.url));
481
- page.data.links_to_self_and_siblings = await generateLinkList(page.parent?.filename || "pages",pages);
482
- page.data.nav_links = await generateLinkList("nav",pageIndex.filter(p => p.nav));
483
- if(page.pages) {
484
- await addLinks(page.pages,page)
485
- }
486
- });
487
- }
488
-
489
- // Main function to handle conversion and site generation
490
- const generateSite = async () => {
491
- console.log('Starting build process...');
492
- // Copy images, CSS, and JS files
493
- await copyAssets();
494
- await optimizeImages();
495
- // Convert markdown in pages directory
496
- const pages = await generatePages(dirs.pages);
497
- await addLinks(pages);
498
- await createPages(pages);
499
- };
500
-
501
- // Run the site generation process
502
- generateSite()
503
- .then(() => console.log('🚀 Site generated successfully! 🥳'))
504
- .catch(err => console.error('🛑 Error generating site:', err));
File without changes