mikel-press 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +29 -0
  2. package/index.js +116 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -98,6 +98,7 @@ Each HTML file processed by **mikel-press** will be handled by the mikel templat
98
98
  | Variable | Description |
99
99
  |----------|-------------|
100
100
  | `site.pages` | A list containing all pages processed by **mikel-press**. |
101
+ | `site.assets` | A list containing all assets files loaded by the `AssetsPlugin`. |
101
102
  | `site.data` | An object containing all data items loaded by `DataPlugin`. |
102
103
  | `site.partials` | A list containing all partials files loaded by the `PartialsPlugin`. |
103
104
  | `site.layouts` | A list containing all layout files loaded by the `LayoutsPlugin`. |
@@ -110,9 +111,22 @@ Each HTML file processed by **mikel-press** will be handled by the mikel templat
110
111
  | `page.content` | The raw content of the page before begin processed by **mikel**. |
111
112
  | `page.title` | The title of the page. |
112
113
  | `page.path` | The path to the page. Example: `about/index.html`. |
114
+ | `page.dir` | The directory of the page. Example: `about`. |
115
+ | `page.name` | The name of the page without extension. Example: `index`. |
116
+ | `page.ext` | The file extension of the page. Example: `.html`. |
113
117
  | `page.url` | The path to the page including the leading `/`. Example: `/about/index.html`. |
114
118
  | `page.attributes` | An object containing all the frontmatter variables in the page processed by `FrontmatterPlugin`. |
115
119
 
120
+ #### Asset variables
121
+
122
+ | Variable | Description |
123
+ |----------|-------------|
124
+ | `asset.path` | The path to the asset file. Example: `images/logo.png`. |
125
+ | `asset.dir` | The directory of the asset file. Example: `images`. |
126
+ | `asset.name` | The name of the asset file without extension. Example: `logo`. |
127
+ | `asset.ext` | The file extension of the asset file. Example: `.png`. |
128
+ | `asset.url` | The path to the asset file including the leading `/`. Example: `/images/logo.png`. |
129
+
116
130
  ## Plugins
117
131
 
118
132
  **mikel-press** relies on plugins to handle file reading, transformation, and rendering. The following plugins are built-in:
@@ -187,6 +201,21 @@ This plugin copies static files from the source to the destination.
187
201
  Options:
188
202
  - `options.patterns` (array): List of file patterns to copy. Each pattern should have `from` and `to`.
189
203
 
204
+ ### `press.RedirectsPlugin(options)`
205
+
206
+ The RedirectsPlugin lets you define URL redirects. For each redirect rule, the plugin generates a small HTML file that forwards visitors to the target URL using both HTML and JavaScript fallbacks.
207
+
208
+ Options:
209
+ - `options.redirects` (array): list of redirections. Each redirection should have `from` and `to`.
210
+
211
+ ```javascript
212
+ press.RedirectsPlugin({
213
+ redirects: [
214
+ { from: "/socials/github.html", to: "https://github.com/jmjuanes" },
215
+ ],
216
+ });
217
+ ```
218
+
190
219
  ## API
191
220
 
192
221
  **mikel-press** exposes a single function that triggers the build with the given configuration object provided as an argument.
package/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import * as crypto from "node:crypto";
4
+
5
+ // @description internal method to get the first node that ends with the provided string
6
+ const getNodeFromSource = (nodes = [], endingStr = "") => {
7
+ return (nodes || []).find(node => (node.source || "").endsWith(endingStr || ""));
8
+ };
3
9
 
4
10
  // @description get all plugins of the given type
5
11
  const getPlugins = (context, name) => {
@@ -11,6 +17,26 @@ const applyLayout = page => {
11
17
  return `{{>>layout:${page.attributes.layout}}}\n\n${page.content}\n\n{{/layout:${page.attributes.layout}}}\n`;
12
18
  };
13
19
 
20
+ // @description generate the content for the redirection page
21
+ const generateRedirectHTML = (to, status) => {
22
+ const content = [
23
+ `<!DOCTYPE html>`,
24
+ `<html lang="en">`,
25
+ `<head>`,
26
+ ` <meta charset="utf-8" />`,
27
+ ` <title>Redirecting...</title>`,
28
+ ` <meta http-equiv="refresh" content="0; url=${to}" />`,
29
+ ` <link rel="canonical" href="${to}" />`,
30
+ ` <script>window.location.replace("${to}");</script>`,
31
+ `</head>`,
32
+ `<body>`,
33
+ ` <p>Redirectig to <a href="${to}">${to}</a>...</p>`,
34
+ `</body>`,
35
+ `</html>`,
36
+ ];
37
+ return content.join("\n");
38
+ };
39
+
14
40
  // @description press main function
15
41
  // @param {Object} config - configuration object
16
42
  // @param {String} config.source - source folder
@@ -41,6 +67,26 @@ press.createContext = (config = {}) => {
41
67
  nodes: [],
42
68
  actions: {},
43
69
  });
70
+ // register helpers and funcions
71
+ context.template.addFunction("getPageUrl", params => {
72
+ return getNodeFromSource(params?.variables?.root?.site?.pages || [], params.args[0])?.url || "";
73
+ });
74
+ context.template.addFunction("getAssetUrl", params => {
75
+ return getNodeFromSource(params?.variables?.root?.site?.assets || [], params.args[0])?.url || "";
76
+ });
77
+ context.template.addHelper("pages", params => {
78
+ const draft = params?.options?.draft ?? params?.opt?.draft;
79
+ const collection = params?.opt?.collection || params?.options?.collection || null;
80
+ const items = (params.data?.site?.pages || []).filter(page => {
81
+ if (typeof draft === "boolean" && draft !== !!page?.attributes?.draft) {
82
+ return false;
83
+ }
84
+ return !collection || page.attributes?.collection === collection;
85
+ });
86
+ const limit = Math.min(items.length, params.options?.limit || params.opt?.limit || items.length);
87
+ return items.slice(0, limit).reverse().map((c, i) => params.fn(c, {index: i})).join("");
88
+ });
89
+ // initialize plugins
44
90
  getPlugins(context, "init").forEach(plugin => {
45
91
  return plugin.init(context);
46
92
  });
@@ -117,6 +163,18 @@ press.watchContext = (context, options = {}) => {
117
163
 
118
164
  // @description general utilities
119
165
  press.utils = {
166
+ // @description generate the md5 hash of the given content
167
+ // @param {String} content - content to hash
168
+ // @returns {String} md5 hash
169
+ md5: content => {
170
+ return crypto.createHash("md5").update(content).digest("hex");
171
+ },
172
+ // @description generate a random identifier
173
+ // @param {Number} length - length of the identifier
174
+ // @returns {String} random identifier
175
+ randomId: (length = 20) => {
176
+ return crypto.randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
177
+ },
120
178
  // @description read a file from disk
121
179
  // @param {String} file path to the file to read
122
180
  read: (file, encoding = "utf8") => {
@@ -149,6 +207,26 @@ press.utils = {
149
207
  .filter(file => (extensions === "*" || extensions.includes(path.extname(file))) && !exclude.includes(file))
150
208
  .filter(file => fs.statSync(path.join(folder, file)).isFile());
151
209
  },
210
+ // @description walk through the given folder and get all files
211
+ // @params {String} folder folder to walk through
212
+ // @params {Array|String} extensions extensions to include. Default: "*"
213
+ walkdir: (folder, extensions = "*", exclude = []) => {
214
+ const walkSync = (currentFolder, files = []) => {
215
+ const fullFolderPath = path.join(folder, currentFolder);
216
+ fs.readdirSync(fullFolderPath).forEach(file => {
217
+ const filePath = path.join(currentFolder, file);
218
+ const fullFilePath = path.join(fullFolderPath, file);
219
+ if (fs.statSync(fullFilePath).isDirectory()) {
220
+ return walkSync(filePath, files);
221
+ }
222
+ if (extensions === "*" || extensions.includes(path.extname(file))) {
223
+ files.push(filePath);
224
+ }
225
+ });
226
+ return files;
227
+ };
228
+ return walkSync("./", []);
229
+ },
152
230
  // @description watch for file changes
153
231
  // @param {String} filePath path to the file to watch
154
232
  // @param {Function} listener method to listen for file changes
@@ -199,7 +277,6 @@ press.SourcePlugin = (options = {}) => {
199
277
  source: path.join(folder, file),
200
278
  label: options.label || press.LABEL_PAGE,
201
279
  path: path.join(options?.basePath || ".", file),
202
- url: path.normalize("/" + path.join(options?.basePath || ".", file)),
203
280
  };
204
281
  });
205
282
  },
@@ -273,10 +350,7 @@ press.FrontmatterPlugin = () => {
273
350
  node.content = result.body || "";
274
351
  node.attributes = result.attributes || {};
275
352
  node.title = node.attributes?.title || node.path;
276
- if (node.attributes.permalink) {
277
- node.path = node.attributes.permalink;
278
- node.url = path.normalize("/" + node.path);
279
- }
353
+ node.path = node.attributes?.permalink || node.path;
280
354
  }
281
355
  },
282
356
  };
@@ -324,6 +398,15 @@ press.ContentPagePlugin = (siteData = {}) => {
324
398
  }
325
399
  });
326
400
  }
401
+ // 5. fix pages and assets variables
402
+ [...siteData.pages, ...siteData.assets].forEach(node => {
403
+ return Object.assign(node, {
404
+ ext: path.extname(node.path),
405
+ name: path.basename(node.path, path.extname(node.path)),
406
+ dir: path.dirname(node.path),
407
+ url: path.normalize(path.join("/", node.path)),
408
+ });
409
+ });
327
410
  },
328
411
  transform: (context, node) => {
329
412
  if (node.label === press.LABEL_PAGE && typeof node.content === "string") {
@@ -343,19 +426,34 @@ press.UsePlugin = mikelPlugin => {
343
426
  };
344
427
 
345
428
  // @description copy plugin
346
- press.CopyAssetsPlugin = (options = {}) => {
347
- return {
348
- load: () => {
349
- return (options?.patterns || [])
350
- .filter(item => item.from && fs.existsSync(path.resolve(item.from)))
351
- .map(item => ({
352
- source: path.resolve(item.from),
353
- path: path.join(options?.basePath || ".", item.to || path.basename(item.from)),
354
- label: options?.label || press.LABEL_ASSET,
355
- }));
356
- },
357
- };
358
- };
429
+ press.CopyAssetsPlugin = (options = {}) => ({
430
+ name: "CopyAssetsPlugin",
431
+ load: () => {
432
+ const filesToCopy = (options?.patterns || []).filter(item => {
433
+ return item.from && fs.existsSync(path.resolve(item.from));
434
+ });
435
+ return filesToCopy.map(item => {
436
+ return {
437
+ source: path.resolve(item.from),
438
+ path: path.join(options?.basePath || ".", item.to || path.basename(item.from)),
439
+ label: options?.label || press.LABEL_ASSET,
440
+ };
441
+ });
442
+ },
443
+ });
444
+
445
+ // @description redirections plugin
446
+ press.RedirectsPlugin = (options = {}) => ({
447
+ name: "RedirectsPlugin",
448
+ load: () => {
449
+ return (options.redirects || []).map(redirection => ({
450
+ source: redirection.from,
451
+ path: path.join(options?.basePath || ".", redirection.from),
452
+ label: press.LABEL_ASSET,
453
+ content: generateRedirectHTML(redirection.to),
454
+ }));
455
+ },
456
+ });
359
457
 
360
458
  // export press generator
361
459
  export default press;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mikel-press",
3
3
  "description": "A tiny and fast static site generator based on mikel templating",
4
- "version": "0.29.0",
4
+ "version": "0.30.1",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {