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.
- package/README.md +67 -63
- package/index.js +160 -378
- 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,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
|
|
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.PartialsPlugin(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.DataPlugin(options)`
|
|
115
133
|
|
|
116
|
-
This plugin
|
|
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
|
-
|
|
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.
|
|
138
|
+
### `press.AssetsPlugin(options)`
|
|
123
139
|
|
|
124
|
-
This plugin
|
|
140
|
+
This plugin loads additional files (aka assets) and includes them in the build folder. This plugin accepts the following options:
|
|
125
141
|
|
|
126
|
-
|
|
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
|
-
|
|
147
|
+
### `press.FrontmatterPlugin()`
|
|
129
148
|
|
|
130
|
-
|
|
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.
|
|
151
|
+
### `press.ContentPagePlugin()`
|
|
134
152
|
|
|
135
|
-
This plugin processes each page and saves it into `config.destination`.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
|
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, 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
|
-
|
|
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")
|
|
53
|
-
|
|
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
|
|
57
|
-
// @params {String}
|
|
58
|
-
// @params {
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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.
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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: "
|
|
151
|
+
name: "AssetsPlugin",
|
|
284
152
|
load: context => {
|
|
285
|
-
const folder = path.
|
|
286
|
-
return utils.readdir(folder,
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
if (node.label ===
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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 (
|
|
312
|
-
node.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
327
|
-
|
|
190
|
+
// @description plugin to generate pages content
|
|
191
|
+
press.ContentPagePlugin = (siteData = {}) => {
|
|
328
192
|
return {
|
|
329
|
-
name: "
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
396
|
-
|
|
205
|
+
partials: getNodes(press.LABEL_PARTIAL),
|
|
206
|
+
assets: getNodes(press.LABEL_ASSET),
|
|
397
207
|
});
|
|
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
|
-
}
|
|
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
|
-
|
|
230
|
+
press.CopyAssetsPlugin = (options = {}) => {
|
|
420
231
|
return {
|
|
421
232
|
name: "CopyAssetsPlugin",
|
|
422
|
-
|
|
423
|
-
(options.patterns || []).forEach(item => {
|
|
424
|
-
if (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
|
-
//
|
|
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.
|
|
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.
|
|
22
|
+
"mikel": "^0.20.1"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"README.md",
|