mikel-press 0.19.1 → 0.20.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 +67 -63
  2. package/index.js +160 -378
  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,43 @@ 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.PartialsPlugin(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.DataPlugin(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. This plugin accepts the following options:
117
135
 
118
- 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`).
136
+ - `options.folder` (string): Specifies a custom source directory for data files. If not provided, `./data` is used.
121
137
 
122
- ### `press.PermalinkPlugin()`
138
+ ### `press.AssetsPlugin(options)`
123
139
 
124
- This plugin allows defining custom permalinks for pages.
140
+ This plugin loads additional files (aka assets) and includes them in the build folder. This plugin accepts the following options:
125
141
 
126
- ### `press.MarkdownPlugin(options)`
142
+ - `options.folder` (string): Specifies a custom source directory for assets files. If not provided, `./assets` is used.
143
+ - `options.extensions` (array): Defines the file extensions that should be processed. If not provided, it will use `"*"`.
144
+ - `options.exclude` (array): Defines the list of file names to exclude.
145
+ - `options.basePath` (string): Allows to specify a base path for the output files.
127
146
 
128
- This plugin processes Markdown files and converts them to HTML.
147
+ ### `press.FrontmatterPlugin()`
129
148
 
130
- Options:
131
- - `options.parser` (function): Markdown parser function (e.g., `marked.parse`).
149
+ This plugin processes and parses the frontmatter in each file. The parsed frontmatter content will be available in `page.attributes` field.
132
150
 
133
- ### `press.ContentPlugin(options)`
151
+ ### `press.ContentPagePlugin()`
134
152
 
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.
153
+ This plugin processes each page and saves it into `config.destination`.
136
154
 
137
155
  ### `press.CopyAssetsPlugin(options)`
138
156
 
@@ -141,28 +159,14 @@ This plugin copies static files from the source to the destination.
141
159
  Options:
142
160
  - `options.patterns` (array): List of file patterns to copy. Each pattern should have `from` and `to`.
143
161
 
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)`
162
+ ## API
159
163
 
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.
164
+ **mikel-press** exposes a single function that triggers the build with the given configuration object provided as an argument.
161
165
 
162
166
  ```javascript
163
167
  import press from "mikel-press";
164
168
 
165
- press.watch(config);
169
+ press({...});
166
170
  ```
167
171
 
168
172
  ## License
package/index.js CHANGED
@@ -1,26 +1,68 @@
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, exclude, 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
+ exclude: exclude || ["node_modules", ".git", ".gitignore", ".github"],
18
+ template: mikel.create("{{>content}}", mikelOptions || {}),
19
+ plugins: [
20
+ press.SourcePlugin({folder: ".", label: press.LABEL_PAGE}),
21
+ ...plugins,
22
+ ],
23
+ nodes: [],
24
+ });
25
+ const getPlugins = name => context.plugins.filter(plugin => typeof plugin[name] === "function");
26
+ // 1. load nodes into context
27
+ const nodesPaths = new Set(); // prevent adding duplicated nodes
28
+ getPlugins("load").forEach(plugin => {
29
+ [plugin.load(context) || []].flat().forEach(node => {
30
+ if (nodesPaths.has(node.source)) {
31
+ throw new Error(`File ${node.source} has been already processed by another plugin`);
32
+ }
33
+ context.nodes.push(node);
34
+ nodesPaths.add(node.source);
35
+ });
36
+ });
37
+ // 2. transform nodes
38
+ const transformPlugins = getPlugins("transform");
39
+ context.nodes.forEach((node, _, allNodes) => {
40
+ transformPlugins.forEach(plugin => {
41
+ return plugin.transform(context, node, allNodes);
42
+ });
43
+ });
44
+ // 3. filter nodes and get only the ones that are going to be emitted
45
+ const shouldEmitPlugins = getPlugins("shouldEmit");
46
+ const filteredNodes = context.nodes.filter((node, _, allNodes) => {
47
+ return shouldEmitPlugins.every(plugin => {
48
+ return !!plugin.shouldEmit(context, node, allNodes);
49
+ });
50
+ });
51
+ // 4. before emit
52
+ getPlugins("beforeEmit").forEach(plugin => {
53
+ return plugin.beforeEmit(context);
54
+ });
55
+ // 5. emit each node
56
+ const emitPlugins = getPlugins("emit");
57
+ filteredNodes.forEach((node, _, allNodes) => {
58
+ emitPlugins.forEach(plugin => {
59
+ return plugin.emit(context, node, allNodes);
60
+ });
61
+ });
20
62
  };
21
63
 
22
64
  // @description general utilities
23
- const utils = {
65
+ press.utils = {
24
66
  // @description read a file from disk
25
67
  // @param {String} file path to the file to read
26
68
  read: (file, encoding = "utf8") => {
@@ -45,418 +87,158 @@ const utils = {
45
87
  fs.copyFileSync(source, target);
46
88
  },
47
89
  // @description get all files from the given folder and the given extensions
48
- readdir: (folder, extensions = "*") => {
90
+ readdir: (folder, extensions = "*", exclude = []) => {
49
91
  if (!fs.existsSync(folder) || !fs.statSync(folder).isDirectory()) {
50
92
  return [];
51
93
  }
52
- return fs.readdirSync(folder, "utf8").filter(file => {
53
- return extensions === "*" || extensions.includes(path.extname(file));
54
- });
94
+ return fs.readdirSync(folder, "utf8")
95
+ .filter(file => (extensions === "*" || extensions.includes(path.extname(file))) && !exclude.includes(file))
96
+ .filter(file => fs.statSync(path.join(folder, file)).isFile());
55
97
  },
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;
76
- },
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
- }
98
+ // @description frontmatter parser
99
+ // @params {String} content content to parse
100
+ // @params {Function} parser parser function to use
101
+ frontmatter: (content = "", parser = JSON.parse) => {
102
+ const matches = Array.from(content.matchAll(/^(--- *)/gm))
103
+ if (matches?.length === 2 && matches[0].index === 0) {
104
+ return {
105
+ body: content.substring(matches[1].index + matches[1][1].length).trim(),
106
+ attributes: parser(content.substring(matches[0].index + matches[0][1].length, matches[1].index).trim()),
107
+ };
197
108
  }
198
- return true;
199
- });
200
- // emit each node
201
- getPlugins(context.plugins, "emit").forEach(plugin => {
202
- return plugin.emit(context, filteredNodes);
203
- });
109
+ return {body: content, attributes: {}};
110
+ },
204
111
  };
205
112
 
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
- });
221
- };
222
-
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
- };
113
+ // assign constants
114
+ press.LABEL_PAGE = "page";
115
+ press.LABEL_ASSET = "asset";
116
+ press.LABEL_DATA = "asset/data";
117
+ press.LABEL_PARTIAL = "asset/partial";
258
118
 
259
119
  // @description source plugin
260
- const SourcePlugin = (options = {}) => {
261
- const label = options.label || "pages";
120
+ press.SourcePlugin = (options = {}) => {
262
121
  return {
263
122
  name: "SourcePlugin",
264
123
  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
- }
124
+ const folder = path.join(context.source, options?.folder || ".");
125
+ const extensions = options?.extensions || context.extensions;
126
+ const exclude = options?.exclude || context.exclude;
127
+ return press.utils.readdir(folder, extensions, exclude).map(file => ({
128
+ source: path.join(folder, file),
129
+ label: options.label || press.LABEL_PAGE,
130
+ path: path.join(options?.basePath || ".", file),
131
+ url: path.normalize("/" + path.join(options?.basePath || ".", file)),
132
+ content: press.utils.read(path.join(folder, file)),
133
+ }));
275
134
  },
276
135
  };
277
136
  };
278
137
 
279
138
  // @description data plugin
280
- const DataPlugin = (options = {}) => {
281
- const label = options?.label || "asset/data";
139
+ press.DataPlugin = (options = {}) => {
140
+ return press.SourcePlugin({folder: "./data", extensions: [".json"], label: press.LABEL_DATA, ...options});
141
+ };
142
+
143
+ // @description partials plugin
144
+ press.PartialsPlugin = (options = {}) => {
145
+ return press.SourcePlugin({folder: "./partials", extensions: [".html"], label: press.LABEL_PARTIAL, ...options});
146
+ };
147
+
148
+ // @description assets plugin
149
+ press.AssetsPlugin = (options = {}) => {
282
150
  return {
283
- name: "DataPlugin",
151
+ name: "AssetsPlugin",
284
152
  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
- });
153
+ const folder = path.join(context.source, options?.folder || "./assets");
154
+ return press.utils.readdir(folder, options?.extensions || "*", options?.exclude || context.exclude).map(file => ({
155
+ source: path.join(folder, file),
156
+ label: options.label || press.LABEL_ASSET,
157
+ path: path.join(options?.basePath || ".", file),
158
+ }));
289
159
  },
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)));
160
+ emit: (context, node) => {
161
+ if (node.label === press.LABEL_ASSET && typeof node.content === "string") {
162
+ press.utils.write(path.join(context.destination, node.path), node.content);
163
+ }
164
+ else if (node.label === press.LABEL_ASSET) {
165
+ press.utils.copy(node.source, path.join(context.destination, node.path));
294
166
  }
295
- },
296
- shouldEmit: (_, node) => {
297
- return node.label !== label;
298
167
  },
299
168
  };
300
169
  };
301
170
 
302
171
  // @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"];
172
+ press.FrontmatterPlugin = () => {
308
173
  return {
309
174
  name: "FrontmatterPlugin",
310
175
  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
- }
176
+ if (typeof node.content === "string") {
177
+ const result = press.utils.frontmatter(node.content, JSON.parse);
178
+ node.content = result.body || "";
179
+ node.attributes = result.attributes || {};
180
+ node.title = node.attributes?.title || node.path;
181
+ if (node.attributes.permalink) {
182
+ node.path = node.attributes.permalink;
183
+ node.url = path.normalize("/" + node.path);
320
184
  }
321
185
  }
322
186
  },
323
187
  };
324
188
  };
325
189
 
326
- // @description permalink plugin
327
- const PermalinkPlugin = () => {
190
+ // @description plugin to generate pages content
191
+ press.ContentPagePlugin = (siteData = {}) => {
328
192
  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);
193
+ name: "ContentPagePlugin",
194
+ shouldEmit: (context, node) => {
195
+ return ![press.LABEL_DATA, press.LABEL_PARTIAL].includes(node.label);
333
196
  },
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 = {}) => {
341
- 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);
367
- },
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];
197
+ beforeEmit: context => {
198
+ const getNodes = label => context.nodes.filter(n => n.label === label);
199
+ // 1. prepare site data
200
+ Object.assign(siteData, context.config, {
201
+ pages: getNodes(press.LABEL_PAGE),
202
+ data: Object.fromEntries(getNodes(press.LABEL_DATA).map(node => {
203
+ return [path.basename(node.path, ".json"), JSON.parse(node.content)];
394
204
  })),
395
- pages: getNodesByLabel(context.nodes, "pages").map(n => n.data),
396
- posts: getNodesByLabel(context.nodes, "posts").map(n => n.data),
205
+ partials: getNodes(press.LABEL_PARTIAL),
206
+ assets: getNodes(press.LABEL_ASSET),
397
207
  });
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
- }
208
+ // 2. register partials into template
209
+ siteData.partials.forEach(partial => {
210
+ context.template.addPartial(path.basename(partial.path), {
211
+ body: partial.content || "",
212
+ attributes: partial.attributes || {},
213
+ });
413
214
  });
414
215
  },
216
+ emit: (context, node) => {
217
+ if (node.label === press.LABEL_PAGE && typeof node.content === "string") {
218
+ context.template.use(ctx => {
219
+ ctx.tokens = mikel.tokenize(node.content || "");
220
+ });
221
+ // compile and write the template
222
+ const result = context.template({site: siteData, page: node});
223
+ press.utils.write(path.join(context.destination, node.path), result);
224
+ }
225
+ },
415
226
  };
416
227
  };
417
228
 
418
229
  // @description copy plugin
419
- const CopyAssetsPlugin = (options = {}) => {
230
+ press.CopyAssetsPlugin = (options = {}) => {
420
231
  return {
421
232
  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));
233
+ beforeEmit: context => {
234
+ return (options.patterns || []).forEach(item => {
235
+ if (item.from && fs.existsSync(item.from)) {
236
+ press.utils.copy(item.from, path.join(context.destination, options.basePath || ".", item.to || path.basename(item.from)));
426
237
  }
427
238
  });
428
239
  },
429
240
  };
430
241
  };
431
242
 
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
- };
243
+ // export press generator
244
+ 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.1",
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.1"
23
23
  },
24
24
  "files": [
25
25
  "README.md",