fossbook 0.0.2
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/README.md +246 -0
- package/bin/fossbook.js +119 -0
- package/lib/all_posts.js +32 -0
- package/lib/assets/placeholder.svg +4 -0
- package/lib/deploy.js +202 -0
- package/lib/home.js +59 -0
- package/lib/index.js +79 -0
- package/lib/init.js +253 -0
- package/lib/mod/config.js +98 -0
- package/lib/mod/marked.js +95 -0
- package/lib/mod/page.js +115 -0
- package/lib/mod/page_base.js +97 -0
- package/lib/new.js +48 -0
- package/lib/posts.js +42 -0
- package/lib/server.js +14 -0
- package/lib/tag/index.js +69 -0
- package/lib/tag/tag.js +29 -0
- package/package.json +45 -0
- package/themes/archie/assets/fonts/ComicNeue-Bold.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-BoldItalic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Italic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Light.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-LightItalic.ttf +0 -0
- package/themes/archie/assets/fonts/ComicNeue-Regular.ttf +0 -0
- package/themes/archie/assets/fonts/LyonDisplay-Bold.otf +0 -0
- package/themes/archie/assets/fonts/OFL.txt +93 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.eot +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.svg +330 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.ttf +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.woff +0 -0
- package/themes/archie/assets/fonts/fira-sans-v10-latin-regular.woff2 +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.eot +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.svg +365 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.ttf +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.woff +0 -0
- package/themes/archie/assets/fonts/ibm-plex-mono-v6-latin-500italic.woff2 +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.eot +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.svg +405 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.ttf +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.woff +0 -0
- package/themes/archie/assets/fonts/roboto-mono-v12-latin-regular.woff2 +0 -0
- package/themes/archie/assets/images/node-ssg-1.png +0 -0
- package/themes/archie/assets/images/node-ssg-2.png +0 -0
- package/themes/archie/assets/styles/fonts.css +51 -0
- package/themes/archie/assets/styles/highlights.css +75 -0
- package/themes/archie/assets/styles/main.css +402 -0
- package/themes/archie/layouts/all_posts.html +42 -0
- package/themes/archie/layouts/home.html +62 -0
- package/themes/archie/layouts/page.html +50 -0
- package/themes/archie/layouts/partials/footer.html +10 -0
- package/themes/archie/layouts/post.html +82 -0
- package/themes/archie/layouts/tag.html +43 -0
- package/themes/archie/layouts/tag_list.html +44 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const Page = require("./mod/page");
|
|
5
|
+
const HomePagination = require("./home");
|
|
6
|
+
const AllPostsPage = require("./all_posts");
|
|
7
|
+
const Posts = require("./posts");
|
|
8
|
+
const TagPages = require("./tag");
|
|
9
|
+
|
|
10
|
+
function copyDirectoryRecursive(src, dest) {
|
|
11
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
12
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
13
|
+
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const srcPath = path.join(src, entry.name);
|
|
16
|
+
const destPath = path.join(dest, entry.name);
|
|
17
|
+
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
copyDirectoryRecursive(srcPath, destPath);
|
|
20
|
+
} else {
|
|
21
|
+
fs.copyFileSync(srcPath, destPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function build(config) {
|
|
27
|
+
// Remove the output directory
|
|
28
|
+
if (fs.existsSync(config.dev.outdir))
|
|
29
|
+
fs.rmSync(config.dev.outdir, { recursive: true });
|
|
30
|
+
fs.mkdirSync(config.dev.outdir);
|
|
31
|
+
|
|
32
|
+
// Create post pages in output directory
|
|
33
|
+
const posts = new Posts(config);
|
|
34
|
+
const postObjects = posts.createPostObjects();
|
|
35
|
+
|
|
36
|
+
postObjects.forEach((post) => {
|
|
37
|
+
post.generateContent("post.html");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Create home page and pagination in output/page directory
|
|
41
|
+
const homePagination = new HomePagination(config);
|
|
42
|
+
homePagination.generateContent(postObjects);
|
|
43
|
+
|
|
44
|
+
// Create all posts page in output/all_posts directory
|
|
45
|
+
const allPostsPage = new AllPostsPage(config);
|
|
46
|
+
allPostsPage.generateContent(postObjects);
|
|
47
|
+
|
|
48
|
+
// Create tag pages in output/tags directory
|
|
49
|
+
const tagPages = new TagPages(config);
|
|
50
|
+
tagPages.generateContent(postObjects);
|
|
51
|
+
|
|
52
|
+
// Create about page in output/about directory
|
|
53
|
+
const aboutPath = config.dev.about;
|
|
54
|
+
if (fs.existsSync(aboutPath)) {
|
|
55
|
+
const aboutPage = new Page(config);
|
|
56
|
+
aboutPage.readSource(aboutPath);
|
|
57
|
+
aboutPage.generateContent("page.html", "about");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Copy static directory to output directory
|
|
61
|
+
const staticImagesDir = path.join(config.dev.staticDir, "images");
|
|
62
|
+
if (fs.existsSync(staticImagesDir)) {
|
|
63
|
+
copyDirectoryRecursive(staticImagesDir, path.join(config.dev.outdir, "images"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Copy theme assets to output directory
|
|
67
|
+
const themeAssetsDir = path.join(config.themePath, "assets");
|
|
68
|
+
if (fs.existsSync(themeAssetsDir)) {
|
|
69
|
+
copyDirectoryRecursive(themeAssetsDir, config.dev.outdir);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create CNAME file for GitHub Pages
|
|
73
|
+
if (config.githubCNAME)
|
|
74
|
+
fs.writeFileSync(path.join(config.dev.outdir, "CNAME"), config.githubCNAME);
|
|
75
|
+
|
|
76
|
+
console.log("Build completed successfully");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { build };
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const DEPLOY_WORKFLOW = `name: Deploy to GitHub Pages
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
pages: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: "pages"
|
|
18
|
+
cancel-in-progress: false
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
build-and-deploy:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
environment:
|
|
24
|
+
name: github-pages
|
|
25
|
+
url: \${{ steps.deployment.outputs.page_url }}
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Setup Node.js
|
|
30
|
+
uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: '20'
|
|
33
|
+
cache: 'npm'
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: npm ci
|
|
37
|
+
|
|
38
|
+
- name: Build site
|
|
39
|
+
run: npx fossbook build
|
|
40
|
+
|
|
41
|
+
- name: Setup Pages
|
|
42
|
+
uses: actions/configure-pages@v4
|
|
43
|
+
|
|
44
|
+
- name: Upload artifact
|
|
45
|
+
uses: actions/upload-pages-artifact@v3
|
|
46
|
+
with:
|
|
47
|
+
path: './public'
|
|
48
|
+
|
|
49
|
+
- name: Deploy to GitHub Pages
|
|
50
|
+
id: deployment
|
|
51
|
+
uses: actions/deploy-pages@v4
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
function initProject(options = {}) {
|
|
55
|
+
const cwd = process.cwd();
|
|
56
|
+
|
|
57
|
+
// Create directories
|
|
58
|
+
const dirs = [
|
|
59
|
+
"content/posts",
|
|
60
|
+
"static/images",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
dirs.forEach((dir) => {
|
|
64
|
+
const dirPath = path.join(cwd, dir);
|
|
65
|
+
if (!fs.existsSync(dirPath)) {
|
|
66
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
67
|
+
console.log(`Created: ${dir}/`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Create .github/workflows/deploy.yml
|
|
72
|
+
const workflowDir = path.join(cwd, ".github", "workflows");
|
|
73
|
+
const workflowPath = path.join(workflowDir, "deploy.yml");
|
|
74
|
+
if (!fs.existsSync(workflowPath)) {
|
|
75
|
+
fs.mkdirSync(workflowDir, { recursive: true });
|
|
76
|
+
fs.writeFileSync(workflowPath, DEPLOY_WORKFLOW);
|
|
77
|
+
console.log("Created: .github/workflows/deploy.yml");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Create .gitignore
|
|
81
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
82
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
83
|
+
fs.writeFileSync(gitignorePath, "node_modules/\npublic/\n");
|
|
84
|
+
console.log("Created: .gitignore");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create fossbook.config.js
|
|
88
|
+
const configPath = path.join(cwd, "fossbook.config.js");
|
|
89
|
+
if (!fs.existsSync(configPath)) {
|
|
90
|
+
const configContent = `module.exports = {
|
|
91
|
+
blogName: "My Blog",
|
|
92
|
+
authorName: "",
|
|
93
|
+
authorDescription: "",
|
|
94
|
+
authorWebsite: "",
|
|
95
|
+
blogDescription: "A blog powered by fossbook",
|
|
96
|
+
blogsite: "http://localhost:3000",
|
|
97
|
+
|
|
98
|
+
// Optional
|
|
99
|
+
githubCNAME: "",
|
|
100
|
+
googleAnalyticsID: "",
|
|
101
|
+
authorTwitter: "",
|
|
102
|
+
siteTwitter: "",
|
|
103
|
+
githubRepository: "",
|
|
104
|
+
image: "",
|
|
105
|
+
theme: "archie",
|
|
106
|
+
|
|
107
|
+
// Comment system (optional)
|
|
108
|
+
// comments: { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" },
|
|
109
|
+
|
|
110
|
+
// Deployment
|
|
111
|
+
// deploy: { branch: "main", remote: "origin" },
|
|
112
|
+
|
|
113
|
+
// Directory overrides (defaults shown)
|
|
114
|
+
content: "./content",
|
|
115
|
+
postsDir: "./content/posts",
|
|
116
|
+
outputDir: "./public",
|
|
117
|
+
staticDir: "./static",
|
|
118
|
+
themesDir: "./themes",
|
|
119
|
+
};
|
|
120
|
+
`;
|
|
121
|
+
fs.writeFileSync(configPath, configContent);
|
|
122
|
+
console.log("Created: fossbook.config.js");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Create content/about.md
|
|
126
|
+
const aboutPath = path.join(cwd, "content", "about.md");
|
|
127
|
+
if (!fs.existsSync(aboutPath)) {
|
|
128
|
+
const aboutContent = `---
|
|
129
|
+
title: About
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
Welcome to my blog!
|
|
133
|
+
`;
|
|
134
|
+
fs.writeFileSync(aboutPath, aboutContent);
|
|
135
|
+
console.log("Created: content/about.md");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create package.json if it doesn't exist
|
|
139
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
140
|
+
const createdPkg = !fs.existsSync(pkgPath);
|
|
141
|
+
if (createdPkg) {
|
|
142
|
+
const pkg = {
|
|
143
|
+
name: path.basename(cwd),
|
|
144
|
+
version: "1.0.0",
|
|
145
|
+
private: true,
|
|
146
|
+
scripts: {
|
|
147
|
+
build: "fossbook build",
|
|
148
|
+
start: "fossbook serve",
|
|
149
|
+
deploy: "fossbook deploy",
|
|
150
|
+
},
|
|
151
|
+
dependencies: {
|
|
152
|
+
fossbook: "latest",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
156
|
+
console.log("Created: package.json");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Generate package-lock.json (required by CI/CD npm ci + cache)
|
|
160
|
+
const lockPath = path.join(cwd, "package-lock.json");
|
|
161
|
+
if (!fs.existsSync(lockPath)) {
|
|
162
|
+
console.log("Running npm install to generate package-lock.json...");
|
|
163
|
+
try {
|
|
164
|
+
execSync("npm install", { cwd, stdio: "inherit" });
|
|
165
|
+
console.log("Created: package-lock.json");
|
|
166
|
+
} catch {
|
|
167
|
+
console.warn("Warning: npm install failed. You may need to run it manually.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle --github flag: create repo and push
|
|
172
|
+
if (options.github) {
|
|
173
|
+
initGitHub(cwd);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log("\nSite initialized! Next steps:");
|
|
177
|
+
if (!options.github) {
|
|
178
|
+
console.log(' 1. Edit fossbook.config.js with your site settings');
|
|
179
|
+
console.log(' 2. Run: fossbook new "My First Post"');
|
|
180
|
+
console.log(" 3. Edit your post in content/posts/");
|
|
181
|
+
console.log(" 4. Run: fossbook build");
|
|
182
|
+
console.log(" 5. Run: fossbook serve");
|
|
183
|
+
} else {
|
|
184
|
+
console.log(' 1. Edit fossbook.config.js with your site settings');
|
|
185
|
+
console.log(' 2. Run: fossbook new "My First Post"');
|
|
186
|
+
console.log(" 3. Edit your post in content/posts/");
|
|
187
|
+
console.log(" 4. Run: fossbook deploy");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function initGitHub(cwd) {
|
|
192
|
+
// Check if gh CLI is available
|
|
193
|
+
try {
|
|
194
|
+
execSync("gh --version", { stdio: "ignore" });
|
|
195
|
+
} catch {
|
|
196
|
+
console.error(
|
|
197
|
+
"Error: GitHub CLI (gh) is not installed.\n" +
|
|
198
|
+
"Install it: https://cli.github.com/\n" +
|
|
199
|
+
" Linux: sudo apt install gh\n" +
|
|
200
|
+
" macOS: brew install gh\n" +
|
|
201
|
+
" Windows: winget install GitHub.cli"
|
|
202
|
+
);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if gh is authenticated
|
|
207
|
+
try {
|
|
208
|
+
execSync("gh auth status", { stdio: "ignore" });
|
|
209
|
+
} catch {
|
|
210
|
+
console.error("Error: GitHub CLI is not authenticated. Run: gh auth login");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Initialize git repo if needed
|
|
215
|
+
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
|
216
|
+
console.log("Initializing git repository...");
|
|
217
|
+
execSync("git init", { cwd, stdio: "inherit" });
|
|
218
|
+
execSync("git branch -M main", { cwd, stdio: "inherit" });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create GitHub repository
|
|
222
|
+
const repoName = path.basename(cwd);
|
|
223
|
+
console.log(`Creating GitHub repository: ${repoName}...`);
|
|
224
|
+
try {
|
|
225
|
+
execSync(`gh repo create ${repoName} --public --source=. --remote=origin`, {
|
|
226
|
+
cwd,
|
|
227
|
+
stdio: "inherit",
|
|
228
|
+
});
|
|
229
|
+
} catch {
|
|
230
|
+
// Repo may already exist or remote may already be set
|
|
231
|
+
console.warn("Note: GitHub repo creation skipped (may already exist).");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Initial commit and push
|
|
235
|
+
console.log("Committing and pushing to GitHub...");
|
|
236
|
+
execSync("git add -A", { cwd, stdio: "inherit" });
|
|
237
|
+
try {
|
|
238
|
+
execSync('git commit -m "Initial fossbook site"', { cwd, stdio: "inherit" });
|
|
239
|
+
} catch {
|
|
240
|
+
// Nothing to commit
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
execSync("git push -u origin main", { cwd, stdio: "inherit" });
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.warn("Warning: Could not push to origin. You may need to push manually.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log("\nGitHub repository created and pushed!");
|
|
249
|
+
console.log("Enable GitHub Pages in your repo settings:");
|
|
250
|
+
console.log(" Settings → Pages → Source → GitHub Actions");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { initProject };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
const defaults = {
|
|
5
|
+
blogName: "My Blog",
|
|
6
|
+
authorName: "",
|
|
7
|
+
authorDescription: "",
|
|
8
|
+
authorWebsite: "",
|
|
9
|
+
blogDescription: "",
|
|
10
|
+
blogsite: "http://localhost:3000",
|
|
11
|
+
githubCNAME: "",
|
|
12
|
+
googleAnalyticsID: "",
|
|
13
|
+
authorTwitter: "",
|
|
14
|
+
siteTwitter: "",
|
|
15
|
+
githubRepository: "",
|
|
16
|
+
image: "",
|
|
17
|
+
theme: "archie",
|
|
18
|
+
comments: null, // { provider: "utterances", repo: "user/repo", issueTerm: "pathname", theme: "github-light" }
|
|
19
|
+
content: "./content",
|
|
20
|
+
postsDir: "./content/posts",
|
|
21
|
+
outputDir: "./public",
|
|
22
|
+
staticDir: "./static",
|
|
23
|
+
themesDir: "./themes",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the theme path, checking the user's themesDir first,
|
|
28
|
+
* then falling back to the built-in themes directory.
|
|
29
|
+
*/
|
|
30
|
+
function resolveThemePath(config) {
|
|
31
|
+
const themeName = config.theme || "archie";
|
|
32
|
+
const userThemePath = path.resolve(config.themesDir || "./themes", themeName);
|
|
33
|
+
const builtinThemePath = path.resolve(__dirname, "../../themes", themeName);
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(userThemePath)) {
|
|
36
|
+
return userThemePath;
|
|
37
|
+
}
|
|
38
|
+
if (fs.existsSync(builtinThemePath)) {
|
|
39
|
+
return builtinThemePath;
|
|
40
|
+
}
|
|
41
|
+
console.error(`Theme "${themeName}" not found in ${userThemePath} or ${builtinThemePath}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load configuration from the user's fossbook.config.js (or a custom path),
|
|
47
|
+
* merge with defaults, and build the internal dev paths.
|
|
48
|
+
*/
|
|
49
|
+
function loadConfig(configPath) {
|
|
50
|
+
const resolvedConfigPath = path.resolve(configPath || "./fossbook.config.js");
|
|
51
|
+
|
|
52
|
+
let userConfig = {};
|
|
53
|
+
if (fs.existsSync(resolvedConfigPath)) {
|
|
54
|
+
userConfig = require(resolvedConfigPath);
|
|
55
|
+
} else if (configPath) {
|
|
56
|
+
// Only error if user explicitly specified a config path
|
|
57
|
+
console.error(`Config file not found: ${resolvedConfigPath}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
} else {
|
|
60
|
+
console.warn("No fossbook.config.js found, using defaults.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const merged = { ...defaults, ...userConfig };
|
|
64
|
+
|
|
65
|
+
// Build the internal dev paths object used by the generator modules
|
|
66
|
+
const themePath = resolveThemePath(merged);
|
|
67
|
+
|
|
68
|
+
merged.dev = {
|
|
69
|
+
postsdir: path.resolve(merged.postsDir),
|
|
70
|
+
content: path.resolve(merged.content),
|
|
71
|
+
about: path.resolve(merged.content, "about.md"),
|
|
72
|
+
outdir: path.resolve(merged.outputDir),
|
|
73
|
+
themePath: path.dirname(themePath), // e.g. /path/to/themes
|
|
74
|
+
staticDir: path.resolve(merged.staticDir),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
merged.themePath = themePath; // full path to the active theme directory
|
|
78
|
+
|
|
79
|
+
// Inject version and build date
|
|
80
|
+
merged.version = require("../../package.json").version;
|
|
81
|
+
merged.date_time = formatDate(new Date());
|
|
82
|
+
|
|
83
|
+
return merged;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatDate(
|
|
87
|
+
date,
|
|
88
|
+
locale = "en-US",
|
|
89
|
+
options = {
|
|
90
|
+
year: "numeric",
|
|
91
|
+
month: "short",
|
|
92
|
+
day: "numeric",
|
|
93
|
+
},
|
|
94
|
+
) {
|
|
95
|
+
return date.toLocaleDateString(locale, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { loadConfig, resolveThemePath, formatDate };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const marked = require("marked");
|
|
2
|
+
|
|
3
|
+
marked.setOptions({
|
|
4
|
+
renderer: new marked.Renderer(),
|
|
5
|
+
highlight: function (code, language) {
|
|
6
|
+
const hljs = require("highlight.js");
|
|
7
|
+
const validLanguage = hljs.getLanguage(language) ? language : "plaintext";
|
|
8
|
+
return hljs.highlight(validLanguage, code).value;
|
|
9
|
+
},
|
|
10
|
+
pedantic: false,
|
|
11
|
+
gfm: true,
|
|
12
|
+
breaks: false,
|
|
13
|
+
sanitize: false,
|
|
14
|
+
smartLists: true,
|
|
15
|
+
smartypants: false,
|
|
16
|
+
xhtml: false,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Override function
|
|
20
|
+
const renderer = {
|
|
21
|
+
image(href, title, text) {
|
|
22
|
+
let size = null;
|
|
23
|
+
// Check if the title contains a size specification
|
|
24
|
+
if (title && title.includes("size:")) {
|
|
25
|
+
const sizeMatch = title.match(/size:(\d+%)/);
|
|
26
|
+
if (sizeMatch && sizeMatch[1]) {
|
|
27
|
+
size = sizeMatch[1];
|
|
28
|
+
// Remove the size specification from the title
|
|
29
|
+
title = title.replace(/size:\d+%/g, "").trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
let align = "center";
|
|
33
|
+
if (title && title.includes("align:")) {
|
|
34
|
+
// align: left, right, center
|
|
35
|
+
const alignMatch = title.match(/align:(left|right|center)/);
|
|
36
|
+
if (alignMatch && alignMatch[1]) {
|
|
37
|
+
align = alignMatch[1];
|
|
38
|
+
// Remove the alignment specification from the title
|
|
39
|
+
title = title.replace(/align:(left|right|center)/g, "").trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Construct the image tag with optional size and title
|
|
44
|
+
let imageTag = `<img src="${href}" alt="${text}"`;
|
|
45
|
+
if (size) {
|
|
46
|
+
imageTag += ` style="width: ${size};"`;
|
|
47
|
+
}
|
|
48
|
+
imageTag += ">";
|
|
49
|
+
|
|
50
|
+
return `
|
|
51
|
+
<div style="text-align: ${align};">
|
|
52
|
+
<figure style="text-align: ${align};">
|
|
53
|
+
${imageTag}
|
|
54
|
+
${title ? `<figcaption>${title}</figcaption>` : ""}
|
|
55
|
+
</figure>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
},
|
|
59
|
+
link(href, title, text) {
|
|
60
|
+
let align = null;
|
|
61
|
+
if (title && title.includes("align:")) {
|
|
62
|
+
// align: left, right, center
|
|
63
|
+
const alignMatch = title.match(/align:(left|right|center)/);
|
|
64
|
+
if (alignMatch && alignMatch[1]) {
|
|
65
|
+
align = alignMatch[1];
|
|
66
|
+
// Remove the alignment specification from the title
|
|
67
|
+
title = title.replace(/align:(left|right|center)/g, "").trim();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// if align is not null, add div with text-align style
|
|
71
|
+
if (align) {
|
|
72
|
+
return `
|
|
73
|
+
<div style="text-align: ${align};">
|
|
74
|
+
<a href="${href} "${title ? `title="${title}"` : ""}>${text}</a>
|
|
75
|
+
</div>`;
|
|
76
|
+
} else {
|
|
77
|
+
return `<a href="${href} "${title ? `title="${title}"` : ""}>${text}</a>`;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
blockquote(quote) {
|
|
81
|
+
return `<div class="blockquote-container"><blockquote>${quote}</blockquote></div>`;
|
|
82
|
+
},
|
|
83
|
+
paragraph(text) {
|
|
84
|
+
// remove <p> surrounding the image
|
|
85
|
+
if (text.includes('<figure style="text-align: center;">')) {
|
|
86
|
+
return text;
|
|
87
|
+
} else {
|
|
88
|
+
return `<p>${text}</p>`;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
marked.use({ renderer });
|
|
94
|
+
|
|
95
|
+
module.exports = marked;
|
package/lib/mod/page.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fm = require("front-matter");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const marked = require("./marked");
|
|
5
|
+
const PageBase = require("./page_base");
|
|
6
|
+
|
|
7
|
+
module.exports = class Page extends PageBase {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super(config);
|
|
10
|
+
this.srcFilePath = "";
|
|
11
|
+
this.markdownFilePath = ""; // markdown file path
|
|
12
|
+
this.contentPath = config.dev.content;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
readSource(filePath) {
|
|
16
|
+
if (filePath.indexOf(".md") === -1)
|
|
17
|
+
this.srcFilePath = filePath + "/index.md";
|
|
18
|
+
else this.srcFilePath = filePath;
|
|
19
|
+
|
|
20
|
+
const mdContent = fs.readFileSync(this.srcFilePath, "utf8");
|
|
21
|
+
|
|
22
|
+
if (path.extname(filePath) === ".md") {
|
|
23
|
+
// If the file has a .md extension, extract the file name without the extension
|
|
24
|
+
this.path = path.basename(filePath, ".md");
|
|
25
|
+
} else {
|
|
26
|
+
// If there's no .md extension, just get the last part of the path
|
|
27
|
+
this.path = path.basename(filePath);
|
|
28
|
+
}
|
|
29
|
+
// parsed content by fields and body
|
|
30
|
+
const content = fm(mdContent);
|
|
31
|
+
|
|
32
|
+
this.title = `${content.attributes.title}`;
|
|
33
|
+
this.date = content.attributes.date;
|
|
34
|
+
this.url = `${this.config.blogsite}/${this.path}/`;
|
|
35
|
+
this.image = content.attributes.image;
|
|
36
|
+
this.description = content.attributes.description;
|
|
37
|
+
// default image if no imageURL is specified
|
|
38
|
+
this.imageURL = this.config.image;
|
|
39
|
+
|
|
40
|
+
if (content.attributes.tags && content.attributes.tags.length > 0) {
|
|
41
|
+
const tagArray = content.attributes.tags.split(",");
|
|
42
|
+
this.tags = tagArray
|
|
43
|
+
.map((tag) => tag.trim())
|
|
44
|
+
.sort((a, b) => a.localeCompare(b));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// generated HTML from markdown
|
|
48
|
+
this.body = marked.parse(content.body);
|
|
49
|
+
// remove <p></p> and <p> </p> from the beginning and end of the content.body
|
|
50
|
+
this.body = this.body.replace(/<p><\/p>/g, "").replace(/<p> <\/p>/g, "");
|
|
51
|
+
|
|
52
|
+
// for generating the navigation link in the post.html template
|
|
53
|
+
this.next = null;
|
|
54
|
+
this.previous = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// about/index.html or about/hello.html
|
|
58
|
+
generateContent(templateFile, outputPath) {
|
|
59
|
+
// For a series of posts
|
|
60
|
+
if (outputPath === undefined) outputPath = `${this.path}/index.html`;
|
|
61
|
+
// if file name is not included in the path
|
|
62
|
+
if (outputPath.indexOf(".htm") === -1) {
|
|
63
|
+
if (outputPath[outputPath.length - 1] !== "/") outputPath += "/";
|
|
64
|
+
outputPath += "index.html";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// if a directory path is included in the path
|
|
68
|
+
if (outputPath.indexOf("/") !== -1) {
|
|
69
|
+
if (this.path === "")
|
|
70
|
+
this.path = outputPath.split("/").slice(0, -1).join("/");
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(`${this.config.dev.outdir}/${this.path}`))
|
|
73
|
+
fs.rmSync(`${this.config.dev.outdir}/${this.path}`, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
fs.mkdirSync(`${this.config.dev.outdir}/${this.path}`);
|
|
78
|
+
} else {
|
|
79
|
+
// remove the outputPath file if it exists
|
|
80
|
+
if (fs.existsSync(`${this.config.dev.outdir}/${this.path}`))
|
|
81
|
+
fs.unlinkSync(`${this.config.dev.outdir}/${this.path}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const layoutsPath = path.join(this.config.themePath, "layouts", templateFile);
|
|
85
|
+
const postHTML = this.generateHTML(layoutsPath);
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
`${this.config.dev.outdir}/${outputPath}`,
|
|
89
|
+
postHTML,
|
|
90
|
+
(e) => {
|
|
91
|
+
if (e) throw e;
|
|
92
|
+
console.log(`${outputPath}/index.html was created successfully`);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// if there is the images folder in the output directory.
|
|
97
|
+
if (
|
|
98
|
+
fs.existsSync(`${this.config.dev.postsdir}/${this.path}/images`) &&
|
|
99
|
+
this.path !== ""
|
|
100
|
+
) {
|
|
101
|
+
// Copy images folder from postsdir to outdir
|
|
102
|
+
if (!fs.existsSync(`${this.config.dev.outdir}/${this.path}/images`))
|
|
103
|
+
fs.mkdirSync(`${this.config.dev.outdir}/${this.path}/images`);
|
|
104
|
+
|
|
105
|
+
fs.readdirSync(`${this.config.dev.postsdir}/${this.path}/images`).forEach(
|
|
106
|
+
(image) => {
|
|
107
|
+
fs.copyFileSync(
|
|
108
|
+
`${this.config.dev.postsdir}/${this.path}/images/${image}`,
|
|
109
|
+
`${this.config.dev.outdir}/${this.path}/images/${image}`,
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|