mikel-press 0.0.1 → 0.2.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.
Files changed (4) hide show
  1. package/index.d.ts +55 -0
  2. package/index.js +136 -88
  3. package/package.json +3 -7
  4. package/cli.js +0 -47
package/index.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ interface MikelTemplateOptions {
2
+ functions: {[key: string]: (args: any) => string};
3
+ helpers: {[key: string]: (args: any) => string};
4
+ partials: {[key: string]: string};
5
+ }
6
+
7
+ interface FrontmatterResult {
8
+ body: string;
9
+ attributes: any;
10
+ }
11
+
12
+ interface VirtualPageOptions {
13
+ content?: string;
14
+ file?: string;
15
+ extname?: string;
16
+ basename?: string;
17
+ frontmatter?: (str: string) => FrontmatterResult;
18
+ transform?: (str: string) => string;
19
+ }
20
+
21
+ interface VirtualPage {
22
+ content: string;
23
+ attributes: any;
24
+ name: string;
25
+ extname: string;
26
+ basename: string;
27
+ url: string;
28
+ }
29
+
30
+ interface PostsPluginOptions {
31
+ dir: string;
32
+ parser: (str: string) => string;
33
+ }
34
+
35
+ interface SiteConfig {
36
+ source: string;
37
+ destination: string;
38
+ layout: string;
39
+ layoutContent: string;
40
+ dataDir: string;
41
+ pagesDir: string;
42
+ assetsDir: string;
43
+ frontmatter: (str: string) => FrontmatterResult;
44
+ mikel: Partial<MikelTemplateOptions>;
45
+ plugins: any[];
46
+ }
47
+
48
+ declare module "mikel-press" {
49
+ export function frontmatter(str: string): FrontmatterResult;
50
+ export function createVirtualPage(options: VirtualPageOptions): VirtualPage;
51
+ export function run(config: Partial<SiteConfig>): void;
52
+
53
+ export function postsPlugin(options: PostsPluginOptions): any;
54
+ export function progressPlugin(): any;
55
+ }
package/index.js CHANGED
@@ -2,74 +2,39 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import mikel from "mikel";
4
4
 
5
- // global utils
6
- const utils = {
7
- // @description save file
8
- saveFile: (filePath, fileContent) => {
9
- const folder = path.dirname(filePath);
10
- if (!fs.existsSync(folder)) {
11
- fs.mkdirSync(folder, {recursive: true});
5
+ // @description tiny yaml parser
6
+ const parseYaml = (str = "") => {
7
+ const lines = str.split("\n").filter(line => line.trim() !== "" && !line.trim().startsWith("#"));
8
+ return Object.fromEntries(lines.map(line => {
9
+ const [key, value] = line.split(":").map(part => part.trim());
10
+ if (!isNaN(value)) {
11
+ return [key, Number(value)];
12
12
  }
13
- fs.writeFileSync(filePath, fileContent, "utf8");
14
- },
15
- // @description tiny front-matter parser
16
- frontmatter: (str = "", parser = null) => {
17
- let body = (str || "").trim(), attributes = {};
18
- if (!!parser) {
19
- const matches = Array.from(body.matchAll(/^(--- *)/gm))
20
- if (matches?.length === 2 && matches[0].index === 0) {
21
- const front = body.substring(0 + matches[0][1].length, matches[1].index).trim();
22
- body = body.substring(matches[1].index + matches[1][1].length).trim();
23
- attributes = typeof parser === "function" ? parser(front) : front;
24
- }
13
+ if (value === "true" || value === "false" || value === "null") {
14
+ return [key, JSON.parse(value)];
25
15
  }
26
- return {body, attributes};
27
- },
28
- // @description read a data folder
29
- readData: folder => {
30
- const files = fs.readdirSync(folder, "utf8")
31
- .filter(file => path.extname(file) === ".json")
32
- .map(file => path.join(folder, file))
33
- .map(file => {
34
- return [path.basename(file, ".json"), JSON.parse(fs.readFileSync(file, "utf8"))];
35
- });
36
- return Object.fromEntries(files);
37
- },
38
- // @description get pages from input folder
39
- readPages: (folder, type = ".html", fm = null, parse = null) => {
40
- return fs.readdirSync(folder, "utf8")
41
- .filter(file => path.extname(file) === type)
42
- .map(file => {
43
- const content = fs.readFileSync(path.join(folder, file), "utf8");
44
- const {body, attributes} = utils.frontmatter(content, fm);
45
- return {
46
- name: file,
47
- basename: path.basename(file, type),
48
- extname: path.extname(file),
49
- url: attributes?.permalink || path.join("/", path.basename(file, type) + ".html"),
50
- data: attributes || {},
51
- content: typeof parse === "function" ? parse(body) : body,
52
- };
53
- });
54
- },
55
- // @description get assets
56
- readAssets: (folder, fm = null) => {
57
- const assetPaths = fs.readdirSync(folder, "utf8");
58
- return Object.fromEntries(assetPaths.map(file => {
59
- const content = fs.readFileSync(path.join(folder, file), "utf8");
60
- const {body, attributes} = utils.frontmatter(content, fm);
61
- const asset = {
62
- name: file,
63
- basename: path.basename(file, path.extname(file)),
64
- extname: path.extname(file),
65
- url: attributes?.permalink || path.join("/", file),
66
- data: attributes || {},
67
- content: body,
68
- };
69
- const assetName = asset.basename.replaceAll(".", "_").replaceAll("-", "_");
70
- return [assetName, asset];
71
- }));
72
- },
16
+ return [key, value.replaceAll(/^["']|["']$/g, "")];
17
+ }));
18
+ };
19
+
20
+ // @description tiny front-matter parser
21
+ const frontmatter = (str = "") => {
22
+ let body = (str || "").trim(), attributes = {};
23
+ const matches = Array.from(body.matchAll(/^(--- *)/gm));
24
+ if (matches?.length === 2 && matches[0].index === 0) {
25
+ attributes = parseYaml(body.substring(0 + matches[0][1].length, matches[1].index).trim());
26
+ body = body.substring(matches[1].index + matches[1][1].length).trim();
27
+ }
28
+ return {body, attributes};
29
+ };
30
+
31
+ // @description utility to save a file to disk
32
+ const saveFile = (filePath, fileContent) => {
33
+ const folder = path.dirname(filePath);
34
+ if (!fs.existsSync(folder)) {
35
+ fs.mkdirSync(folder, {recursive: true});
36
+ }
37
+ fs.writeFileSync(filePath, fileContent, "utf8");
73
38
  };
74
39
 
75
40
  // @description returns the layout content from the given options
@@ -84,25 +49,77 @@ const getLayoutContent = config => {
84
49
  content = config.layoutContent || config.templateContent;
85
50
  }
86
51
  // parse with frontmatter
87
- const {body, attributes} = utils.frontmatter(content, config.frontmatter);
52
+ const {body, attributes} = typeof config.frontmatter == "function" ? config.frontmatter(content) : {body: content, attributes: {}};
88
53
  return {
89
54
  content: body,
90
55
  data: attributes || {},
56
+ attributes: attributes || {},
57
+ };
58
+ };
59
+
60
+ // @description create a virtual page object from the given options
61
+ const createVirtualPage = (options = {}) => {
62
+ const content = options.content || fs.readFileSync(options.file, "utf8");
63
+ const extname = options.extname || path.extname(options.file || "") || ".html";
64
+ const basename = options.basename || path.basename(options.file || "", extname) || "virtual";
65
+ const {body, attributes} = typeof options?.frontmatter == "function" ? options.frontmatter(content) : {body: content, attributes: {}};
66
+ return {
67
+ name: basename + extname,
68
+ basename: basename,
69
+ extname: extname,
70
+ url: options.url || attributes?.permalink || path.join("/", basename + extname),
71
+ data: attributes || {}, // DEPRECATED
72
+ attributes: attributes || {},
73
+ content: typeof options.transform === "function" ? options.transform(body) : body,
91
74
  };
92
75
  };
93
76
 
94
- // @description plugins
95
- const plugins = {
96
- // plugin to read and include posts in markdown
97
- posts: (parser = null) => {
98
- return context => {
99
- context.hooks.beforeEmit.add(() => {
100
- const posts = utils.readPages(path.join(context.source, "posts"), ".md", context.site.frontmatter, parser);
101
- context.site.posts = posts; // posts will be accesible in site.posts
102
- context.site.pages = [...context.site.pages, ...posts]; // posts will be included as pages also
77
+ // @description get pages from input folder
78
+ const readPages = (folder, extensions = ".html", fm = null, transform = null) => {
79
+ const extensionsList = new Set([extensions].flat());
80
+ if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
81
+ return fs.readdirSync(folder, "utf8")
82
+ .filter(file => extensionsList.has(path.extname(file)))
83
+ .map(file => {
84
+ return createVirtualPage({
85
+ file: path.join(folder, file),
86
+ frontmatter: fm,
87
+ transform: transform,
88
+ extname: ".html",
89
+ });
90
+ });
91
+ }
92
+ return [];
93
+ };
94
+
95
+ // @description get assets
96
+ const readAssets = (folder, fm = null) => {
97
+ if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
98
+ const assetPaths = fs.readdirSync(folder, "utf8");
99
+ return Object.fromEntries(assetPaths.map(file => {
100
+ const asset = createVirtualPage({
101
+ file: path.join(folder, file),
102
+ frontmatter: fm,
103
103
  });
104
- };
105
- },
104
+ const assetName = asset.basename.replaceAll(".", "_").replaceAll("-", "_");
105
+ return [assetName, asset];
106
+ }));
107
+ }
108
+ return {};
109
+ };
110
+
111
+ // @description read a data folder
112
+ const readData = folder => {
113
+ if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
114
+ const files = fs.readdirSync(folder, "utf8")
115
+ .filter(file => path.extname(file) === ".json")
116
+ .map(file => path.join(folder, file))
117
+ .map(file => {
118
+ return [path.basename(file, ".json"), JSON.parse(fs.readFileSync(file, "utf8"))];
119
+ });
120
+ return Object.fromEntries(files);
121
+ }
122
+ return {};
106
123
  };
107
124
 
108
125
  // @description run mikel press with the provided configuration
@@ -113,7 +130,6 @@ const run = (config = {}) => {
113
130
  site: config || {},
114
131
  source: path.resolve(process.cwd(), config?.source || "."),
115
132
  destination: path.resolve(process.cwd(), config?.destination || "./www"),
116
- compiler: null,
117
133
  layout: getLayoutContent(config),
118
134
  hooks: Object.freeze(Object.fromEntries(hooks.map(name => {
119
135
  return [name, new Set()];
@@ -126,31 +142,63 @@ const run = (config = {}) => {
126
142
  }
127
143
  dispatch("initialize", []);
128
144
  // 2. initialize mikel instance
129
- context.compiler = mikel.create(context.layout.content, config?.mikel || {});
145
+ const compiler = mikel.create(context.layout.content, config?.mikel || {});
130
146
  dispatch("compiler", [context.compiler]);
131
147
  // 3. read stuff
132
- context.site.data = utils.readData(path.join(context.source, config?.dataDir || "data"));
133
- context.site.pages = utils.readPages(path.join(context.source, config?.pagesDir || "pages"), ".html", config?.frontmatter ?? true, c => c);
134
- context.site.assets = utils.readAssets(path.join(context.source, config?.assetsDir || "assets"), config?.frontmatter ?? true);
148
+ context.site.data = readData(path.join(context.source, config?.dataDir || "data"));
149
+ context.site.pages = readPages(path.join(context.source, config?.pagesDir || "pages"), ".html", config?.frontmatter, c => c);
150
+ context.site.assets = readAssets(path.join(context.source, config?.assetsDir || "assets"), config?.frontmatter);
135
151
  dispatch("beforeEmit", []);
136
152
  // 4. save pages
137
153
  context.site.pages.forEach(page => {
138
- context.compiler.addPartial("content", page.content); // register page content as partial
139
- const content = context.compiler({
154
+ compiler.addPartial("content", page.content); // register page content as partial
155
+ const content = compiler({
140
156
  site: context.site,
141
157
  layout: context.layout,
142
158
  page: page,
143
159
  });
144
160
  dispatch("emitPage", [page, content]);
145
- utils.saveFile(path.join(context.destination, page.url), content);
161
+ saveFile(path.join(context.destination, page.url), content);
146
162
  });
147
163
  // 5. save assets
148
164
  Object.values(context.site.assets).forEach(asset => {
149
165
  dispatch("emitAsset", [asset]);
150
- utils.saveFile(path.join(context.destination, asset.url), asset.content);
166
+ saveFile(path.join(context.destination, asset.url), asset.content);
151
167
  });
152
168
  dispatch("done", []);
153
169
  };
154
170
 
171
+ // plugin to read and include posts in markdown
172
+ const postsPlugin = (options = {}) => {
173
+ return context => {
174
+ context.hooks.beforeEmit.add(() => {
175
+ const posts = readPages(path.join(context.source, options?.dir || "posts"), [".md"], context.site.frontmatter, options?.parser);
176
+ context.site.posts = posts; // posts will be accesible in site.posts
177
+ context.site.pages = [...context.site.pages, ...posts]; // posts will be included as pages also
178
+ });
179
+ };
180
+ };
181
+
182
+ // progress plugin
183
+ const progressPlugin = () => {
184
+ return context => {
185
+ const timeStart = Date.now();
186
+ const log = (status, msg) => console.log(`[${new Date().toISOString()}] (${status}) ${msg}`);
187
+ context.hooks.initialize.add(() => {
188
+ log("info", `source directory: ${context.source}`);
189
+ log("info", `destination directory: ${context.destination}`);
190
+ });
191
+ context.hooks.emitPage.add(page => {
192
+ log("info", `saving page: ${page.url} --> ${path.join(context.destination, page.url)}`);
193
+ });
194
+ context.hooks.emitAsset.add(asset => {
195
+ log("info", `saving asset: ${asset.url} --> ${path.join(context.destination, asset.url)}`);
196
+ });
197
+ context.hooks.done.add(() => {
198
+ log("done", `build completed in ${Date.now() - timeStart}ms`);
199
+ });
200
+ };
201
+ };
202
+
155
203
  // export
156
- export default {utils, plugins, run};
204
+ export default {run, createVirtualPage, frontmatter, postsPlugin, progressPlugin};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mikel-press",
3
3
  "description": "A minimal static site generator based on mikel templating",
4
- "version": "0.0.1",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "author": {
7
7
  "name": "Josemi Juanes",
@@ -12,16 +12,12 @@
12
12
  "bugs": "https://github.com/jmjuanes/mikel/issues",
13
13
  "main": "index.js",
14
14
  "module": "index.js",
15
+ "types": "index.d.ts",
15
16
  "exports": {
16
17
  ".": "./index.js",
17
18
  "./index.js": "./index.js",
18
- "./cli.js": "./cli.js",
19
19
  "./package.json": "./package.json"
20
20
  },
21
- "bin": {
22
- "mikelpress": "./cli.js",
23
- "mikel-press": "./cli.js"
24
- },
25
21
  "scripts": {
26
22
  "test": "node test.js"
27
23
  },
@@ -39,6 +35,6 @@
39
35
  "files": [
40
36
  "README.md",
41
37
  "index.js",
42
- "cli.js"
38
+ "index.d.ts"
43
39
  ]
44
40
  }
package/cli.js DELETED
@@ -1,47 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import * as path from "node:path";
4
- import {parseArgs} from "node:util";
5
- import mikelPress from "./index.js";
6
-
7
- // get the configuration file from the provided path
8
- const resolveConfig = value => {
9
- const configPath = path.resolve(process.cwd(), value || "./press.config.js");
10
- return import(configPath).then(config => {
11
- return config?.default || {};
12
- });
13
- };
14
-
15
- // available commands
16
- const commands = {
17
- build: {
18
- description: "Generate the static site with the provided configuration.",
19
- execute: async values => {
20
- const config = await resolveConfig(values.config);
21
- return mikelPress.run(config);
22
- },
23
- options: {
24
- config: {
25
- type: "string",
26
- short: "c",
27
- default: "press.config.js",
28
- },
29
- },
30
- },
31
- };
32
-
33
- const main = async (args = []) => {
34
- const [commandName, ...otherArguments] = args;
35
- if (typeof commands[commandName] !== "undefined") {
36
- const {values} = parseArgs({
37
- args: otherArguments,
38
- options: commands[commandName].options || {},
39
- });
40
- return commands[commandName].execute(values);
41
- }
42
- // if this command is not available, print help [TODO]
43
- // return commands.help.execute();
44
- };
45
-
46
- // run
47
- main(process.argv.slice(2));