mikel-press 0.19.1 → 0.20.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.
Files changed (3) hide show
  1. package/README.md +61 -65
  2. package/index.js +138 -386
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -19,39 +19,61 @@ Or **npm**:
19
19
  $ npm install --dev mikel-press
20
20
  ```
21
21
 
22
+ ## Directory structure
23
+
24
+ A basic **mikel-press** directory structure looks like this:
25
+
26
+ ```
27
+ .
28
+ ├── data
29
+ │ └── projects.json
30
+ ├── partials
31
+ │ ├── footer.html
32
+ │ └── header.html
33
+ ├── posts
34
+ │ ├── 2025-04-03-introducing-our-new-project.html
35
+ │ ├── 2025-04-05-how-to-stay-productive.html
36
+ │ └── 2025-04-07-understanding-javascript-closures.html
37
+ ├── www
38
+ ├── press.js
39
+ ├── package.json
40
+ ├── projects.html
41
+ ├── blog.html
42
+ ├── about.html
43
+ └── index.html
44
+ ```
45
+
22
46
  ## Configuration
23
47
 
24
48
  **mikel-press** can be configured using a `config` object that accepts the following options:
25
49
 
26
50
  | Field | Description | Default |
27
51
  |-------|-------------|---------|
28
- | `source` | The path to the directory containing the site's HTML or Markdown files. | `"content"` |
52
+ | `source` | The path to the directory containing the site folders. | `"."` |
29
53
  | `destination` | The output directory where the generated static site will be saved. | `"www"` |
30
- | `layout` | The path to the layout file that will be used as the base template for all pages. | - |
31
- | `plugins` | A list of plugins used to extend the functionality of mikel-press. | `[]` |
54
+ | `extensions` | List of file extensions to process. | `[".html"]` |
55
+ | `mikelOptions` | An object containing custom configuration for the **mikel** templating engine. | `{}` |
56
+ | `plugins` | A list of plugins used to extend the functionality of **mikel-press**. | `[]` |
32
57
  | `*` | Any other properties passed in config will be available as `site.*` inside each page template. | - |
33
58
 
34
59
  Here is an example configuration object:
35
60
 
36
61
  ```javascript
37
- const config = {
38
- source: "./content",
62
+ import press from "mikel-press";
63
+
64
+ press({
65
+ source: ".",
39
66
  destination: "./www",
40
- layout: "./layout.html",
41
67
  title: "Hello world",
42
68
  description: "My awesome site",
43
69
  plugins: [
44
- press.SourcePlugin(),
70
+ press.SourcePlugin({folder: "posts", basePath: "blog"}),
71
+ press.PartialsLoaderPlugin(),
72
+ press.DataLoaderPlugin(),
45
73
  press.FrontmatterPlugin(),
46
- press.PermalinkPlugin(),
47
- press.ContentPlugin(),
48
- press.CopyAssetsPlugin({
49
- patterns: [
50
- { from: "./static/styles.css", to: "static/" },
51
- ],
52
- }),
74
+ press.ContentPagePlugin(),
53
75
  ],
54
- };
76
+ });
55
77
  ```
56
78
 
57
79
  ## Content
@@ -66,31 +88,25 @@ Each HTML file processed by **mikel-press** will be handled by the mikel templat
66
88
  |----------|-------------|
67
89
  | `site` | Contains the site information and all the additional keys provided in the configuration object. |
68
90
  | `page` | Specific information about the page that is rendered. |
69
- | `layout` | Specific information about the layout that is used for renderin the page. |
70
91
 
71
92
  #### Site variables
72
93
 
73
94
  | Variable | Description |
74
95
  |----------|-------------|
75
- | `site.data` | An object containing all data items loaded by `DataPlugin`. |
76
- | `site.pages` | A list containing all pages processed by **mikel-pres**. |
96
+ | `site.pages` | A list containing all pages processed by **mikel-press**. |
97
+ | `site.data` | An object containing all data items loaded by `DataLoaderPlugin`. |
98
+ | `site.partials` | A list containing all partials files loaded by the `PartialsLoaderPlugin`. |
77
99
  | `site.*` | All the additional configuration fields provided in the configuration. |
78
100
 
79
101
  #### Page variables
80
102
 
81
103
  | Variable | Description |
82
104
  |----------|-------------|
105
+ | `page.content` | The raw content of the page before begin processed by **mikel**. |
106
+ | `page.title` | The title of the page. |
83
107
  | `page.path` | The path to the page. Example: `about/index.html`. |
84
108
  | `page.url` | The path to the page including the leading `/`. Example: `/about/index.html`. |
85
109
  | `page.attributes` | An object containing all the frontmatter variables in the page processed by `FrontmatterPlugin`. |
86
- | `page.content` | The raw content of the page before begin processed by **mikel**. |
87
-
88
- #### Layout variables
89
-
90
- | Variable | Description |
91
- |----------|-------------|
92
- | `layout.attributes` | An object containing all the frontmatter variables in the layout processed by `FrontmatterPlugin`. |
93
- | `layout.content` | The raw content of the layout. |
94
110
 
95
111
  ## Plugins
96
112
 
@@ -98,41 +114,35 @@ Each HTML file processed by **mikel-press** will be handled by the mikel templat
98
114
 
99
115
  ### `press.SourcePlugin(options)`
100
116
 
101
- This plugin reads content from the specified `config.source` directory and loads it into the system for processing.
117
+ This plugin reads content from the specified directory and loads it into the system for processing.
102
118
 
103
119
  Options:
104
- - `options.source` (string): Specifies a custom source directory. If not provided, `config.source` is used.
105
- - `options.extensions` (array): Defines the file extensions that should be processed. The default value is `[".html", ".md", ".markdown"]`.
120
+ - `options.folder` (string): Specifies a custom source directory. If not provided, `config.source` is used.
121
+ - `options.extensions` (array): Defines the file extensions that should be processed. If not provided, it will use `config.extensions`.
122
+ - `options.basePath` (string): Specifies the base path for the output files.
106
123
 
107
- ### `press.DataPlugin(options)`
124
+ ### `press.PartialsLoaderPlugin(options)`
108
125
 
109
- This plugin loads JSON files from the specified directory and makes them available in the site context.
126
+ An alias of `press.SourcePlugin` that will read all files in the `partials` folder and process them as a partials. The **mikel** tag `{{>file}}` can be used to include the partial in `partials/file.html`.
110
127
 
111
- Options:
112
- - `options.source` (string): Specifies a custom source directory for data files. If not provided, `./data` is used.
128
+ This plugin accepts the following options:
129
+ - `options.folder` (string): To change the directory to load the partials files. Default is `./partials`.
130
+ - `options.extensions` (array): Defines the file extensions that should be processed. If not provided, it will use `config.extensions`.
113
131
 
114
- ### `press.FrontmatterPlugin(options)`
132
+ ### `press.DataLoaderPlugin(options)`
115
133
 
116
- This plugin processes frontmatter in Markdown and HTML files.
134
+ This plugin loads JSON files from the specified directory and makes them available in the site context.
117
135
 
118
136
  Options:
119
- - `options.extensions` (array): Defines the file extensions that should be processed. The default value is `[".md", ".markdown", ".html"]`.
120
- - `options.parser` (function): Frontmatter parser function (e.g., `JSON.parse`, `YAML.load`).
121
-
122
- ### `press.PermalinkPlugin()`
137
+ - `options.folder` (string): Specifies a custom source directory for data files. If not provided, `./data` is used.
123
138
 
124
- This plugin allows defining custom permalinks for pages.
139
+ ### `press.FrontmatterPlugin()`
125
140
 
126
- ### `press.MarkdownPlugin(options)`
141
+ This plugin processes and parses the frontmatter in each file. The parsed frontmatter content will be available in `page.attributes` field.
127
142
 
128
- This plugin processes Markdown files and converts them to HTML.
129
-
130
- Options:
131
- - `options.parser` (function): Markdown parser function (e.g., `marked.parse`).
143
+ ### `press.ContentPagePlugin()`
132
144
 
133
- ### `press.ContentPlugin(options)`
134
-
135
- This plugin processes each page and saves it into `config.destination`. It accepts an `options` object, which is passed to mikel for template processing.
145
+ This plugin processes each page and saves it into `config.destination`.
136
146
 
137
147
  ### `press.CopyAssetsPlugin(options)`
138
148
 
@@ -141,28 +151,14 @@ This plugin copies static files from the source to the destination.
141
151
  Options:
142
152
  - `options.patterns` (array): List of file patterns to copy. Each pattern should have `from` and `to`.
143
153
 
144
- ## Node API
145
-
146
- **mikel-press** provides an API with two main methods:
147
-
148
- ### `press.build(config)`
149
-
150
- Triggers the build of **mikel-press** with the given configuration object provided as an argument.
151
-
152
- ```javascript
153
- import press from "mikel-press";
154
-
155
- press.build(config);
156
- ```
157
-
158
- ### `press.watch(config)`
154
+ ## API
159
155
 
160
- Calling the `watch` method triggers the build, but also watches for changes and rebuilds the site as soon as it detects a change in any of the source files.
156
+ **mikel-press** exposes a single function that triggers the build with the given configuration object provided as an argument.
161
157
 
162
158
  ```javascript
163
159
  import press from "mikel-press";
164
160
 
165
- press.watch(config);
161
+ press({...});
166
162
  ```
167
163
 
168
164
  ## License
package/index.js CHANGED
@@ -1,26 +1,67 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import * as http from "node:http";
4
3
  import mikel from "mikel";
5
4
 
6
- // @description default mime types
7
- const DEFAULT_MIME_TYPES = {
8
- ".css": "text/css",
9
- ".gif": "image/gif",
10
- ".html": "text/html",
11
- ".ico": "image/vnd.microsoft.icon",
12
- ".jpg": "image/jpeg",
13
- ".jpeg": "image/jpeg",
14
- ".js": "text/javascript",
15
- ".json": "application/json",
16
- ".mjs": "text/javascript",
17
- ".png": "image/png",
18
- ".svg": "image/svg+xml",
19
- ".txt": "text/plain",
5
+ // @description press main function
6
+ // @param {Object} config - configuration object
7
+ // @param {String} config.source - source folder
8
+ // @param {String} config.destination - destination folder to save the files
9
+ // @param {Array} config.plugins - list of plugins to apply
10
+ const press = (config = {}) => {
11
+ const {source, destination, plugins, extensions, mikelOptions, ...otherConfig} = config;
12
+ const context = Object.freeze({
13
+ config: otherConfig,
14
+ source: path.resolve(source || "."),
15
+ destination: path.resolve(destination || "./www"),
16
+ extensions: extensions || [".html"],
17
+ template: mikel.create("{{>content}}", mikelOptions || {}),
18
+ plugins: [
19
+ SourcePlugin({folder: ".", label: press.LABEL_PAGE}),
20
+ ...plugins,
21
+ ],
22
+ nodes: [],
23
+ });
24
+ const getPlugins = name => context.plugins.filter(plugin => typeof plugin[name] === "function");
25
+ // 1. load nodes into context
26
+ const nodesPaths = new Set(); // prevent adding duplicated nodes
27
+ getPlugins("load").forEach(plugin => {
28
+ [plugin.load(context) || []].flat().forEach(node => {
29
+ if (nodesPaths.has(node.source)) {
30
+ throw new Error(`File ${node.source} has been already processed by another plugin`);
31
+ }
32
+ context.nodes.push(node);
33
+ nodesPaths.add(node.source);
34
+ });
35
+ });
36
+ // 2. transform nodes
37
+ const transformPlugins = getPlugins("transform");
38
+ context.nodes.forEach((node, _, allNodes) => {
39
+ transformPlugins.forEach(plugin => {
40
+ return plugin.transform(context, node, allNodes);
41
+ });
42
+ });
43
+ // 3. filter nodes and get only the ones that are going to be emitted
44
+ const shouldEmitPlugins = getPlugins("shouldEmit");
45
+ const filteredNodes = context.nodes.filter((node, _, allNodes) => {
46
+ return shouldEmitPlugins.every(plugin => {
47
+ return !!plugin.shouldEmit(context, node, allNodes);
48
+ });
49
+ });
50
+ // 4. before emit
51
+ getPlugins("beforeEmit").forEach(plugin => {
52
+ return plugin.beforeEmit(context);
53
+ });
54
+ // 5. emit each node
55
+ const emitPlugins = getPlugins("emit");
56
+ filteredNodes.forEach((node, _, allNodes) => {
57
+ emitPlugins.forEach(plugin => {
58
+ return plugin.emit(context, node, allNodes);
59
+ });
60
+ });
20
61
  };
21
62
 
22
63
  // @description general utilities
23
- const utils = {
64
+ press.utils = {
24
65
  // @description read a file from disk
25
66
  // @param {String} file path to the file to read
26
67
  read: (file, encoding = "utf8") => {
@@ -49,414 +90,125 @@ const utils = {
49
90
  if (!fs.existsSync(folder) || !fs.statSync(folder).isDirectory()) {
50
91
  return [];
51
92
  }
52
- return fs.readdirSync(folder, "utf8").filter(file => {
53
- return extensions === "*" || extensions.includes(path.extname(file));
54
- });
55
- },
56
- // @description walk through the given folder and get all files
57
- // @params {String} folder folder to walk through
58
- // @params {Array|String} extensions extensions to include. Default: "*"
59
- walkdir: (folder, extensions = "*") => {
60
- const files = [];
61
- const walkSync = currentFolder => {
62
- const fullFolderPath = path.join(folder, currentFolder);
63
- fs.readdirSync(fullFolderPath).forEach(file => {
64
- const filePath = path.join(currentFolder, file);
65
- const fullFilePath = path.join(fullFolderPath, file);
66
- if (fs.statSync(fullFilePath).isDirectory()) {
67
- return walkSync(filePath);
68
- }
69
- if (extensions === "*" || extensions.includes(path.extname(file))) {
70
- files.push(filePath);
71
- }
72
- });
73
- };
74
- walkSync("./");
75
- return files;
93
+ return fs.readdirSync(folder, "utf8")
94
+ .filter(file => extensions === "*" || extensions.includes(path.extname(file)))
95
+ .filter(file => fs.statSync(path.join(folder, file)).isFile());
76
96
  },
77
- // @description watch for file changes
78
- // @param {String} filePath path to the file to watch
79
- // @param {Function} listener method to listen for file changes
80
- watch: (filePath, listener) => {
81
- let lastModifiedTime = null;
82
- fs.watch(filePath, "utf8", () => {
83
- const modifiedTime = fs.statSync(filePath).mtimeMs;
84
- if (lastModifiedTime !== modifiedTime) {
85
- lastModifiedTime = modifiedTime;
86
- return listener(filePath);
87
- }
88
- });
89
- },
90
- // @description change the properties of the given path (dirname, basename, extname)
91
- format: (filePath, options = {}) => {
92
- const dirname = options.dirname || path.dirname(filePath);
93
- const extname = options.extname || path.extname(filePath);
94
- const basename = options.basename || path.basename(filePath, path.extname(filePath));
95
- return path.join(dirname, `${basename}${extname}`);
96
- },
97
- // @description get the mime type from the given extension
98
- getMimeType: (extname = ".txt") => {
99
- return DEFAULT_MIME_TYPES[extname] || "text/plain";
100
- },
101
- };
102
-
103
- // @description add a new node item
104
- const createNode = (source, path, label = "", data = {}) => {
105
- return {source, path, label, data};
106
- };
107
-
108
- // @description get nodes with the specified label
109
- const getNodesByLabel = (nodes, label) => {
110
- return Array.from(nodes).filter(node => node.label === label);
111
- };
112
-
113
- // @description get all nodes to update
114
- const getNodesToUpdate = (graph, affectedNode) => {
115
- const listOfAffectedNodes = new Set();
116
- const walkNodes = currentNode => {
117
- listOfAffectedNodes.add(currentNode);
118
- return graph.forEach(edge => {
119
- if (edge[0] === currentNode && !listOfAffectedNodes.has(edge[1])) {
120
- walkNodes(edge[1]);
121
- }
122
- });
123
- };
124
- walkNodes(affectedNode);
125
- return listOfAffectedNodes;
126
- };
127
-
128
- // @description get plugins with the specified function
129
- const getPlugins = (plugins, functionName) => {
130
- return plugins.filter(plugin => typeof plugin[functionName] === "function");
131
- };
132
-
133
- // create a new context from the provided configuration
134
- const createContext = config => {
135
- const {source, destination, plugins, ...otherConfiguration} = config;
136
- const context = Object.freeze({
137
- config: otherConfiguration,
138
- source: path.resolve(source || "."),
139
- destination: path.resolve(destination || "./www"),
140
- plugins: plugins || [],
141
- nodes: [],
142
- edges: [],
143
- });
144
- // load nodes into context
145
- const nodesPaths = new Set(); // prevent adding duplicated nodes
146
- getPlugins(context.plugins, "load").forEach(plugin => {
147
- const nodes = plugin.load(context) || [];
148
- [nodes].flat().forEach(node => {
149
- const nodeFullPath = path.join(node.source, node.path);
150
- if (nodesPaths.has(nodeFullPath)) {
151
- throw new Error(`File ${nodeFullPath} has been already processed by another plugin`);
152
- }
153
- context.nodes.push(node);
154
- nodesPaths.add(nodeFullPath);
155
- });
156
- });
157
- // generate dependency graph
158
- const edgesPaths = new Set(); // prevent adding duplicated edges
159
- getPlugins(context.plugins, "getDependencyGraph").forEach(plugin => {
160
- (plugin.getDependencyGraph(context) || []).forEach(edge => {
161
- if (!edge.every(node => nodesPaths.has(node))) {
162
- throw new Error(`Dependency graph contains nodes that have not been loaded`);
163
- }
164
- const edgePath = edge.join(" -> ");
165
- if (!edgesPaths.has(edgePath)) {
166
- context.edges.push(edge);
167
- edgesPaths.add(edgePath);
168
- }
169
- });
170
- });
171
- // return context
172
- return context;
173
- };
174
-
175
- // @description build context
176
- const buildContext = (context, nodes = null) => {
177
- const nodesToBuild = (nodes && Array.isArray(nodes)) ? nodes : context.nodes;
178
- // reset nodes path
179
- nodesToBuild.forEach(node => {
180
- node.data.path = node.path;
181
- });
182
- // transform nodes
183
- const transformPlugins = getPlugins(context.plugins, "transform");
184
- nodesToBuild.forEach((node, _, allNodes) => {
185
- transformPlugins.forEach(plugin => {
186
- return plugin.transform(context, node, allNodes);
187
- });
188
- });
189
- // filter nodes and get only the ones that are going to be emitted
190
- const shouldEmitPlugins = getPlugins(context.plugins, "shouldEmit");
191
- const filteredNodes = nodesToBuild.filter((node, _, allNodes) => {
192
- for (let i = 0; i < shouldEmitPlugins.length; i++) {
193
- const plugin = shouldEmitPlugins[i];
194
- if (!plugin.shouldEmit(context, node, allNodes)) {
195
- return false;
196
- }
97
+ // @description frontmatter parser
98
+ // @params {String} content content to parse
99
+ // @params {Function} parser parser function to use
100
+ frontmatter: (content = "", parser = JSON.parse) => {
101
+ const matches = Array.from(content.matchAll(/^(--- *)/gm))
102
+ if (matches?.length === 2 && matches[0].index === 0) {
103
+ return {
104
+ body: content.substring(matches[1].index + matches[1][1].length).trim(),
105
+ attributes: parser(content.substring(matches[0].index + matches[0][1].length, matches[1].index).trim()),
106
+ };
197
107
  }
198
- return true;
199
- });
200
- // emit each node
201
- getPlugins(context.plugins, "emit").forEach(plugin => {
202
- return plugin.emit(context, filteredNodes);
203
- });
204
- };
205
-
206
- // @description start a watch on the current context
207
- const watchContext = context => {
208
- // force to rebuild
209
- const rebuild = changedNodePath => {
210
- const nodesPathsToBuild = getNodesToUpdate(context.edges, changedNodePath);
211
- const nodesToRebuild = context.nodes.filter(node => {
212
- return nodesPathsToBuild.has(path.join(node.source, node.path));
213
- });
214
- // perform the rebuild of the context
215
- buildContext(context, nodesToRebuild);
216
- };
217
- // create a watch for each registered node in the context
218
- context.nodes.forEach(node => {
219
- return utils.watch(path.join(node.source, node.path), rebuild);
220
- });
108
+ return {body: content, attributes: {}};
109
+ },
221
110
  };
222
111
 
223
- // @description start a server on the current context
224
- // @param {Object} context current site context
225
- // @param {Object} options server options
226
- // @param {String} options.port port that the server will listen. Default: "3000"
227
- // @param {Function} options.getMimeType function to obtain the associated mime type from the given extension
228
- const serveContext = (context, options = {}) => {
229
- const port = parseInt(options?.port || "3000");
230
- const getMimeType = options?.getMimeType || utils.getMimeType;
231
- const server = http.createServer((request, response) => {
232
- let responseCode = 200;
233
- let url = path.join(context.destination, path.normalize(request.url));
234
- // check for directory
235
- if (url.endsWith("/") || (fs.existsSync(url) && fs.statSync(url).isDirectory())) {
236
- url = path.join(url, "index.html");
237
- }
238
- // check if we have to append the '.html' extension
239
- if (!fs.existsSync(url) && fs.existsSync(url + ".html")) {
240
- url = url + ".html";
241
- }
242
- // check if the file does not exist
243
- if (!fs.existsSync(url)) {
244
- url = path.join(context.destination, "404.html");
245
- responseCode = 404;
246
- }
247
- // send the file
248
- response.writeHead(responseCode, {
249
- "Content-Type": getMimeType?.(path.extname(url)) || "text/plain",
250
- });
251
- fs.createReadStream(url).pipe(response);
252
- console.log(`[${responseCode}] ${request.method} ${request.url}`);
253
- });
254
- // launch server
255
- server.listen(port);
256
- console.log(`Server running at http://127.0.0.1:${port}/`);
257
- };
112
+ // assign constants
113
+ press.LABEL_PAGE = "page";
114
+ press.LABEL_ASSET = "asset";
115
+ press.LABEL_DATA = "asset/data";
116
+ press.LABEL_PARTIAL = "asset/partial";
258
117
 
259
118
  // @description source plugin
260
- const SourcePlugin = (options = {}) => {
261
- const label = options.label || "pages";
119
+ press.SourcePlugin = (options = {}) => {
262
120
  return {
263
121
  name: "SourcePlugin",
264
122
  load: context => {
265
- const folder = path.resolve(context.source, options.source || "./content");
266
- const nodes = utils.walkdir(folder, options?.extensions || "*").map(file => {
267
- return createNode(folder, file, label);
268
- });
269
- return nodes;
270
- },
271
- transform: (_, node) => {
272
- if (node.label === label) {
273
- node.data.content = utils.read(path.join(node.source, node.path));
274
- }
123
+ const folder = path.join(context.source, options?.folder || ".");
124
+ return press.utils.readdir(folder, options?.extensions || context.extensions).map(file => ({
125
+ source: path.join(folder, file),
126
+ label: options.label || press.LABEL_PAGE,
127
+ path: path.join(options?.basePath || ".", file),
128
+ url: path.normalize("/" + path.join(options?.basePath || ".", file)),
129
+ content: press.utils.read(path.join(folder, file)),
130
+ }));
275
131
  },
276
132
  };
277
133
  };
278
134
 
279
- // @description data plugin
280
- const DataPlugin = (options = {}) => {
281
- const label = options?.label || "asset/data";
282
- return {
283
- name: "DataPlugin",
284
- load: context => {
285
- const folder = path.resolve(context.source, options.source || "./data");
286
- return utils.readdir(folder, [".json"]).map(file => {
287
- return createNode(folder, file, label);
288
- });
289
- },
290
- transform: (_, node) => {
291
- if (node.label === label && path.extname(node.path) === ".json") {
292
- node.data.name = path.basename(node.path, ".json");
293
- node.data.content = JSON.parse(utils.read(path.join(node.source, node.path)));
294
- }
295
- },
296
- shouldEmit: (_, node) => {
297
- return node.label !== label;
298
- },
299
- };
135
+ // @description loader plugins
136
+ press.DataLoaderPlugin = (options = {}) => {
137
+ return SourcePlugin({folder: "./data", extensions: [".json"], label: press.LABEL_DATA, ...options});
138
+ };
139
+ press.PartialsLoaderPlugin = (options = {}) => {
140
+ return SourcePlugin({folder: "./partials", extensions: [".html"], label: press.LABEL_PARTIAL, ...options});
300
141
  };
301
142
 
302
143
  // @description frontmatter plugin
303
- // @params {Object} options options for this plugin
304
- // @params {Array} options.extensions extensions to process. Default: [".md", ".markdown", ".html"]
305
- // @params {Function} options.parser frontmatter parser (JSON.parse, YAML.load)
306
- const FrontmatterPlugin = (options = {}) => {
307
- const extensions = options.extensions || [".md", ".markdown", ".html"];
144
+ press.FrontmatterPlugin = () => {
308
145
  return {
309
146
  name: "FrontmatterPlugin",
310
147
  transform: (_, node) => {
311
- if ((extensions === "*" || extensions.includes(path.extname(node.path))) && typeof node.data.content === "string") {
312
- node.data.attributes = {};
313
- const matches = Array.from(node.data.content.matchAll(/^(--- *)/gm))
314
- if (matches?.length === 2 && matches[0].index === 0) {
315
- const front = node.data.content.substring(0 + matches[0][1].length, matches[1].index).trim();
316
- node.data.content = node.data.content.substring(matches[1].index + matches[1][1].length).trim();
317
- if (typeof options.parser === "function") {
318
- node.data.attributes = options.parser(front);
319
- }
148
+ if (typeof node.content === "string") {
149
+ const result = press.utils.frontmatter(node.content, JSON.parse);
150
+ node.content = result.content;
151
+ node.attributes = result.attributes || {};
152
+ node.title = node.attributes?.title || node.path;
153
+ if (node.attributes.permalink) {
154
+ node.path = node.attributes.permalink;
155
+ node.url = path.normalize("/" + node.path);
320
156
  }
321
157
  }
322
158
  },
323
159
  };
324
160
  };
325
161
 
326
- // @description permalink plugin
327
- const PermalinkPlugin = () => {
328
- return {
329
- name: "PermalinkPlugin",
330
- transform: (_, node) => {
331
- node.data.path = node.data?.attributes?.permalink || node.data.path;
332
- // node.data.url = path.normalize("/" + node.data.path);
333
- },
334
- };
335
- };
336
-
337
- // @description markdown plugin
338
- // @params {Object} options options for this plugin
339
- // @params {Object} options.parser markdown parser (for example marked.parse)
340
- const MarkdownPlugin = (options = {}) => {
162
+ // @description plugin to generate pages content
163
+ press.ContentPagePlugin = (siteData = {}) => {
341
164
  return {
342
- name: "MarkdownPlugin",
343
- transform: (_, node) => {
344
- if (path.extname(node.path) === ".md" || path.extname(node.path) === ".markdown") {
345
- // const marked = new Marked(options);
346
- // getPlugins(context.plugins, "markdownPlugins").forEach(plugin => {
347
- // (plugin.markdownPlugins(context, node) || []).forEach(markedPlugin => {
348
- // marked.use(markedPlugin);
349
- // });
350
- // });
351
- node.data.content = options.parser(node.data.content);
352
- node.data.path = utils.format(node.data.path, {extname: ".html"});
353
- }
354
- },
355
- };
356
- };
357
-
358
- // @description content plugin
359
- const ContentPlugin = (options = {}) => {
360
- const label = options.label || "asset/layout";
361
- const extensions = options.extensions || [".html", ".md", ".markdown"];
362
- return {
363
- name: "ContentPlugin",
364
- load: context => {
365
- const layoutPath = path.resolve(context.source, context.config.layout || options.layout);
366
- return createNode(path.dirname(layoutPath), path.basename(layoutPath), label);
165
+ name: "ContentPagePlugin",
166
+ shouldEmit: (context, node) => {
167
+ return ![press.LABEL_ASSET, press.LABEL_DATA, press.LABEL_PARTIAL].includes(node.label);
367
168
  },
368
- transform: (_, node) => {
369
- if (node.label === label) {
370
- node.data.content = utils.read(path.join(node.source, node.path));
371
- }
372
- },
373
- getDependencyGraph: context => {
374
- const graph = [];
375
- const template = getNodesByLabel(context.nodes, label)[0];
376
- context.nodes.forEach(node => {
377
- if (node.label !== label && extensions.includes(path.extname(node.path))) {
378
- graph.push([
379
- path.join(template.source, template.path),
380
- path.join(node.source, node.path),
381
- ]);
382
- }
383
- });
384
- return graph;
385
- },
386
- shouldEmit: (_, node) => {
387
- return node.label !== label;
388
- },
389
- emit: (context, nodesToEmit) => {
390
- // prepare site data
391
- const siteData = Object.assign({}, context.config, {
392
- data: Object.fromEntries(getNodesByLabel(context.nodes, "asset/data").map(node => {
393
- return [node.data.name, node.data.content];
169
+ beforeEmit: context => {
170
+ const getNodes = label => context.nodes.filter(n => n.label === label);
171
+ // 1. prepare site data
172
+ Object.assign(siteData, context.config, {
173
+ pages: getNodes(press.LABEL_PAGE),
174
+ data: Object.fromEntries(getNodes(press.LABEL_DATA).map(node => {
175
+ return [path.basename(node.path, ".json"), JSON.parse(node.content)];
394
176
  })),
395
- pages: getNodesByLabel(context.nodes, "pages").map(n => n.data),
396
- posts: getNodesByLabel(context.nodes, "posts").map(n => n.data),
177
+ partials: getNodes(press.LABEL_PARTIAL),
178
+ assets: getNodes(press.LABEL_ASSET),
397
179
  });
398
- // get data files
399
- const template = getNodesByLabel(context.nodes, label)[0];
400
- const compiler = mikel.create(template.data.content, options);
401
- nodesToEmit.forEach(node => {
402
- if (extensions.includes(path.extname(node.path))) {
403
- compiler.addPartial("content", node.data.content);
404
- const content = compiler({
405
- site: siteData,
406
- page: node.data,
407
- layout: template.data,
408
- });
409
- // const filePath = utils.format(node.data.path || node.path, {extname: ".html"});
410
- const filePath = node.data?.path || node.path;
411
- utils.write(path.join(context.destination, filePath), content);
412
- }
180
+ // 2. register partials into template
181
+ siteData.partials.forEach(partial => {
182
+ context.template.addPartial(path.basename(partial.path), {
183
+ body: partial.content,
184
+ attributes: partial.attributes || {},
185
+ });
413
186
  });
414
187
  },
188
+ emit: (context, node) => {
189
+ if (node.label === press.LABEL_PAGE && typeof node.content === "string") {
190
+ context.template.use(ctx => {
191
+ ctx.tokens = mikel.tokenize(node.content || "");
192
+ });
193
+ // compile and write the template
194
+ const result = context.template({site: siteData, page: node});
195
+ press.utils.write(path.join(context.destination, node.path), result);
196
+ }
197
+ },
415
198
  };
416
199
  };
417
200
 
418
201
  // @description copy plugin
419
- const CopyAssetsPlugin = (options = {}) => {
202
+ press.CopyAssetsPlugin = (options = {}) => {
420
203
  return {
421
204
  name: "CopyAssetsPlugin",
422
- emit: context => {
423
- (options.patterns || []).forEach(item => {
424
- if (item.from && item.to && fs.existsSync(item.from)) {
425
- utils.copy(item.from, path.join(context.destination, item.to));
426
- }
427
- });
205
+ beforeEmit: context => {
206
+ (options.patterns || [])
207
+ .filter(item => !!item.from && !!item.to && fs.existsSync(item.from))
208
+ .forEach(item => press.utils.copy(item.from, path.join(context.destination, item.to)));
428
209
  },
429
210
  };
430
211
  };
431
212
 
432
- // @description default export of mikel-press
433
- export default {
434
- // @description run mikel-press and generate the static site
435
- // @param {Object} config configuration object
436
- build: config => {
437
- buildContext(createContext(config));
438
- },
439
- // @description watch for changes in the source folder and rebuild the site
440
- // @param {Object} config configuration object
441
- watch: config => {
442
- const context = createContext(config);
443
- buildContext(context, context.nodes);
444
- watchContext(context);
445
- },
446
- // utilities for working with files
447
- utils: utils,
448
- // helpers for working with the context
449
- createNode: createNode,
450
- createContext: createContext,
451
- buildContext: buildContext,
452
- watchContext: watchContext,
453
- serveContext: serveContext,
454
- // plugins
455
- SourcePlugin: SourcePlugin,
456
- DataPlugin: DataPlugin,
457
- MarkdownPlugin: MarkdownPlugin,
458
- FrontmatterPlugin: FrontmatterPlugin,
459
- PermalinkPlugin: PermalinkPlugin,
460
- ContentPlugin: ContentPlugin,
461
- CopyAssetsPlugin: CopyAssetsPlugin,
462
- };
213
+ // export press generator
214
+ 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.19.1",
4
+ "version": "0.20.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": {
@@ -19,7 +19,7 @@
19
19
  "node": ">=20"
20
20
  },
21
21
  "dependencies": {
22
- "mikel": "^0.19.1"
22
+ "mikel": "^0.20.0"
23
23
  },
24
24
  "files": [
25
25
  "README.md",