mikel-press 0.2.0 → 0.18.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.
- package/README.md +137 -19
- package/index.js +377 -180
- package/package.json +16 -20
- package/index.d.ts +0 -55
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# mikel-press
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
**mikel-press** is a static site generator inspired by [Jekyll](https://jekyllrb.com/) and built on top of **mikel**, a Mustache-based templating engine. It allows you to generate static websites from HTML and Markdown files using a flexible plugin system.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -16,37 +19,152 @@ Or **npm**:
|
|
|
16
19
|
$ npm install --dev mikel-press
|
|
17
20
|
```
|
|
18
21
|
|
|
19
|
-
##
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
**mikel-press** can be configured using a `config` object that accepts the following options:
|
|
25
|
+
|
|
26
|
+
| Field | Description | Default |
|
|
27
|
+
|-------|-------------|---------|
|
|
28
|
+
| `source` | The path to the directory containing the site's HTML or Markdown files. | `"content"` |
|
|
29
|
+
| `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. | `[]` |
|
|
32
|
+
| `*` | Any other properties passed in config will be available as `site.*` inside each page template. | - |
|
|
33
|
+
|
|
34
|
+
Here is an example configuration object:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const config = {
|
|
38
|
+
source: "./content",
|
|
39
|
+
destination: "./www",
|
|
40
|
+
layout: "./layout.html",
|
|
41
|
+
title: "Hello world",
|
|
42
|
+
description: "My awesome site",
|
|
43
|
+
plugins: [
|
|
44
|
+
press.SourcePlugin(),
|
|
45
|
+
press.FrontmatterPlugin(),
|
|
46
|
+
press.PermalinkPlugin(),
|
|
47
|
+
press.ContentPlugin(),
|
|
48
|
+
press.CopyAssetsPlugin({
|
|
49
|
+
patterns: [
|
|
50
|
+
{ from: "./static/styles.css", to: "static/" },
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
```
|
|
20
56
|
|
|
21
|
-
|
|
57
|
+
## Content
|
|
22
58
|
|
|
23
|
-
|
|
59
|
+
### Variables
|
|
24
60
|
|
|
25
|
-
|
|
26
|
-
$ mikel-press build
|
|
27
|
-
```
|
|
61
|
+
Each HTML file processed by **mikel-press** will be handled by the mikel templating engine, that will provide the following data variables to each page:
|
|
28
62
|
|
|
29
|
-
|
|
63
|
+
#### Global variables
|
|
30
64
|
|
|
31
|
-
|
|
65
|
+
| Variable | Description |
|
|
66
|
+
|----------|-------------|
|
|
67
|
+
| `site` | Contains the site information and all the additional keys provided in the configuration object. |
|
|
68
|
+
| `page` | Specific information about the page that is rendered. |
|
|
69
|
+
| `layout` | Specific information about the layout that is used for renderin the page. |
|
|
32
70
|
|
|
33
|
-
|
|
71
|
+
#### Site variables
|
|
34
72
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
| Variable | Description |
|
|
74
|
+
|----------|-------------|
|
|
75
|
+
| `site.data` | An object containing all data items loaded by `DataPlugin`. |
|
|
76
|
+
| `site.pages` | A list containing all pages processed by **mikel-pres**. |
|
|
77
|
+
| `site.*` | All the additional configuration fields provided in the configuration. |
|
|
78
|
+
|
|
79
|
+
#### Page variables
|
|
80
|
+
|
|
81
|
+
| Variable | Description |
|
|
82
|
+
|----------|-------------|
|
|
83
|
+
| `page.path` | The path to the page. Example: `about/index.html`. |
|
|
84
|
+
| `page.url` | The path to the page including the leading `/`. Example: `/about/index.html`. |
|
|
85
|
+
| `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
|
+
|
|
95
|
+
## Plugins
|
|
96
|
+
|
|
97
|
+
**mikel-press** relies on plugins to handle file reading, transformation, and rendering. The following plugins are built-in:
|
|
98
|
+
|
|
99
|
+
### `press.SourcePlugin(options)`
|
|
100
|
+
|
|
101
|
+
This plugin reads content from the specified `config.source` directory and loads it into the system for processing.
|
|
102
|
+
|
|
103
|
+
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"]`.
|
|
106
|
+
|
|
107
|
+
### `press.DataPlugin(options)`
|
|
108
|
+
|
|
109
|
+
This plugin loads JSON files from the specified directory and makes them available in the site context.
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
- `options.source` (string): Specifies a custom source directory for data files. If not provided, `./data` is used.
|
|
113
|
+
|
|
114
|
+
### `press.FrontmatterPlugin(options)`
|
|
38
115
|
|
|
39
|
-
This
|
|
116
|
+
This plugin processes frontmatter in Markdown and HTML files.
|
|
40
117
|
|
|
41
|
-
|
|
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`).
|
|
42
121
|
|
|
43
|
-
|
|
122
|
+
### `press.PermalinkPlugin()`
|
|
44
123
|
|
|
124
|
+
This plugin allows defining custom permalinks for pages.
|
|
45
125
|
|
|
46
|
-
|
|
126
|
+
### `press.MarkdownPlugin(options)`
|
|
47
127
|
|
|
48
|
-
|
|
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`).
|
|
132
|
+
|
|
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.
|
|
136
|
+
|
|
137
|
+
### `press.CopyAssetsPlugin(options)`
|
|
138
|
+
|
|
139
|
+
This plugin copies static files from the source to the destination.
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
- `options.patterns` (array): List of file patterns to copy. Each pattern should have `from` and `to`.
|
|
143
|
+
|
|
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)`
|
|
159
|
+
|
|
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.
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
import press from "mikel-press";
|
|
164
|
+
|
|
165
|
+
press.watch(config);
|
|
166
|
+
```
|
|
49
167
|
|
|
50
168
|
## License
|
|
51
169
|
|
|
52
|
-
This project is licensed under the MIT License.
|
|
170
|
+
This project is licensed under the [MIT License](../../LICENSE).
|
package/index.js
CHANGED
|
@@ -2,203 +2,400 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import mikel from "mikel";
|
|
4
4
|
|
|
5
|
-
// @description
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
// @description general utilities
|
|
6
|
+
const utils = {
|
|
7
|
+
// @description read a file from disk
|
|
8
|
+
// @param {String} file path to the file to read
|
|
9
|
+
read: (file, encoding = "utf8") => {
|
|
10
|
+
return fs.readFileSync(file, encoding);
|
|
11
|
+
},
|
|
12
|
+
// @description write a file to disk
|
|
13
|
+
// @param {String} file path to the file to save
|
|
14
|
+
// @param {String} content content to save
|
|
15
|
+
write: (file, content = "") => {
|
|
16
|
+
const folder = path.dirname(file);
|
|
17
|
+
if (!fs.existsSync(folder)) {
|
|
18
|
+
fs.mkdirSync(folder, {recursive: true});
|
|
12
19
|
}
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
fs.writeFileSync(file, content, "utf8");
|
|
21
|
+
},
|
|
22
|
+
// @description copy a file
|
|
23
|
+
copy: (source, target) => {
|
|
24
|
+
const folder = path.dirname(target);
|
|
25
|
+
if (!fs.existsSync(folder)) {
|
|
26
|
+
fs.mkdirSync(folder, {recursive: true});
|
|
15
27
|
}
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
fs.copyFileSync(source, target);
|
|
29
|
+
},
|
|
30
|
+
// @description get all files from the given folder and the given extensions
|
|
31
|
+
readdir: (folder, extensions = "*") => {
|
|
32
|
+
if (!fs.existsSync(folder) || !fs.statSync(folder).isDirectory()) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return fs.readdirSync(folder, "utf8").filter(file => {
|
|
36
|
+
return extensions === "*" || extensions.includes(path.extname(file));
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
// @description walk through the given folder and get all files
|
|
40
|
+
// @params {String} folder folder to walk through
|
|
41
|
+
// @params {Array|String} extensions extensions to include. Default: "*"
|
|
42
|
+
walkdir: (folder, extensions = "*") => {
|
|
43
|
+
const files = [];
|
|
44
|
+
const walkSync = currentFolder => {
|
|
45
|
+
fs.readdirSync(currentFolder).forEach(file => {
|
|
46
|
+
const pathToFile = path.join(currentFolder, file);
|
|
47
|
+
if (fs.statSync(pathToFile).isDirectory()) {
|
|
48
|
+
return walkSync(pathToFile);
|
|
49
|
+
}
|
|
50
|
+
if (extensions === "*" || extensions.includes(path.extname(file))) {
|
|
51
|
+
files.push(pathToFile);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
walkSync(folder);
|
|
56
|
+
return files;
|
|
57
|
+
},
|
|
58
|
+
// @description watch for file changes
|
|
59
|
+
// @param {String} filePath path to the file to watch
|
|
60
|
+
// @param {Function} listener method to listen for file changes
|
|
61
|
+
watch: (filePath, listener) => {
|
|
62
|
+
let lastModifiedTime = null;
|
|
63
|
+
fs.watch(filePath, "utf8", () => {
|
|
64
|
+
const modifiedTime = fs.statSync(filePath).mtimeMs;
|
|
65
|
+
if (lastModifiedTime !== modifiedTime) {
|
|
66
|
+
lastModifiedTime = modifiedTime;
|
|
67
|
+
return listener(filePath);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
// @description change the properties of the given path (dirname, basename, extname)
|
|
72
|
+
format: (filePath, options = {}) => {
|
|
73
|
+
const dirname = options.dirname || path.dirname(filePath);
|
|
74
|
+
const extname = options.extname || path.extname(filePath);
|
|
75
|
+
const basename = options.basename || path.basename(filePath, path.extname(filePath));
|
|
76
|
+
return path.join(dirname, `${basename}${extname}`);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// @description add a new node item
|
|
81
|
+
const createNode = (source, path, label = "", data = {}) => {
|
|
82
|
+
return {source, path, label, data};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// @description get nodes with the specified label
|
|
86
|
+
const getNodesByLabel = (nodes, label) => {
|
|
87
|
+
return Array.from(nodes).filter(node => node.label === label);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// @description get all nodes to update
|
|
91
|
+
const getNodesToUpdate = (graph, affectedNode) => {
|
|
92
|
+
const listOfAffectedNodes = new Set();
|
|
93
|
+
const walkNodes = currentNode => {
|
|
94
|
+
listOfAffectedNodes.add(currentNode);
|
|
95
|
+
return graph.forEach(edge => {
|
|
96
|
+
if (edge[0] === currentNode && !listOfAffectedNodes.has(edge[1])) {
|
|
97
|
+
walkNodes(edge[1]);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
walkNodes(affectedNode);
|
|
102
|
+
return listOfAffectedNodes;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// @description get plugins with the specified function
|
|
106
|
+
const getPlugins = (plugins, functionName) => {
|
|
107
|
+
return plugins.filter(plugin => typeof plugin[functionName] === "function");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// create a new context from the provided configuration
|
|
111
|
+
const createContext = config => {
|
|
112
|
+
const {source, destination, plugins, ...otherConfiguration} = config;
|
|
113
|
+
const context = Object.freeze({
|
|
114
|
+
config: otherConfiguration,
|
|
115
|
+
source: path.resolve(source || "."),
|
|
116
|
+
destination: path.resolve(destination || "./www"),
|
|
117
|
+
plugins: plugins || [],
|
|
118
|
+
nodes: [],
|
|
119
|
+
edges: [],
|
|
120
|
+
});
|
|
121
|
+
// load nodes into context
|
|
122
|
+
const nodesPaths = new Set(); // prevent adding duplicated nodes
|
|
123
|
+
getPlugins(context.plugins, "load").forEach(plugin => {
|
|
124
|
+
const nodes = plugin.load(context) || [];
|
|
125
|
+
[nodes].flat().forEach(node => {
|
|
126
|
+
const nodeFullPath = path.join(node.source, node.path);
|
|
127
|
+
if (nodesPaths.has(nodeFullPath)) {
|
|
128
|
+
throw new Error(`File ${nodeFullPath} has been already processed by another plugin`);
|
|
129
|
+
}
|
|
130
|
+
context.nodes.push(node);
|
|
131
|
+
nodesPaths.add(nodeFullPath);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// generate dependency graph
|
|
135
|
+
const edgesPaths = new Set(); // prevent adding duplicated edges
|
|
136
|
+
getPlugins(context.plugins, "getDependencyGraph").forEach(plugin => {
|
|
137
|
+
(plugin.getDependencyGraph(context) || []).forEach(edge => {
|
|
138
|
+
if (!edge.every(node => nodesPaths.has(node))) {
|
|
139
|
+
throw new Error(`Dependency graph contains nodes that have not been loaded`);
|
|
140
|
+
}
|
|
141
|
+
const edgePath = edge.join(" -> ");
|
|
142
|
+
if (!edgesPaths.has(edgePath)) {
|
|
143
|
+
context.edges.push(edge);
|
|
144
|
+
edgesPaths.add(edgePath);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
// return context
|
|
149
|
+
return context;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// @description build context
|
|
153
|
+
const buildContext = (context, nodes = null) => {
|
|
154
|
+
const nodesToBuild = (nodes && Array.isArray(nodes)) ? nodes : context.nodes;
|
|
155
|
+
// reset nodes path
|
|
156
|
+
nodesToBuild.forEach(node => {
|
|
157
|
+
node.data.path = node.path;
|
|
158
|
+
});
|
|
159
|
+
// transform nodes
|
|
160
|
+
const transformPlugins = getPlugins(context.plugins, "transform");
|
|
161
|
+
nodesToBuild.forEach((node, _, allNodes) => {
|
|
162
|
+
transformPlugins.forEach(plugin => {
|
|
163
|
+
return plugin.transform(context, node, allNodes);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
// filter nodes and get only the ones that are going to be emitted
|
|
167
|
+
const shouldEmitPlugins = getPlugins(context.plugins, "shouldEmit");
|
|
168
|
+
const filteredNodes = nodesToBuild.filter((node, _, allNodes) => {
|
|
169
|
+
for (let i = 0; i < shouldEmitPlugins.length; i++) {
|
|
170
|
+
const plugin = shouldEmitPlugins[i];
|
|
171
|
+
if (!plugin.shouldEmit(context, node, allNodes)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
// emit each node
|
|
178
|
+
getPlugins(context.plugins, "emit").forEach(plugin => {
|
|
179
|
+
return plugin.emit(context, filteredNodes);
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// @description start a watch on the current context
|
|
184
|
+
const watchContext = context => {
|
|
185
|
+
// force to rebuild
|
|
186
|
+
const rebuild = changedNodePath => {
|
|
187
|
+
const nodesPathsToBuild = getNodesToUpdate(context.edges, changedNodePath);
|
|
188
|
+
const nodesToRebuild = context.nodes.filter(node => {
|
|
189
|
+
return nodesPathsToBuild.has(path.join(node.source, node.path));
|
|
190
|
+
});
|
|
191
|
+
// perform the rebuild of the context
|
|
192
|
+
buildContext(context, nodesToRebuild);
|
|
193
|
+
};
|
|
194
|
+
// create a watch for each registered node in the context
|
|
195
|
+
context.nodes.forEach(node => {
|
|
196
|
+
return utils.watch(path.join(node.source, node.path), rebuild);
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// @description source plugin
|
|
201
|
+
const SourcePlugin = (options = {}) => {
|
|
202
|
+
const label = options.label || "pages";
|
|
203
|
+
return {
|
|
204
|
+
name: "SourcePlugin",
|
|
205
|
+
load: context => {
|
|
206
|
+
const folder = path.resolve(context.source, options.source || "./content");
|
|
207
|
+
const nodes = utils.walkdir(folder, options?.extensions || "*").map(file => {
|
|
208
|
+
return createNode(folder, file, label);
|
|
209
|
+
});
|
|
210
|
+
return nodes;
|
|
211
|
+
},
|
|
212
|
+
transform: (_, node) => {
|
|
213
|
+
if (node.label === label) {
|
|
214
|
+
node.data.content = utils.read(path.join(node.source, node.path));
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// @description data plugin
|
|
221
|
+
const DataPlugin = (options = {}) => {
|
|
222
|
+
const label = options?.label || "asset/data";
|
|
223
|
+
return {
|
|
224
|
+
name: "DataPlugin",
|
|
225
|
+
load: context => {
|
|
226
|
+
const folder = path.resolve(context.source, options.source || "./data");
|
|
227
|
+
return utils.readdir(folder, [".json"]).map(file => {
|
|
228
|
+
return createNode(folder, file, label);
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
transform: (_, node) => {
|
|
232
|
+
if (node.label === label && path.extname(node.path) === ".json") {
|
|
233
|
+
node.data.name = path.basename(node.path, ".json");
|
|
234
|
+
node.data.content = JSON.parse(utils.read(path.join(node.source, node.path)));
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
shouldEmit: (_, node) => {
|
|
238
|
+
return node.label !== label;
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// @description frontmatter plugin
|
|
244
|
+
// @params {Object} options options for this plugin
|
|
245
|
+
// @params {Array} options.extensions extensions to process. Default: [".md", ".markdown", ".html"]
|
|
246
|
+
// @params {Function} options.parser frontmatter parser (JSON.parse, YAML.load)
|
|
247
|
+
const FrontmatterPlugin = (options = {}) => {
|
|
248
|
+
const extensions = options.extensions || [".md", ".markdown", ".html"];
|
|
53
249
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
250
|
+
name: "FrontmatterPlugin",
|
|
251
|
+
transform: (_, node) => {
|
|
252
|
+
if ((extensions === "*" || extensions.includes(path.extname(node.path))) && typeof node.data.content === "string") {
|
|
253
|
+
node.data.attributes = {};
|
|
254
|
+
const matches = Array.from(node.data.content.matchAll(/^(--- *)/gm))
|
|
255
|
+
if (matches?.length === 2 && matches[0].index === 0) {
|
|
256
|
+
const front = node.data.content.substring(0 + matches[0][1].length, matches[1].index).trim();
|
|
257
|
+
node.data.content = node.data.content.substring(matches[1].index + matches[1][1].length).trim();
|
|
258
|
+
if (typeof options.parser === "function") {
|
|
259
|
+
node.data.attributes = options.parser(front);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
57
264
|
};
|
|
58
265
|
};
|
|
59
266
|
|
|
60
|
-
// @description
|
|
61
|
-
const
|
|
62
|
-
const content = options.content || fs.readFileSync(options.file, "utf8");
|
|
63
|
-
const extname = options.extname || path.extname(options.file || "") || ".html";
|
|
64
|
-
const basename = options.basename || path.basename(options.file || "", extname) || "virtual";
|
|
65
|
-
const {body, attributes} = typeof options?.frontmatter == "function" ? options.frontmatter(content) : {body: content, attributes: {}};
|
|
267
|
+
// @description permalink plugin
|
|
268
|
+
const PermalinkPlugin = () => {
|
|
66
269
|
return {
|
|
67
|
-
name:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
attributes: attributes || {},
|
|
73
|
-
content: typeof options.transform === "function" ? options.transform(body) : body,
|
|
270
|
+
name: "PermalinkPlugin",
|
|
271
|
+
transform: (_, node) => {
|
|
272
|
+
node.data.path = node.data?.attributes?.permalink || node.data.path;
|
|
273
|
+
// node.data.url = path.normalize("/" + node.data.path);
|
|
274
|
+
},
|
|
74
275
|
};
|
|
75
276
|
};
|
|
76
277
|
|
|
77
|
-
// @description
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
278
|
+
// @description markdown plugin
|
|
279
|
+
// @params {Object} options options for this plugin
|
|
280
|
+
// @params {Object} options.parser markdown parser (for example marked.parse)
|
|
281
|
+
const MarkdownPlugin = (options = {}) => {
|
|
282
|
+
return {
|
|
283
|
+
name: "MarkdownPlugin",
|
|
284
|
+
transform: (_, node) => {
|
|
285
|
+
if (path.extname(node.path) === ".md" || path.extname(node.path) === ".markdown") {
|
|
286
|
+
// const marked = new Marked(options);
|
|
287
|
+
// getPlugins(context.plugins, "markdownPlugins").forEach(plugin => {
|
|
288
|
+
// (plugin.markdownPlugins(context, node) || []).forEach(markedPlugin => {
|
|
289
|
+
// marked.use(markedPlugin);
|
|
290
|
+
// });
|
|
291
|
+
// });
|
|
292
|
+
node.data.content = options.parser(node.data.content);
|
|
293
|
+
node.data.path = utils.format(node.data.path, {extname: ".html"});
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// @description content plugin
|
|
300
|
+
const ContentPlugin = (options = {}) => {
|
|
301
|
+
const label = options.label || "asset/layout";
|
|
302
|
+
const extensions = options.extensions || [".html", ".md", ".markdown"];
|
|
303
|
+
return {
|
|
304
|
+
name: "ContentPlugin",
|
|
305
|
+
load: context => {
|
|
306
|
+
return createNode(context.source, context.config.layout || options.layout, label);
|
|
307
|
+
},
|
|
308
|
+
transform: (_, node) => {
|
|
309
|
+
if (node.label === label) {
|
|
310
|
+
node.data.content = utils.read(path.join(node.source, node.path));
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
getDependencyGraph: context => {
|
|
314
|
+
const graph = [];
|
|
315
|
+
const template = getNodesByLabel(context.nodes, label)[0];
|
|
316
|
+
context.nodes.forEach(node => {
|
|
317
|
+
if (node.label !== label && extensions.includes(path.extname(node.path))) {
|
|
318
|
+
graph.push([
|
|
319
|
+
path.join(template.source, template.path),
|
|
320
|
+
path.join(node.source, node.path),
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
90
323
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
324
|
+
return graph;
|
|
325
|
+
},
|
|
326
|
+
shouldEmit: (_, node) => {
|
|
327
|
+
return node.label !== label;
|
|
328
|
+
},
|
|
329
|
+
emit: (context, nodesToEmit) => {
|
|
330
|
+
// prepare site data
|
|
331
|
+
const siteData = Object.assign({}, context.config, {
|
|
332
|
+
data: Object.fromEntries(getNodesByLabel(context.nodes, "asset/data").map(node => {
|
|
333
|
+
return [node.data.name, node.data.content];
|
|
334
|
+
})),
|
|
335
|
+
pages: getNodesByLabel(context.nodes, "pages").map(n => n.data),
|
|
336
|
+
posts: getNodesByLabel(context.nodes, "posts").map(n => n.data),
|
|
103
337
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
338
|
+
// get data files
|
|
339
|
+
const template = getNodesByLabel(context.nodes, label)[0];
|
|
340
|
+
const compiler = mikel.create(template.data.content, options);
|
|
341
|
+
nodesToEmit.forEach(node => {
|
|
342
|
+
if (extensions.includes(path.extname(node.path))) {
|
|
343
|
+
compiler.addPartial("content", node.data.content);
|
|
344
|
+
const content = compiler({
|
|
345
|
+
site: siteData,
|
|
346
|
+
page: node.data,
|
|
347
|
+
layout: template.data,
|
|
348
|
+
});
|
|
349
|
+
// const filePath = utils.format(node.data.path || node.path, {extname: ".html"});
|
|
350
|
+
const filePath = node.data?.path || node.path;
|
|
351
|
+
utils.write(path.join(context.destination, filePath), content);
|
|
352
|
+
}
|
|
119
353
|
});
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
return {};
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// @description run mikel press with the provided configuration
|
|
126
|
-
const run = (config = {}) => {
|
|
127
|
-
// 0. initialize context object
|
|
128
|
-
const hooks = ["initialize", "compiler", "beforeEmit", "emitPage", "emitAsset", "done"];
|
|
129
|
-
const context = {
|
|
130
|
-
site: config || {},
|
|
131
|
-
source: path.resolve(process.cwd(), config?.source || "."),
|
|
132
|
-
destination: path.resolve(process.cwd(), config?.destination || "./www"),
|
|
133
|
-
layout: getLayoutContent(config),
|
|
134
|
-
hooks: Object.freeze(Object.fromEntries(hooks.map(name => {
|
|
135
|
-
return [name, new Set()];
|
|
136
|
-
}))),
|
|
354
|
+
},
|
|
137
355
|
};
|
|
138
|
-
const dispatch = (name, args) => Array.from(context.hooks[name]).forEach(fn => fn.apply(null, args));
|
|
139
|
-
// 1. execute plugins
|
|
140
|
-
if (config?.plugins && Array.isArray(config?.plugins)) {
|
|
141
|
-
config.plugins.forEach(plugin => plugin(context));
|
|
142
|
-
}
|
|
143
|
-
dispatch("initialize", []);
|
|
144
|
-
// 2. initialize mikel instance
|
|
145
|
-
const compiler = mikel.create(context.layout.content, config?.mikel || {});
|
|
146
|
-
dispatch("compiler", [context.compiler]);
|
|
147
|
-
// 3. read stuff
|
|
148
|
-
context.site.data = readData(path.join(context.source, config?.dataDir || "data"));
|
|
149
|
-
context.site.pages = readPages(path.join(context.source, config?.pagesDir || "pages"), ".html", config?.frontmatter, c => c);
|
|
150
|
-
context.site.assets = readAssets(path.join(context.source, config?.assetsDir || "assets"), config?.frontmatter);
|
|
151
|
-
dispatch("beforeEmit", []);
|
|
152
|
-
// 4. save pages
|
|
153
|
-
context.site.pages.forEach(page => {
|
|
154
|
-
compiler.addPartial("content", page.content); // register page content as partial
|
|
155
|
-
const content = compiler({
|
|
156
|
-
site: context.site,
|
|
157
|
-
layout: context.layout,
|
|
158
|
-
page: page,
|
|
159
|
-
});
|
|
160
|
-
dispatch("emitPage", [page, content]);
|
|
161
|
-
saveFile(path.join(context.destination, page.url), content);
|
|
162
|
-
});
|
|
163
|
-
// 5. save assets
|
|
164
|
-
Object.values(context.site.assets).forEach(asset => {
|
|
165
|
-
dispatch("emitAsset", [asset]);
|
|
166
|
-
saveFile(path.join(context.destination, asset.url), asset.content);
|
|
167
|
-
});
|
|
168
|
-
dispatch("done", []);
|
|
169
356
|
};
|
|
170
357
|
|
|
171
|
-
//
|
|
172
|
-
const
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
358
|
+
// @description copy plugin
|
|
359
|
+
const CopyAssetsPlugin = (options = {}) => {
|
|
360
|
+
return {
|
|
361
|
+
name: "CopyAssetsPlugin",
|
|
362
|
+
emit: context => {
|
|
363
|
+
(options.patterns || []).forEach(item => {
|
|
364
|
+
if (item.from && item.to && fs.existsSync(item.from)) {
|
|
365
|
+
utils.copy(item.from, path.join(context.destination, item.to));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
},
|
|
179
369
|
};
|
|
180
370
|
};
|
|
181
371
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
context
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
372
|
+
// @description default export of mikel-press
|
|
373
|
+
export default {
|
|
374
|
+
// @description run mikel-press and generate the static site
|
|
375
|
+
// @param {Object} config configuration object
|
|
376
|
+
build: config => {
|
|
377
|
+
buildContext(createContext(config));
|
|
378
|
+
},
|
|
379
|
+
// @description watch for changes in the source folder and rebuild the site
|
|
380
|
+
// @param {Object} config configuration object
|
|
381
|
+
watch: config => {
|
|
382
|
+
const context = createContext(config);
|
|
383
|
+
buildContext(context, context.nodes);
|
|
384
|
+
watchContext(context);
|
|
385
|
+
},
|
|
386
|
+
// utilities for working with files
|
|
387
|
+
utils: utils,
|
|
388
|
+
// helpers for working with the context
|
|
389
|
+
createNode: createNode,
|
|
390
|
+
createContext: createContext,
|
|
391
|
+
buildContext: buildContext,
|
|
392
|
+
watchContext: watchContext,
|
|
393
|
+
// plugins
|
|
394
|
+
SourcePlugin: SourcePlugin,
|
|
395
|
+
DataPlugin: DataPlugin,
|
|
396
|
+
MarkdownPlugin: MarkdownPlugin,
|
|
397
|
+
FrontmatterPlugin: FrontmatterPlugin,
|
|
398
|
+
PermalinkPlugin: PermalinkPlugin,
|
|
399
|
+
ContentPlugin: ContentPlugin,
|
|
400
|
+
CopyAssetsPlugin: CopyAssetsPlugin,
|
|
201
401
|
};
|
|
202
|
-
|
|
203
|
-
// export
|
|
204
|
-
export default {run, createVirtualPage, frontmatter, postsPlugin, progressPlugin};
|
package/package.json
CHANGED
|
@@ -1,40 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikel-press",
|
|
3
|
-
"description": "A
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "A tiny and fast static site generator based on mikel templating",
|
|
4
|
+
"version": "0.18.1",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"author": {
|
|
7
8
|
"name": "Josemi Juanes",
|
|
8
9
|
"email": "hello@josemi.xyz"
|
|
9
10
|
},
|
|
10
|
-
"license": "MIT",
|
|
11
11
|
"repository": "https://github.com/jmjuanes/mikel",
|
|
12
12
|
"bugs": "https://github.com/jmjuanes/mikel/issues",
|
|
13
|
-
"main": "index.js",
|
|
14
|
-
"module": "index.js",
|
|
15
|
-
"types": "index.d.ts",
|
|
16
13
|
"exports": {
|
|
17
14
|
".": "./index.js",
|
|
18
15
|
"./index.js": "./index.js",
|
|
19
16
|
"./package.json": "./package.json"
|
|
20
17
|
},
|
|
21
|
-
"
|
|
22
|
-
"
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": "20 || >=22"
|
|
23
20
|
},
|
|
24
|
-
"keywords": [
|
|
25
|
-
"static",
|
|
26
|
-
"site",
|
|
27
|
-
"generator",
|
|
28
|
-
"mikel",
|
|
29
|
-
"template",
|
|
30
|
-
"templating"
|
|
31
|
-
],
|
|
32
21
|
"dependencies": {
|
|
33
|
-
"mikel": "^0.
|
|
22
|
+
"mikel": "^0.18.1"
|
|
34
23
|
},
|
|
35
24
|
"files": [
|
|
36
25
|
"README.md",
|
|
37
|
-
"index.js"
|
|
38
|
-
|
|
26
|
+
"index.js"
|
|
27
|
+
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"static-site",
|
|
30
|
+
"site-generator",
|
|
31
|
+
"digital-garden",
|
|
32
|
+
"markdown",
|
|
33
|
+
"blog",
|
|
34
|
+
"mikel"
|
|
39
35
|
]
|
|
40
|
-
}
|
|
36
|
+
}
|
package/index.d.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
interface MikelTemplateOptions {
|
|
2
|
-
functions: {[key: string]: (args: any) => string};
|
|
3
|
-
helpers: {[key: string]: (args: any) => string};
|
|
4
|
-
partials: {[key: string]: string};
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
interface FrontmatterResult {
|
|
8
|
-
body: string;
|
|
9
|
-
attributes: any;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface VirtualPageOptions {
|
|
13
|
-
content?: string;
|
|
14
|
-
file?: string;
|
|
15
|
-
extname?: string;
|
|
16
|
-
basename?: string;
|
|
17
|
-
frontmatter?: (str: string) => FrontmatterResult;
|
|
18
|
-
transform?: (str: string) => string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface VirtualPage {
|
|
22
|
-
content: string;
|
|
23
|
-
attributes: any;
|
|
24
|
-
name: string;
|
|
25
|
-
extname: string;
|
|
26
|
-
basename: string;
|
|
27
|
-
url: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface PostsPluginOptions {
|
|
31
|
-
dir: string;
|
|
32
|
-
parser: (str: string) => string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface SiteConfig {
|
|
36
|
-
source: string;
|
|
37
|
-
destination: string;
|
|
38
|
-
layout: string;
|
|
39
|
-
layoutContent: string;
|
|
40
|
-
dataDir: string;
|
|
41
|
-
pagesDir: string;
|
|
42
|
-
assetsDir: string;
|
|
43
|
-
frontmatter: (str: string) => FrontmatterResult;
|
|
44
|
-
mikel: Partial<MikelTemplateOptions>;
|
|
45
|
-
plugins: any[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
declare module "mikel-press" {
|
|
49
|
-
export function frontmatter(str: string): FrontmatterResult;
|
|
50
|
-
export function createVirtualPage(options: VirtualPageOptions): VirtualPage;
|
|
51
|
-
export function run(config: Partial<SiteConfig>): void;
|
|
52
|
-
|
|
53
|
-
export function postsPlugin(options: PostsPluginOptions): any;
|
|
54
|
-
export function progressPlugin(): any;
|
|
55
|
-
}
|