mikel-press 0.1.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.
Files changed (4) hide show
  1. package/README.md +137 -19
  2. package/index.js +380 -143
  3. package/package.json +16 -20
  4. package/index.d.ts +0 -50
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # mikel-press
2
2
 
3
- **mikel-press** is a tiny static site generator inspired by Jekyll that uses **mikel** as its templating engine. It provides a simple and efficient way to build static websites with customizable layouts and content.
3
+ ![npm version](https://badgen.net/npm/v/mikel-press?labelColor=1d2734&color=21bf81)
4
+ ![license](https://badgen.net/github/license/jmjuanes/mikel?labelColor=1d2734&color=21bf81)
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
- ## Usage
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
- ### Building the Site
57
+ ## Content
22
58
 
23
- Run the `build` command to generate your static website:
59
+ ### Variables
24
60
 
25
- ```bash
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
- The generated files will be available in the `www` directory by default.
63
+ #### Global variables
30
64
 
31
- ### (WIP) Starting a Development Server
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
- To preview your site locally, use the `serve` command:
71
+ #### Site variables
34
72
 
35
- ```bash
36
- mikel-press serve
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 will start a local server at `http://localhost:4000`.
116
+ This plugin processes frontmatter in Markdown and HTML files.
40
117
 
41
- ## (WIP) Configuration
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
- Create a file called `press.config.js` with the configuration.
122
+ ### `press.PermalinkPlugin()`
44
123
 
124
+ This plugin allows defining custom permalinks for pages.
45
125
 
46
- ## Contributing
126
+ ### `press.MarkdownPlugin(options)`
47
127
 
48
- Contributions are welcome! If you have ideas or find a bug, feel free to open an issue or submit a pull request.
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,163 +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 tiny front-matter parser
6
- const frontmatter = (str = "", options = {}) => {
7
- let body = (str || "").trim(), attributes = {};
8
- if (!!options && typeof options === "object") {
9
- const matches = Array.from(body.matchAll(new RegExp("^(" + (options.separator || "---") + " *)", "gm")));
10
- if (matches?.length === 2 && matches[0].index === 0) {
11
- const front = body.substring(0 + matches[0][1].length, matches[1].index).trim();
12
- body = body.substring(matches[1].index + matches[1][1].length).trim();
13
- attributes = typeof options.parse === "function" ? options.parse(front) : front;
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});
14
19
  }
15
- }
16
- return {body, attributes};
17
- };
18
-
19
- // @description utility to save a file to disk
20
- const saveFile = (filePath, fileContent) => {
21
- const folder = path.dirname(filePath);
22
- if (!fs.existsSync(folder)) {
23
- fs.mkdirSync(folder, {recursive: true});
24
- }
25
- fs.writeFileSync(filePath, fileContent, "utf8");
26
- };
27
-
28
- // @description returns the layout content from the given options
29
- const getLayoutContent = config => {
30
- let content = "";
31
- // using options.template to specify the the absolute path to the template file
32
- if (typeof config?.layout === "string" || typeof config?.template === "string") {
33
- content = fs.readFileSync(config.layout || config.template, "utf8");
34
- }
35
- // using templateContent to specify the string content of the template
36
- if (typeof config?.layoutContent === "string" || typeof config?.templateContent === "string") {
37
- content = config.layoutContent || config.templateContent;
38
- }
39
- // parse with frontmatter
40
- const {body, attributes} = frontmatter(content, config.frontmatter);
41
- return {
42
- content: body,
43
- data: attributes || {},
44
- };
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});
27
+ }
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
+ },
45
78
  };
46
79
 
47
- // @description create a virtual page object from the given options
48
- const createVirtualPage = (options = {}) => {
49
- const content = options.content || fs.readFileSync(options.file, "utf8");
50
- const extname = options.extname || path.extname(options.file || "") || ".html";
51
- const basename = options.basename || path.basename(options.file || "", extname) || "virtual";
52
- const {body, attributes} = frontmatter(content, options.frontmatter);
53
- return {
54
- name: basename + extname,
55
- basename: basename,
56
- extname: extname,
57
- url: options.url || attributes?.permalink || path.join("/", basename + extname),
58
- data: attributes || {}, // DEPRECATED
59
- attributes: attributes || {},
60
- content: typeof options.parse === "function" ? options.parse(body) : body,
61
- };
80
+ // @description add a new node item
81
+ const createNode = (source, path, label = "", data = {}) => {
82
+ return {source, path, label, data};
62
83
  };
63
84
 
64
- // @description get pages from input folder
65
- const readPages = (folder, extensions = ".html", fm = null, parse = null) => {
66
- const extensionsList = new Set([extensions].flat());
67
- return fs.readdirSync(folder, "utf8")
68
- .filter(file => extensionsList.has(path.extname(file)))
69
- .map(file => {
70
- return createVirtualPage({
71
- file: path.join(folder, file),
72
- frontmatter: fm,
73
- parse: parse,
74
- extname: ".html",
75
- });
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
+ }
76
99
  });
100
+ };
101
+ walkNodes(affectedNode);
102
+ return listOfAffectedNodes;
77
103
  };
78
104
 
79
- // @description get assets
80
- const readAssets = (folder, fm = null) => {
81
- const assetPaths = fs.readdirSync(folder, "utf8");
82
- return Object.fromEntries(assetPaths.map(file => {
83
- const asset = createVirtualPage({
84
- file: path.join(folder, file),
85
- frontmatter: fm,
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);
86
132
  });
87
- const assetName = asset.basename.replaceAll(".", "_").replaceAll("-", "_");
88
- return [assetName, asset];
89
- }));
90
- };
91
-
92
- // @description read a data folder
93
- const readData = folder => {
94
- const files = fs.readdirSync(folder, "utf8")
95
- .filter(file => path.extname(file) === ".json")
96
- .map(file => path.join(folder, file))
97
- .map(file => {
98
- return [path.basename(file, ".json"), JSON.parse(fs.readFileSync(file, "utf8"))];
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
+ }
99
146
  });
100
- return Object.fromEntries(files);
101
- };
102
-
103
- // @description plugins
104
- const plugins = {
105
- // plugin to read and include posts in markdown
106
- posts: (options = {}) => {
107
- return context => {
108
- context.hooks.beforeEmit.add(() => {
109
- const posts = readPages(path.join(context.source, options?.dir || "posts"), [".md"], context.site.frontmatter, options?.parser);
110
- context.site.posts = posts; // posts will be accesible in site.posts
111
- context.site.pages = [...context.site.pages, ...posts]; // posts will be included as pages also
112
- });
113
- };
114
- },
147
+ });
148
+ // return context
149
+ return context;
115
150
  };
116
151
 
117
- // @description run mikel press with the provided configuration
118
- const run = (config = {}) => {
119
- // 0. initialize context object
120
- const hooks = ["initialize", "compiler", "beforeEmit", "emitPage", "emitAsset", "done"];
121
- const context = {
122
- site: config || {},
123
- source: path.resolve(process.cwd(), config?.source || "."),
124
- destination: path.resolve(process.cwd(), config?.destination || "./www"),
125
- layout: getLayoutContent(config),
126
- hooks: Object.freeze(Object.fromEntries(hooks.map(name => {
127
- return [name, new Set()];
128
- }))),
129
- };
130
- const dispatch = (name, args) => Array.from(context.hooks[name]).forEach(fn => fn.apply(null, args));
131
- // 1. execute plugins
132
- if (config?.plugins && Array.isArray(config?.plugins)) {
133
- config.plugins.forEach(plugin => plugin(context));
134
- }
135
- dispatch("initialize", []);
136
- // 2. initialize mikel instance
137
- const compiler = mikel.create(context.layout.content, config?.mikel || {});
138
- dispatch("compiler", [context.compiler]);
139
- // 3. read stuff
140
- context.site.data = readData(path.join(context.source, config?.dataDir || "data"));
141
- context.site.pages = readPages(path.join(context.source, config?.pagesDir || "pages"), ".html", config?.frontmatter, c => c);
142
- context.site.assets = readAssets(path.join(context.source, config?.assetsDir || "assets"), config?.frontmatter);
143
- dispatch("beforeEmit", []);
144
- // 4. save pages
145
- context.site.pages.forEach(page => {
146
- compiler.addPartial("content", page.content); // register page content as partial
147
- const content = compiler({
148
- site: context.site,
149
- layout: context.layout,
150
- page: page,
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);
151
164
  });
152
- dispatch("emitPage", [page, content]);
153
- saveFile(path.join(context.destination, page.url), content);
154
165
  });
155
- // 5. save assets
156
- Object.values(context.site.assets).forEach(asset => {
157
- dispatch("emitAsset", [asset]);
158
- saveFile(path.join(context.destination, asset.url), asset.content);
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);
159
197
  });
160
- dispatch("done", []);
161
198
  };
162
199
 
163
- // export
164
- export default {run, createVirtualPage, frontmatter, plugins};
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"];
249
+ return {
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
+ },
264
+ };
265
+ };
266
+
267
+ // @description permalink plugin
268
+ const PermalinkPlugin = () => {
269
+ return {
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
+ },
275
+ };
276
+ };
277
+
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
+ }
323
+ });
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),
337
+ });
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
+ }
353
+ });
354
+ },
355
+ };
356
+ };
357
+
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
+ },
369
+ };
370
+ };
371
+
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,
401
+ };
package/package.json CHANGED
@@ -1,40 +1,36 @@
1
1
  {
2
2
  "name": "mikel-press",
3
- "description": "A minimal static site generator based on mikel templating",
4
- "version": "0.1.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
- "scripts": {
22
- "test": "node test.js"
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.17.0"
22
+ "mikel": "^0.18.1"
34
23
  },
35
24
  "files": [
36
25
  "README.md",
37
- "index.js",
38
- "index.d.ts"
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,50 +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 FrontmatterOptions {
8
- separator?: string;
9
- parse?: (front: string) => any;
10
- }
11
-
12
- interface FrontmatterResult {
13
- body: string;
14
- attributes: any;
15
- }
16
-
17
- interface VirtualPageOptions {
18
- content?: string;
19
- file?: string;
20
- extname?: string;
21
- basename?: string;
22
- }
23
-
24
- interface VirtualPage {
25
- content: string,
26
- attributes: any,
27
- name: string,
28
- extname: string,
29
- basename: string,
30
- url: string,
31
- }
32
-
33
- interface SiteConfig {
34
- source: string;
35
- destination: string;
36
- layout: string;
37
- layoutContent: string;
38
- dataDir: string;
39
- pagesDir: string;
40
- assetsDir: string;
41
- frontmatter: Partial<FrontmatterOptions>;
42
- mikel: Partial<MikelTemplateOptions>;
43
- plugins: any[];
44
- }
45
-
46
- declare module "mikel-press" {
47
- export function frontmatter(str: string, options?: FrontmatterOptions): FrontmatterResult;
48
- export function createVirtualPage(options: VirtualPageOptions): VirtualPage;
49
- export function run(config: Partial<SiteConfig>): void;
50
- }