mikel-press 0.19.0 → 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.
- package/README.md +61 -65
- package/index.js +138 -386
- 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
|
|
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
|
-
| `
|
|
31
|
-
| `
|
|
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
|
-
|
|
38
|
-
|
|
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.
|
|
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.
|
|
76
|
-
| `site.
|
|
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
|
|
117
|
+
This plugin reads content from the specified directory and loads it into the system for processing.
|
|
102
118
|
|
|
103
119
|
Options:
|
|
104
|
-
- `options.
|
|
105
|
-
- `options.extensions` (array): Defines the file extensions that should be processed.
|
|
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.
|
|
124
|
+
### `press.PartialsLoaderPlugin(options)`
|
|
108
125
|
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
- `options.
|
|
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.
|
|
132
|
+
### `press.DataLoaderPlugin(options)`
|
|
115
133
|
|
|
116
|
-
This plugin
|
|
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.
|
|
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
|
-
|
|
139
|
+
### `press.FrontmatterPlugin()`
|
|
125
140
|
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
Options:
|
|
131
|
-
- `options.parser` (function): Markdown parser function (e.g., `marked.parse`).
|
|
143
|
+
### `press.ContentPagePlugin()`
|
|
132
144
|
|
|
133
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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")
|
|
53
|
-
|
|
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
|
|
78
|
-
// @
|
|
79
|
-
// @
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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 (
|
|
312
|
-
node.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
327
|
-
|
|
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: "
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
396
|
-
|
|
177
|
+
partials: getNodes(press.LABEL_PARTIAL),
|
|
178
|
+
assets: getNodes(press.LABEL_ASSET),
|
|
397
179
|
});
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
202
|
+
press.CopyAssetsPlugin = (options = {}) => {
|
|
420
203
|
return {
|
|
421
204
|
name: "CopyAssetsPlugin",
|
|
422
|
-
|
|
423
|
-
(options.patterns || [])
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
22
|
+
"mikel": "^0.20.0"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"README.md",
|