@moonwave99/goffre 0.0.6 → 0.1.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/LICENSE +1 -1
- package/dist/index.d.ts +83 -0
- package/dist/index.js +395 -0
- package/package.json +46 -17
- package/index.js +0 -1
- package/lib/goffre.js +0 -321
- package/lib/helpers.js +0 -84
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(The MIT License)
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Diego Caponera <hello@diegocaponera.com>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
6
|
a copy of this software and associated documentation files (the
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { MarkedExtension, RendererObject } from 'marked';
|
|
2
|
+
|
|
3
|
+
type Page = {
|
|
4
|
+
slug: string;
|
|
5
|
+
link?: string;
|
|
6
|
+
template?: string;
|
|
7
|
+
layout?: string | null;
|
|
8
|
+
content?: string;
|
|
9
|
+
extname?: string;
|
|
10
|
+
};
|
|
11
|
+
declare function getSlug(slug: string, params: Record<string, unknown>): string;
|
|
12
|
+
type GetTemplateParams = {
|
|
13
|
+
page: Page;
|
|
14
|
+
templates: string[];
|
|
15
|
+
defaultTemplate?: string;
|
|
16
|
+
};
|
|
17
|
+
declare function getTemplate({ page, templates, defaultTemplate, }: GetTemplateParams): string;
|
|
18
|
+
type LoadParams = {
|
|
19
|
+
dataPath?: string;
|
|
20
|
+
};
|
|
21
|
+
declare function load({ dataPath }?: LoadParams): Promise<{
|
|
22
|
+
json: {};
|
|
23
|
+
pages: {
|
|
24
|
+
excerpt: string | undefined;
|
|
25
|
+
slug: string;
|
|
26
|
+
description: any;
|
|
27
|
+
content: string;
|
|
28
|
+
}[];
|
|
29
|
+
}>;
|
|
30
|
+
declare function loadJSON(cwd: string): Promise<{}>;
|
|
31
|
+
declare function loadMarkdown(cwd: string): Promise<{
|
|
32
|
+
excerpt: string | undefined;
|
|
33
|
+
slug: string;
|
|
34
|
+
description: any;
|
|
35
|
+
content: string;
|
|
36
|
+
}[]>;
|
|
37
|
+
type GetSorterParams = {
|
|
38
|
+
sortBy: string;
|
|
39
|
+
order: "asc" | "desc";
|
|
40
|
+
};
|
|
41
|
+
declare function getSorter<T extends Record<string, unknown>>({ sortBy, order, }: GetSorterParams): (a: T, b: T) => number;
|
|
42
|
+
type RenderParams = {
|
|
43
|
+
pages: Page[];
|
|
44
|
+
viewsPath?: string;
|
|
45
|
+
buildPath?: string;
|
|
46
|
+
blockSeparator?: string;
|
|
47
|
+
domain?: string;
|
|
48
|
+
uglyUrls?: boolean;
|
|
49
|
+
logLevel?: "silent" | "verbose" | "normal";
|
|
50
|
+
locals: Record<string, unknown>;
|
|
51
|
+
sitemap?: {
|
|
52
|
+
generate?: boolean;
|
|
53
|
+
template?: string;
|
|
54
|
+
};
|
|
55
|
+
env?: Record<string, unknown>;
|
|
56
|
+
handlebars?: {
|
|
57
|
+
extname?: string;
|
|
58
|
+
helpers?: Record<string, unknown>;
|
|
59
|
+
};
|
|
60
|
+
markdown?: {
|
|
61
|
+
middleware?: (MarkedExtension | (() => MarkedExtension))[];
|
|
62
|
+
renderer?: RendererObject;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
declare function render({ pages, viewsPath, buildPath, blockSeparator, domain, uglyUrls, logLevel, locals, markdown, handlebars, sitemap, env, }: RenderParams): Promise<unknown[]>;
|
|
66
|
+
type PaginateParams<T extends Page> = {
|
|
67
|
+
collection: T[];
|
|
68
|
+
size?: number;
|
|
69
|
+
sortBy?: keyof T;
|
|
70
|
+
order?: "asc" | "desc";
|
|
71
|
+
};
|
|
72
|
+
type PaginatedResult<T extends Page> = {
|
|
73
|
+
pagination: {
|
|
74
|
+
page: number;
|
|
75
|
+
prev: number | null;
|
|
76
|
+
next: number | null;
|
|
77
|
+
total: number;
|
|
78
|
+
};
|
|
79
|
+
items: T[];
|
|
80
|
+
};
|
|
81
|
+
declare function paginate<T extends Page>({ collection, size, sortBy, order, }: PaginateParams<T>): PaginatedResult<T>[];
|
|
82
|
+
|
|
83
|
+
export { type Page, getSlug, getSorter, getTemplate, load, loadJSON, loadMarkdown, paginate, render };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// lib/goffre.ts
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { globby } from "globby";
|
|
10
|
+
import { marked as marked2 } from "marked";
|
|
11
|
+
import matter from "gray-matter";
|
|
12
|
+
import { createRequire } from "module";
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
import express from "express";
|
|
15
|
+
import { engine } from "express-handlebars";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import slugify from "slugify";
|
|
18
|
+
|
|
19
|
+
// lib/helpers.ts
|
|
20
|
+
var helpers_exports = {};
|
|
21
|
+
__export(helpers_exports, {
|
|
22
|
+
getAsset: () => getAsset,
|
|
23
|
+
getLink: () => getLink,
|
|
24
|
+
getNavClass: () => getNavClass,
|
|
25
|
+
getParamLink: () => getParamLink,
|
|
26
|
+
getSitemapLink: () => getSitemapLink,
|
|
27
|
+
list: () => list,
|
|
28
|
+
markdown: () => markdown,
|
|
29
|
+
nextItem: () => nextItem,
|
|
30
|
+
prevItem: () => prevItem
|
|
31
|
+
});
|
|
32
|
+
import { marked } from "marked";
|
|
33
|
+
var markdown = (text) => marked(text);
|
|
34
|
+
var getParamLink = (url, options) => {
|
|
35
|
+
const output = getSlug(url, options.hash);
|
|
36
|
+
return getAsset(output, options);
|
|
37
|
+
};
|
|
38
|
+
var getAsset = (asset, context) => {
|
|
39
|
+
const { options, env } = context.data.root;
|
|
40
|
+
return env.mode === "prod" && options.domain ? `${options.domain}${asset.startsWith("/") ? "" : "/"}${asset}` : asset;
|
|
41
|
+
};
|
|
42
|
+
var getSitemapLink = (page, context) => {
|
|
43
|
+
const { options } = context.data.root;
|
|
44
|
+
return `${options.domain}${getLink(page, context)}`;
|
|
45
|
+
};
|
|
46
|
+
var getLink = (page, context) => {
|
|
47
|
+
const base = page.link || `${page.slug.startsWith("/") ? "" : "/"}${page.slug}`;
|
|
48
|
+
const { uglyUrls } = context.data.root.options;
|
|
49
|
+
if (uglyUrls) {
|
|
50
|
+
return getAsset(`${base === "/" ? "/index" : base}.html`, context);
|
|
51
|
+
}
|
|
52
|
+
return getAsset(base.replace(/^\/index/, "/"), context);
|
|
53
|
+
};
|
|
54
|
+
var getNavClass = ({ slug }, currentPage) => {
|
|
55
|
+
const cleanSlug = slug && slug[0] === "/" ? slug.slice(1) : slug;
|
|
56
|
+
return currentPage.slug.startsWith(cleanSlug) ? `${cleanSlug} current` : cleanSlug;
|
|
57
|
+
};
|
|
58
|
+
var list = (context, options) => {
|
|
59
|
+
const offset = parseInt(options.hash.offset, 10) || 0;
|
|
60
|
+
const limit = parseInt(options.hash.limit, 10) || 100;
|
|
61
|
+
const sortBy = options.hash.sortBy || "slug";
|
|
62
|
+
const order = options.hash.order || "asc";
|
|
63
|
+
let output = "";
|
|
64
|
+
let i;
|
|
65
|
+
const data = context.toSorted(getSorter({ sortBy, order }));
|
|
66
|
+
if (offset < 0) {
|
|
67
|
+
i = -offset < data.length ? data.length - -offset : 0;
|
|
68
|
+
} else {
|
|
69
|
+
i = offset < data.length ? offset : 0;
|
|
70
|
+
}
|
|
71
|
+
const j = limit + i < data.length ? limit + i : data.length;
|
|
72
|
+
for (; i < j; i++) {
|
|
73
|
+
output += options.fn(data[i]);
|
|
74
|
+
}
|
|
75
|
+
return output;
|
|
76
|
+
};
|
|
77
|
+
var nextItem = (context, options) => {
|
|
78
|
+
const { list: list2 } = options.hash;
|
|
79
|
+
const index = list2.findIndex((x) => x.slug === context.slug);
|
|
80
|
+
const next = list2[index + 1];
|
|
81
|
+
if (!next) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
return options.fn(next);
|
|
85
|
+
};
|
|
86
|
+
var prevItem = (context, options) => {
|
|
87
|
+
const { list: list2 } = options.hash;
|
|
88
|
+
const index = list2.findIndex((x) => x.slug === context.slug);
|
|
89
|
+
const prev = list2[index - 1];
|
|
90
|
+
if (!prev) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return options.fn(prev);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// lib/goffre.ts
|
|
97
|
+
var require2 = createRequire(import.meta.url);
|
|
98
|
+
var { readFile, outputFile } = fs;
|
|
99
|
+
var DEFAULT_DATA_PATH = path.join(process.cwd(), "data");
|
|
100
|
+
var DEFAULT_VIEWS_PATH = path.join(process.cwd(), "src", "views");
|
|
101
|
+
var DEFAULT_BUILD_PATH = path.join(process.cwd(), "dist");
|
|
102
|
+
var MAX_SLUG_LOG_LENGTH = 40;
|
|
103
|
+
var DEFAULT_BLOCK_SEPARATOR = "<!-- block -->";
|
|
104
|
+
function log(...args) {
|
|
105
|
+
console.log.apply(
|
|
106
|
+
null,
|
|
107
|
+
["[goffre]", ...args].map((x) => chalk.cyan(x))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
function getEnv() {
|
|
111
|
+
return {
|
|
112
|
+
mode: process.env.MODE || "dev"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function stringify(token) {
|
|
116
|
+
if (token instanceof Date) {
|
|
117
|
+
return token.toISOString().split("T")[0];
|
|
118
|
+
}
|
|
119
|
+
return `${token}`;
|
|
120
|
+
}
|
|
121
|
+
function getSlug(slug, params) {
|
|
122
|
+
return slug.split("/").reduce((memo, x) => {
|
|
123
|
+
if (!x.startsWith(":")) {
|
|
124
|
+
return [...memo, x];
|
|
125
|
+
}
|
|
126
|
+
const param = x.slice(1);
|
|
127
|
+
const value = params[param];
|
|
128
|
+
if (!value) {
|
|
129
|
+
throw new Error(`No value found for parameter: ${param}`);
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
...memo,
|
|
133
|
+
slugify(stringify(value), {
|
|
134
|
+
lower: true,
|
|
135
|
+
strict: true
|
|
136
|
+
})
|
|
137
|
+
];
|
|
138
|
+
}, []).join("/");
|
|
139
|
+
}
|
|
140
|
+
function getTemplate({
|
|
141
|
+
page,
|
|
142
|
+
templates = [],
|
|
143
|
+
defaultTemplate = "_default"
|
|
144
|
+
}) {
|
|
145
|
+
if (templates.find((x) => x === `${page.template}.handlebars`)) {
|
|
146
|
+
return page.template;
|
|
147
|
+
}
|
|
148
|
+
if (templates.find((x) => x.startsWith(page.slug))) {
|
|
149
|
+
return page.slug;
|
|
150
|
+
}
|
|
151
|
+
return defaultTemplate;
|
|
152
|
+
}
|
|
153
|
+
function renderPage({
|
|
154
|
+
app,
|
|
155
|
+
templates,
|
|
156
|
+
buildPath,
|
|
157
|
+
maxSlugLogLength,
|
|
158
|
+
blockSeparator,
|
|
159
|
+
...page
|
|
160
|
+
}) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const template = getTemplate({ page, templates });
|
|
163
|
+
switch (app.locals.options.logLevel) {
|
|
164
|
+
case "silent":
|
|
165
|
+
break;
|
|
166
|
+
case "verbose":
|
|
167
|
+
log(
|
|
168
|
+
`Generating ${chalk.yellow(
|
|
169
|
+
page.slug.padEnd(maxSlugLogLength || MAX_SLUG_LOG_LENGTH, " ")
|
|
170
|
+
)} with template ${chalk.green(template)}...`
|
|
171
|
+
);
|
|
172
|
+
break;
|
|
173
|
+
case "normal":
|
|
174
|
+
default:
|
|
175
|
+
log(`Generating ${chalk.yellow(page.slug)}...`);
|
|
176
|
+
}
|
|
177
|
+
app.render(
|
|
178
|
+
template,
|
|
179
|
+
{
|
|
180
|
+
...page,
|
|
181
|
+
layout: typeof page.layout === "undefined" ? "main" : page.layout,
|
|
182
|
+
content: page.content ? marked2.parse(page.content) : null,
|
|
183
|
+
blocks: getPageBlocks(
|
|
184
|
+
page.content,
|
|
185
|
+
blockSeparator || DEFAULT_BLOCK_SEPARATOR
|
|
186
|
+
)
|
|
187
|
+
},
|
|
188
|
+
async (error, html) => {
|
|
189
|
+
if (error) {
|
|
190
|
+
reject(error);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const outputFileName = `${page.slug}${page.extname || ".html"}`;
|
|
194
|
+
await outputFile(path.join(buildPath, outputFileName), html);
|
|
195
|
+
resolve({
|
|
196
|
+
...page,
|
|
197
|
+
outputFileName
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function getPageBlocks(content = "", separator) {
|
|
204
|
+
if (!content.includes(separator)) {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const blocks = content.split(separator).map((x) => marked2.parse(x));
|
|
208
|
+
return blocks.filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
async function load({ dataPath } = {}) {
|
|
211
|
+
return {
|
|
212
|
+
json: await loadJSON(dataPath || DEFAULT_DATA_PATH),
|
|
213
|
+
pages: await loadMarkdown(dataPath || DEFAULT_DATA_PATH)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
async function loadJSON(cwd) {
|
|
217
|
+
const files = await globby("**/*.json", { cwd });
|
|
218
|
+
return files.reduce(
|
|
219
|
+
(memo, x) => ({
|
|
220
|
+
...memo,
|
|
221
|
+
[path.basename(x, ".json")]: require2(path.join(cwd, x))
|
|
222
|
+
}),
|
|
223
|
+
{}
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
async function loadMarkdown(cwd) {
|
|
227
|
+
const files = await globby("**/*.md", { cwd });
|
|
228
|
+
return Promise.all(
|
|
229
|
+
files.map(async (fileName) => {
|
|
230
|
+
const fullPath = path.join(cwd, fileName);
|
|
231
|
+
const contents = await readFile(fullPath, "utf-8");
|
|
232
|
+
const parsed = matter(contents, { excerpt: true });
|
|
233
|
+
const outputFileName = fileName.replace(".md", "");
|
|
234
|
+
const slug = !parsed.data.slug ? outputFileName : getSlug(parsed.data.slug, parsed.data);
|
|
235
|
+
return {
|
|
236
|
+
...parsed.data,
|
|
237
|
+
excerpt: parsed.excerpt,
|
|
238
|
+
slug,
|
|
239
|
+
description: parsed.data.description || parsed.excerpt,
|
|
240
|
+
content: parsed.content
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function getSorter({
|
|
246
|
+
sortBy,
|
|
247
|
+
order
|
|
248
|
+
}) {
|
|
249
|
+
return (a, b) => {
|
|
250
|
+
let output;
|
|
251
|
+
const valA = a[sortBy];
|
|
252
|
+
const valB = b[sortBy];
|
|
253
|
+
if (valA instanceof Date && valB instanceof Date) {
|
|
254
|
+
output = Number(new Date(valA)) - Number(new Date(valB));
|
|
255
|
+
} else {
|
|
256
|
+
output = Number(valA) - Number(valB);
|
|
257
|
+
}
|
|
258
|
+
return order === "desc" ? -output : output;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function render({
|
|
262
|
+
pages,
|
|
263
|
+
viewsPath = DEFAULT_VIEWS_PATH,
|
|
264
|
+
buildPath = DEFAULT_BUILD_PATH,
|
|
265
|
+
blockSeparator = DEFAULT_BLOCK_SEPARATOR,
|
|
266
|
+
domain,
|
|
267
|
+
uglyUrls = false,
|
|
268
|
+
logLevel = "normal",
|
|
269
|
+
locals = {},
|
|
270
|
+
markdown: markdown2 = {},
|
|
271
|
+
handlebars = {},
|
|
272
|
+
sitemap = {},
|
|
273
|
+
env = {}
|
|
274
|
+
}) {
|
|
275
|
+
const extname = handlebars.extname || ".handlebars";
|
|
276
|
+
const app = express();
|
|
277
|
+
app.engine(
|
|
278
|
+
extname,
|
|
279
|
+
engine({
|
|
280
|
+
...handlebars,
|
|
281
|
+
helpers: {
|
|
282
|
+
...helpers_exports,
|
|
283
|
+
...handlebars.helpers
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
app.set("view engine", "handlebars");
|
|
288
|
+
app.set("layoutsDir", path.join(viewsPath, "layouts"));
|
|
289
|
+
app.set("views", viewsPath);
|
|
290
|
+
const templates = await globby(`**/*${extname}`, {
|
|
291
|
+
cwd: viewsPath
|
|
292
|
+
});
|
|
293
|
+
app.locals = {
|
|
294
|
+
...app.locals,
|
|
295
|
+
...locals,
|
|
296
|
+
options: {
|
|
297
|
+
domain,
|
|
298
|
+
uglyUrls,
|
|
299
|
+
logLevel
|
|
300
|
+
},
|
|
301
|
+
env: { ...getEnv(), ...env }
|
|
302
|
+
};
|
|
303
|
+
if (markdown2.middleware) {
|
|
304
|
+
markdown2.middleware.forEach(
|
|
305
|
+
(x) => marked2.use(typeof x === "function" ? x() : x)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
marked2.use(markdown2);
|
|
309
|
+
switch (logLevel) {
|
|
310
|
+
case "silent":
|
|
311
|
+
break;
|
|
312
|
+
case "verbose":
|
|
313
|
+
case "normal":
|
|
314
|
+
default:
|
|
315
|
+
log(`Start generation...`);
|
|
316
|
+
}
|
|
317
|
+
const results = await Promise.all(
|
|
318
|
+
pages.map(
|
|
319
|
+
(x) => renderPage({
|
|
320
|
+
...x,
|
|
321
|
+
buildPath,
|
|
322
|
+
app,
|
|
323
|
+
templates,
|
|
324
|
+
blockSeparator,
|
|
325
|
+
maxSlugLogLength: Math.min(
|
|
326
|
+
Math.max.call(null, ...pages.map((x2) => x2.slug.length)),
|
|
327
|
+
MAX_SLUG_LOG_LENGTH
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
switch (logLevel) {
|
|
333
|
+
case "silent":
|
|
334
|
+
break;
|
|
335
|
+
case "verbose":
|
|
336
|
+
case "normal":
|
|
337
|
+
default:
|
|
338
|
+
log(`Generated ${results.length} pages`);
|
|
339
|
+
}
|
|
340
|
+
if (sitemap.generate) {
|
|
341
|
+
renderPage({
|
|
342
|
+
slug: "sitemap",
|
|
343
|
+
template: sitemap.template || "sitemap",
|
|
344
|
+
extname: ".xml",
|
|
345
|
+
layout: null,
|
|
346
|
+
pages: results,
|
|
347
|
+
buildPath,
|
|
348
|
+
app,
|
|
349
|
+
templates
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return results;
|
|
353
|
+
}
|
|
354
|
+
function paginate({
|
|
355
|
+
collection,
|
|
356
|
+
size = 10,
|
|
357
|
+
sortBy = "slug",
|
|
358
|
+
order = "asc"
|
|
359
|
+
}) {
|
|
360
|
+
const total = Math.ceil(collection.length / size);
|
|
361
|
+
return collection.toSorted(getSorter({ sortBy, order })).reduce((memo, x, index) => {
|
|
362
|
+
if (index % size === 0) {
|
|
363
|
+
const page = Math.floor(index / size) + 1;
|
|
364
|
+
return [
|
|
365
|
+
...memo,
|
|
366
|
+
{
|
|
367
|
+
pagination: {
|
|
368
|
+
page,
|
|
369
|
+
prev: page > 1 ? page - 1 : null,
|
|
370
|
+
next: page < total ? page + 1 : null,
|
|
371
|
+
total
|
|
372
|
+
},
|
|
373
|
+
items: [x]
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
return [
|
|
378
|
+
...memo.slice(0, -1),
|
|
379
|
+
{
|
|
380
|
+
...memo[memo.length - 1],
|
|
381
|
+
items: [...memo[memo.length - 1].items, x]
|
|
382
|
+
}
|
|
383
|
+
];
|
|
384
|
+
}, []);
|
|
385
|
+
}
|
|
386
|
+
export {
|
|
387
|
+
getSlug,
|
|
388
|
+
getSorter,
|
|
389
|
+
getTemplate,
|
|
390
|
+
load,
|
|
391
|
+
loadJSON,
|
|
392
|
+
loadMarkdown,
|
|
393
|
+
paginate,
|
|
394
|
+
render
|
|
395
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonwave99/goffre",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Mini static site generator",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Diego Caponera",
|
|
@@ -13,11 +13,23 @@
|
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://moonwave99.github.io/goffre",
|
|
15
15
|
"type": "module",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
16
|
+
"ava": {
|
|
17
|
+
"extensions": {
|
|
18
|
+
"ts": "module"
|
|
19
|
+
},
|
|
20
|
+
"nodeArguments": [
|
|
21
|
+
"--import=tsimp"
|
|
22
|
+
],
|
|
23
|
+
"environmentVariables": {
|
|
24
|
+
"TSIMP_DIAG": "ignore"
|
|
25
|
+
}
|
|
19
26
|
},
|
|
20
|
-
"main": "index.js",
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"module": "dist/index.mjs",
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
21
33
|
"keywords": [
|
|
22
34
|
"front-matter",
|
|
23
35
|
"generate",
|
|
@@ -27,29 +39,46 @@
|
|
|
27
39
|
"static-site",
|
|
28
40
|
"yaml"
|
|
29
41
|
],
|
|
30
|
-
"files": [
|
|
31
|
-
"lib/goffre.js",
|
|
32
|
-
"lib/helpers.js"
|
|
33
|
-
],
|
|
34
42
|
"license": "MIT",
|
|
35
43
|
"dependencies": {
|
|
36
44
|
"chalk": "^4.1.2",
|
|
37
45
|
"express": "^4.17.1",
|
|
38
|
-
"express-handlebars": "^
|
|
46
|
+
"express-handlebars": "^8.0.6",
|
|
39
47
|
"fs-extra": "^10.0.0",
|
|
40
48
|
"globby": "^12.0.2",
|
|
41
49
|
"gray-matter": "^4.0.3",
|
|
42
|
-
"marked": "^
|
|
50
|
+
"marked": "^17.0.3",
|
|
43
51
|
"slugify": "^1.6.2"
|
|
44
52
|
},
|
|
45
53
|
"devDependencies": {
|
|
46
|
-
"
|
|
54
|
+
"@eslint/compat": "^2.0.2",
|
|
55
|
+
"@eslint/eslintrc": "^3.3.3",
|
|
56
|
+
"@eslint/js": "^10.0.1",
|
|
57
|
+
"@faker-js/faker": "^10.3.0",
|
|
58
|
+
"@types/express": "^5.0.6",
|
|
59
|
+
"@types/fs-extra": "^11.0.4",
|
|
60
|
+
"@types/js-yaml": "^4.0.9",
|
|
61
|
+
"@types/node": "^25.2.3",
|
|
62
|
+
"@types/sinon": "^21.0.0",
|
|
63
|
+
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
|
64
|
+
"@typescript-eslint/parser": "^8.56.0",
|
|
65
|
+
"ava": "^6.4.1",
|
|
47
66
|
"cheerio": "^1.0.0-rc.10",
|
|
48
67
|
"dirname-filename-esm": "^1.1.1",
|
|
49
|
-
"eslint": "^
|
|
68
|
+
"eslint": "^10.0.0",
|
|
50
69
|
"faker": "^5.5.3",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
70
|
+
"globals": "^17.3.0",
|
|
71
|
+
"js-yaml": "^4.1.1",
|
|
72
|
+
"rimraf": "^6.1.3",
|
|
73
|
+
"sinon": "^21.0.1",
|
|
74
|
+
"tsimp": "^2.0.12",
|
|
75
|
+
"tsup": "^8.5.1",
|
|
76
|
+
"typescript": "^5.9.3"
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsup",
|
|
80
|
+
"dev": "tsup --watch",
|
|
81
|
+
"test": "ava **/*.test.ts",
|
|
82
|
+
"lint": "pnpm eslint ./lib --ext .js,.jsx,.ts,.tsx"
|
|
54
83
|
}
|
|
55
|
-
}
|
|
84
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./lib/goffre.js";
|
package/lib/goffre.js
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { globby } from "globby";
|
|
3
|
-
import { marked } from "marked";
|
|
4
|
-
import matter from "gray-matter";
|
|
5
|
-
import { createRequire } from "module";
|
|
6
|
-
import fs from "fs-extra";
|
|
7
|
-
import express from "express";
|
|
8
|
-
import { engine } from "express-handlebars";
|
|
9
|
-
import chalk from "chalk";
|
|
10
|
-
import slugify from "slugify";
|
|
11
|
-
import * as defaultHelpers from "./helpers.js";
|
|
12
|
-
|
|
13
|
-
const require = createRequire(import.meta.url);
|
|
14
|
-
const { readFile, outputFile } = fs;
|
|
15
|
-
|
|
16
|
-
const DEFAULT_DATA_PATH = path.join(process.cwd(), "data");
|
|
17
|
-
const DEFAULT_VIEWS_PATH = path.join(process.cwd(), "src", "views");
|
|
18
|
-
const DEFAULT_BUILD_PATH = path.join(process.cwd(), "dist");
|
|
19
|
-
const MAX_SLUG_LOG_LENGTH = 40;
|
|
20
|
-
const DEFAULT_BLOCK_SEPARATOR = "<!-- block -->";
|
|
21
|
-
|
|
22
|
-
function log() {
|
|
23
|
-
console.log.apply(
|
|
24
|
-
null,
|
|
25
|
-
["[goffre]", ...arguments].map((x) => chalk.cyan(x)),
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getEnv() {
|
|
30
|
-
return {
|
|
31
|
-
mode: process.env.MODE || "dev",
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function stringify(token) {
|
|
36
|
-
if (token instanceof Date) {
|
|
37
|
-
return token.toISOString().split("T")[0];
|
|
38
|
-
}
|
|
39
|
-
return token;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getSlug(slug, params) {
|
|
43
|
-
return slug
|
|
44
|
-
.split("/")
|
|
45
|
-
.reduce(
|
|
46
|
-
(memo, x) =>
|
|
47
|
-
!x.startsWith(":")
|
|
48
|
-
? [...memo, x]
|
|
49
|
-
: [
|
|
50
|
-
...memo,
|
|
51
|
-
slugify(stringify(params[x.slice(1)]), {
|
|
52
|
-
lower: true,
|
|
53
|
-
strict: true,
|
|
54
|
-
}),
|
|
55
|
-
],
|
|
56
|
-
[],
|
|
57
|
-
)
|
|
58
|
-
.join("/");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function getTemplate({
|
|
62
|
-
page,
|
|
63
|
-
templates = [],
|
|
64
|
-
defaultTemplate = "_default",
|
|
65
|
-
}) {
|
|
66
|
-
if (
|
|
67
|
-
templates.find((x) => path.basename(x, ".handlebars") === page.template)
|
|
68
|
-
) {
|
|
69
|
-
return page.template;
|
|
70
|
-
}
|
|
71
|
-
if (templates.find((x) => x.startsWith(page.slug))) {
|
|
72
|
-
return page.slug;
|
|
73
|
-
}
|
|
74
|
-
return defaultTemplate;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function renderPage({
|
|
78
|
-
app,
|
|
79
|
-
templates,
|
|
80
|
-
buildPath,
|
|
81
|
-
maxSlugLogLength,
|
|
82
|
-
blockSeparator,
|
|
83
|
-
...page
|
|
84
|
-
}) {
|
|
85
|
-
return new Promise((resolve, reject) => {
|
|
86
|
-
const template = getTemplate({ page, templates });
|
|
87
|
-
|
|
88
|
-
switch (app.locals.options.logLevel) {
|
|
89
|
-
case "silent":
|
|
90
|
-
break;
|
|
91
|
-
case "verbose":
|
|
92
|
-
log(
|
|
93
|
-
`Generating ${chalk.yellow(
|
|
94
|
-
page.slug.padEnd(maxSlugLogLength, " "),
|
|
95
|
-
)} with template ${chalk.green(template)}...`,
|
|
96
|
-
);
|
|
97
|
-
break;
|
|
98
|
-
case "normal":
|
|
99
|
-
default:
|
|
100
|
-
log(`Generating ${chalk.yellow(page.slug)}...`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
app.render(
|
|
104
|
-
template,
|
|
105
|
-
{
|
|
106
|
-
...page,
|
|
107
|
-
layout: typeof page.layout === "undefined" ? "main" : page.layout,
|
|
108
|
-
content: page.content ? marked.parse(page.content) : null,
|
|
109
|
-
blocks: getPageBlocks(page.content, blockSeparator),
|
|
110
|
-
},
|
|
111
|
-
async (error, html) => {
|
|
112
|
-
if (error) {
|
|
113
|
-
reject(error);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const outputFileName = `${page.slug}${page.extname || ".html"}`;
|
|
117
|
-
await outputFile(path.join(buildPath, outputFileName), html);
|
|
118
|
-
resolve({
|
|
119
|
-
...page,
|
|
120
|
-
outputFileName,
|
|
121
|
-
});
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function getPageBlocks(content = "", separator) {
|
|
128
|
-
if (!content.includes(separator)) {
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
131
|
-
const blocks = content.split(separator).map((x) => marked.parse(x));
|
|
132
|
-
return blocks.filter(Boolean);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export async function load({ dataPath } = {}) {
|
|
136
|
-
return {
|
|
137
|
-
json: await loadJSON(dataPath || DEFAULT_DATA_PATH),
|
|
138
|
-
pages: await loadMarkdown(dataPath || DEFAULT_DATA_PATH),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function loadJSON(cwd) {
|
|
143
|
-
const files = await globby("**/*.json", { cwd });
|
|
144
|
-
return files.reduce(
|
|
145
|
-
(memo, x) => ({
|
|
146
|
-
...memo,
|
|
147
|
-
[path.basename(x, ".json")]: require(path.join(cwd, x)),
|
|
148
|
-
}),
|
|
149
|
-
{},
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function excerpt(file) {
|
|
154
|
-
file.excerpt = file.content.split("\n")[1];
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function loadMarkdown(cwd) {
|
|
158
|
-
const files = await globby("**/*.md", { cwd });
|
|
159
|
-
return Promise.all(
|
|
160
|
-
files.map(async (fileName) => {
|
|
161
|
-
const fullPath = path.join(cwd, fileName);
|
|
162
|
-
const contents = await readFile(fullPath, "utf-8");
|
|
163
|
-
const parsed = matter(contents, { excerpt });
|
|
164
|
-
const outputFileName = fileName.replace(".md", "");
|
|
165
|
-
const slug = !parsed.data.slug
|
|
166
|
-
? outputFileName
|
|
167
|
-
: getSlug(parsed.data.slug, parsed.data);
|
|
168
|
-
return {
|
|
169
|
-
...parsed.data,
|
|
170
|
-
excerpt: parsed.excerpt,
|
|
171
|
-
slug,
|
|
172
|
-
description: parsed.data.description || parsed.excerpt,
|
|
173
|
-
content: parsed.content,
|
|
174
|
-
};
|
|
175
|
-
}),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export const getSorter =
|
|
180
|
-
({ sortBy, order }) =>
|
|
181
|
-
(a, b) => {
|
|
182
|
-
let output;
|
|
183
|
-
if (a[sortBy] instanceof Date) {
|
|
184
|
-
output = new Date(a[sortBy]) - new Date(b[sortBy]);
|
|
185
|
-
} else {
|
|
186
|
-
output = a[sortBy] - b[sortBy];
|
|
187
|
-
}
|
|
188
|
-
return order === "desc" ? -output : output;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
export async function render({
|
|
192
|
-
pages,
|
|
193
|
-
viewsPath = DEFAULT_VIEWS_PATH,
|
|
194
|
-
buildPath = DEFAULT_BUILD_PATH,
|
|
195
|
-
blockSeparator = DEFAULT_BLOCK_SEPARATOR,
|
|
196
|
-
domain,
|
|
197
|
-
uglyUrls = false,
|
|
198
|
-
logLevel = "normal",
|
|
199
|
-
locals = {},
|
|
200
|
-
markdown = {},
|
|
201
|
-
handlebars = {},
|
|
202
|
-
sitemap = {},
|
|
203
|
-
env = {},
|
|
204
|
-
}) {
|
|
205
|
-
const extname = handlebars.extname || ".handlebars";
|
|
206
|
-
const app = express();
|
|
207
|
-
app.engine(
|
|
208
|
-
extname,
|
|
209
|
-
engine({
|
|
210
|
-
...handlebars,
|
|
211
|
-
helpers: {
|
|
212
|
-
...defaultHelpers,
|
|
213
|
-
...handlebars.helpers,
|
|
214
|
-
},
|
|
215
|
-
}),
|
|
216
|
-
);
|
|
217
|
-
app.set("view engine", "handlebars");
|
|
218
|
-
app.set("layoutsDir", path.join(viewsPath, "layouts"));
|
|
219
|
-
app.set("views", viewsPath);
|
|
220
|
-
|
|
221
|
-
const templates = await globby(`**/*${extname}`, {
|
|
222
|
-
cwd: viewsPath,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
app.locals = {
|
|
226
|
-
...app.locals,
|
|
227
|
-
...locals,
|
|
228
|
-
options: {
|
|
229
|
-
domain,
|
|
230
|
-
uglyUrls,
|
|
231
|
-
logLevel,
|
|
232
|
-
},
|
|
233
|
-
env: { ...getEnv(), ...env },
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
marked.use(markdown);
|
|
237
|
-
|
|
238
|
-
switch (logLevel) {
|
|
239
|
-
case "silent":
|
|
240
|
-
break;
|
|
241
|
-
case "verbose":
|
|
242
|
-
case "normal":
|
|
243
|
-
default:
|
|
244
|
-
log(`Start generation...`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const results = await Promise.all(
|
|
248
|
-
pages.map((x) =>
|
|
249
|
-
renderPage({
|
|
250
|
-
...x,
|
|
251
|
-
buildPath,
|
|
252
|
-
app,
|
|
253
|
-
templates,
|
|
254
|
-
blockSeparator,
|
|
255
|
-
maxSlugLogLength: Math.min(
|
|
256
|
-
Math.max.call(null, ...pages.map((x) => x.slug.length)),
|
|
257
|
-
MAX_SLUG_LOG_LENGTH,
|
|
258
|
-
),
|
|
259
|
-
}),
|
|
260
|
-
),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
switch (logLevel) {
|
|
264
|
-
case "silent":
|
|
265
|
-
break;
|
|
266
|
-
case "verbose":
|
|
267
|
-
case "normal":
|
|
268
|
-
default:
|
|
269
|
-
log(`Generated ${results.length} pages`);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (sitemap.generate) {
|
|
273
|
-
renderPage({
|
|
274
|
-
slug: "sitemap",
|
|
275
|
-
template: sitemap.template || "sitemap",
|
|
276
|
-
extname: ".xml",
|
|
277
|
-
layout: null,
|
|
278
|
-
pages: results,
|
|
279
|
-
buildPath,
|
|
280
|
-
app,
|
|
281
|
-
templates,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return results;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export function paginate({
|
|
289
|
-
collection,
|
|
290
|
-
size = 10,
|
|
291
|
-
sortBy = "slug",
|
|
292
|
-
order = "asc",
|
|
293
|
-
}) {
|
|
294
|
-
const total = Math.ceil(collection.length / size);
|
|
295
|
-
return collection
|
|
296
|
-
.sort(getSorter({ sortBy, order }))
|
|
297
|
-
.reduce((memo, x, index) => {
|
|
298
|
-
if (index % size === 0) {
|
|
299
|
-
const page = Math.floor(index / size) + 1;
|
|
300
|
-
return [
|
|
301
|
-
...memo,
|
|
302
|
-
{
|
|
303
|
-
pagination: {
|
|
304
|
-
page,
|
|
305
|
-
prev: page > 1 ? page - 1 : null,
|
|
306
|
-
next: page < total ? page + 1 : null,
|
|
307
|
-
total,
|
|
308
|
-
},
|
|
309
|
-
items: [x],
|
|
310
|
-
},
|
|
311
|
-
];
|
|
312
|
-
}
|
|
313
|
-
return [
|
|
314
|
-
...memo.slice(0, -1),
|
|
315
|
-
{
|
|
316
|
-
...memo[memo.length - 1],
|
|
317
|
-
items: [...memo[memo.length - 1].items, x],
|
|
318
|
-
},
|
|
319
|
-
];
|
|
320
|
-
}, []);
|
|
321
|
-
}
|
package/lib/helpers.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { getSorter, getSlug } from "./goffre.js";
|
|
2
|
-
import { marked } from "marked";
|
|
3
|
-
|
|
4
|
-
export const markdown = (text) => marked(text);
|
|
5
|
-
|
|
6
|
-
export const getParamLink = (url, options) => {
|
|
7
|
-
const output = getSlug(url, options.hash);
|
|
8
|
-
return getAsset(output, options);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const getAsset = (asset, context) => {
|
|
12
|
-
const { options, env } = context.data.root;
|
|
13
|
-
return env.mode === "prod" && options.domain
|
|
14
|
-
? `${options.domain}${asset.startsWith("/") ? "" : "/"}${asset}`
|
|
15
|
-
: asset;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const getSitemapLink = (page, context) => {
|
|
19
|
-
const { options } = context.data.root;
|
|
20
|
-
return `${options.domain}${getLink(page, context)}`;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const getLink = (page, context) => {
|
|
24
|
-
const base =
|
|
25
|
-
page.link || `${page.slug.startsWith("/") ? "" : "/"}${page.slug}`;
|
|
26
|
-
const { uglyUrls } = context.data.root.options;
|
|
27
|
-
if (uglyUrls) {
|
|
28
|
-
return getAsset(`${base === "/" ? "/index" : base}.html`, context);
|
|
29
|
-
}
|
|
30
|
-
return getAsset(base.replace(/^\/index/, "/"), context);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const getNavClass = ({ slug }, currentPage) => {
|
|
34
|
-
const cleanSlug = slug && slug[0] === "/" ? slug.slice(1) : slug;
|
|
35
|
-
return currentPage.slug.startsWith(cleanSlug)
|
|
36
|
-
? `${cleanSlug} current`
|
|
37
|
-
: cleanSlug;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const list = (context, options) => {
|
|
41
|
-
const offset = parseInt(options.hash.offset, 10) || 0;
|
|
42
|
-
const limit = parseInt(options.hash.limit, 10) || 100;
|
|
43
|
-
const sortBy = options.hash.sortBy || "slug";
|
|
44
|
-
const order = options.hash.order || "asc";
|
|
45
|
-
|
|
46
|
-
let output = "";
|
|
47
|
-
let i, j;
|
|
48
|
-
|
|
49
|
-
const data = [...context].sort(getSorter({ sortBy, order }));
|
|
50
|
-
|
|
51
|
-
if (offset < 0) {
|
|
52
|
-
i = -offset < data.length ? data.length - -offset : 0;
|
|
53
|
-
} else {
|
|
54
|
-
i = offset < data.length ? offset : 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
j = limit + i < data.length ? limit + i : data.length;
|
|
58
|
-
|
|
59
|
-
for (i, j; i < j; i++) {
|
|
60
|
-
output += options.fn(data[i]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return output;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const nextItem = (context, options) => {
|
|
67
|
-
const { list } = options.hash;
|
|
68
|
-
const index = list.findIndex((x) => x.slug === context.slug);
|
|
69
|
-
const next = list[index + 1];
|
|
70
|
-
if (!next) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
return options.fn(next);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export const prevItem = (context, options) => {
|
|
77
|
-
const { list } = options.hash;
|
|
78
|
-
const index = list.findIndex((x) => x.slug === context.slug);
|
|
79
|
-
const prev = list[index - 1];
|
|
80
|
-
if (!prev) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
return options.fn(prev);
|
|
84
|
-
};
|