@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 +10 -9
- package/src/assets.js +80 -0
- package/src/build.js +13 -0
- package/src/cli.js +32 -0
- package/src/config.js +36 -0
- package/src/layout.js +43 -0
- package/src/pages.js +210 -0
- package/src/partials.js +63 -0
- package/src/watcher.js +39 -0
- package/cli.js +0 -22
- package/swifty.js +0 -504
- /package/{init.js → src/init.js} +0 -0
package/package.json
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/swifty",
|
|
3
|
-
"version": "
|
|
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
|
-
"
|
|
11
|
-
"cli.js",
|
|
12
|
-
"init.js"
|
|
10
|
+
"src/"
|
|
13
11
|
],
|
|
14
12
|
"scripts": {
|
|
15
13
|
"test": "mocha",
|
|
16
|
-
"build": "node
|
|
17
|
-
"start": "node
|
|
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
|
-
"
|
|
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 };
|
package/src/partials.js
ADDED
|
@@ -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));
|
/package/{init.js → src/init.js}
RENAMED
|
File without changes
|