@kenjura/ursa 0.5.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.
@@ -0,0 +1,266 @@
1
+ import recurse from "recursive-readdir";
2
+
3
+ import { copyFile, mkdir, readdir, readFile } from "fs/promises";
4
+ import { getAutomenu } from "../helper/automenu.js";
5
+ import { filterAsync } from "../helper/filterAsync.js";
6
+ import { isDirectory } from "../helper/isDirectory.js";
7
+ import {
8
+ extractMetadata,
9
+ extractRawMetadata,
10
+ } from "../helper/metadataExtractor.js";
11
+ import { renderFile } from "../helper/fileRenderer.js";
12
+ import { copy as copyDir, emptyDir, outputFile } from "fs-extra";
13
+ import { basename, dirname, extname, join, parse, resolve } from "path";
14
+ import { URL } from "url";
15
+ import o2x from "object-to-xml";
16
+
17
+ const DEFAULT_TEMPLATE_NAME =
18
+ process.env.DEFAULT_TEMPLATE_NAME ?? "default-template";
19
+
20
+ export async function generate({
21
+ source = join(process.cwd(), "."),
22
+ meta = join(process.cwd(), "meta"),
23
+ output = join(process.cwd(), "build"),
24
+ } = {}) {
25
+ console.log({ source, meta, output });
26
+
27
+ const allSourceFilenamesUnfiltered = await recurse(source, [() => false]);
28
+ const includeFilter = process.env.INCLUDE_FILTER
29
+ ? (fileName) => fileName.match(process.env.INCLUDE_FILTER)
30
+ : Boolean;
31
+ const allSourceFilenames = allSourceFilenamesUnfiltered.filter(includeFilter);
32
+ console.log(allSourceFilenames);
33
+
34
+ if (source.substr(-1) !== "/") source += "/"; // warning: might not work in windows
35
+ if (output.substr(-1) !== "/") output += "/";
36
+
37
+ const templates = await getTemplates(meta); // todo: error if no default template
38
+ // console.log({ templates });
39
+
40
+ const menu = await getMenu(allSourceFilenames, source);
41
+
42
+ // clean build directory
43
+ await emptyDir(output);
44
+
45
+ // create public folder
46
+ const pub = join(output, "public");
47
+ await mkdir(pub);
48
+ await copyDir(meta, pub);
49
+
50
+ // read all articles, process them, copy them to build
51
+ const articleExtensions = /\.(md|txt|yml)/;
52
+ const allSourceFilenamesThatAreArticles = allSourceFilenames.filter(
53
+ (filename) => filename.match(articleExtensions)
54
+ );
55
+ const allSourceFilenamesThatAreDirectories = await filterAsync(
56
+ allSourceFilenames,
57
+ (filename) => isDirectory(filename)
58
+ );
59
+
60
+ // process individual articles
61
+ const jsonCache = new Map();
62
+ await Promise.all(
63
+ allSourceFilenamesThatAreArticles.map(async (file) => {
64
+ console.log(`processing article ${file}`);
65
+
66
+ const rawBody = await readFile(file, "utf8");
67
+ const type = parse(file).ext;
68
+ const meta = extractMetadata(rawBody);
69
+ const rawMeta = extractRawMetadata(rawBody);
70
+ const bodyLessMeta = rawBody.replace(rawMeta, "");
71
+ const transformedMetadata = await getTransformedMetadata(
72
+ dirname(file),
73
+ meta
74
+ );
75
+ const ext = extname(file);
76
+ const base = basename(file, ext);
77
+ const dir = addTrailingSlash(dirname(file)).replace(source, "");
78
+ const body = renderFile({
79
+ fileContents: rawBody,
80
+ type,
81
+ dirname: dir,
82
+ basename: base,
83
+ });
84
+
85
+ const requestedTemplateName = meta && meta.template;
86
+ const template =
87
+ templates[requestedTemplateName] || templates[DEFAULT_TEMPLATE_NAME];
88
+ // console.log({ requestedTemplateName, templates: templates.keys });
89
+
90
+ const finalHtml = template
91
+ .replace("${menu}", menu)
92
+ .replace("${meta}", JSON.stringify(meta))
93
+ .replace("${transformedMetadata}", transformedMetadata)
94
+ .replace("${body}", body);
95
+
96
+ const outputFilename = file
97
+ .replace(source, output)
98
+ .replace(parse(file).ext, ".html");
99
+
100
+ console.log(`writing article to ${outputFilename}`);
101
+
102
+ await outputFile(outputFilename, finalHtml);
103
+
104
+ // json
105
+
106
+ const jsonOutputFilename = outputFilename.replace(".html", ".json");
107
+ const jsonObject = {
108
+ name: base,
109
+ contents: rawBody,
110
+ bodyLessMeta: bodyLessMeta,
111
+ bodyHtml: body,
112
+ metadata: meta,
113
+ transformedMetadata,
114
+ html: finalHtml,
115
+ };
116
+ jsonCache.set(file, jsonObject);
117
+ const json = JSON.stringify(jsonObject);
118
+ console.log(`writing article to ${jsonOutputFilename}`);
119
+ await outputFile(jsonOutputFilename, json);
120
+
121
+ // xml
122
+
123
+ const xmlOutputFilename = outputFilename.replace(".html", ".xml");
124
+ const xml = `<article>${o2x(jsonObject)}</article>`;
125
+ await outputFile(xmlOutputFilename, xml);
126
+ })
127
+ );
128
+
129
+ console.log(jsonCache.keys());
130
+ // process directory indices
131
+ await Promise.all(
132
+ allSourceFilenamesThatAreDirectories.map(async (dir) => {
133
+ console.log(`processing directory ${dir}`);
134
+
135
+ const pathsInThisDirectory = allSourceFilenames.filter((filename) =>
136
+ filename.match(new RegExp(`${dir}.+`))
137
+ );
138
+
139
+ const jsonObjects = pathsInThisDirectory
140
+ .map((path) => {
141
+ const object = jsonCache.get(path);
142
+ return typeof object === "object" ? object : null;
143
+ })
144
+ .filter((a) => a);
145
+
146
+ const json = JSON.stringify(jsonObjects);
147
+
148
+ const outputFilename = dir.replace(source, output) + ".json";
149
+
150
+ console.log(`writing directory index to ${outputFilename}`);
151
+ await outputFile(outputFilename, json);
152
+
153
+ // html
154
+ const template = templates["default-template"]; // TODO: figure out a way to specify template for a directory index
155
+ const indexHtml = `<ul>${pathsInThisDirectory
156
+ .map((path) => {
157
+ const partialPath = path
158
+ .replace(source, "")
159
+ .replace(parse(path).ext, ".html");
160
+ const name = basename(path, parse(path).ext);
161
+ return `<li><a href="${partialPath}">${name}</a></li>`;
162
+ })
163
+ .join("")}</ul>`;
164
+ const finalHtml = template
165
+ .replace("${menu}", menu)
166
+ .replace("${body}", indexHtml);
167
+ const htmlOutputFilename = dir.replace(source, output) + ".html";
168
+ console.log(`writing directory index to ${htmlOutputFilename}`);
169
+ await outputFile(htmlOutputFilename, finalHtml);
170
+ })
171
+ );
172
+
173
+ // copy all static files (i.e. images)
174
+ const imageExtensions = /\.(jpg|png|gif|webp)/; // todo: handle-extensionless images...ugh
175
+ const allSourceFilenamesThatAreImages = allSourceFilenames.filter(
176
+ (filename) => filename.match(imageExtensions)
177
+ );
178
+ await Promise.all(
179
+ allSourceFilenamesThatAreImages.map(async (file) => {
180
+ console.log(`processing static file ${file}`);
181
+
182
+ const outputFilename = file.replace(source, output);
183
+
184
+ console.log(`writing static file to ${outputFilename}`);
185
+
186
+ return await copyFile(file, outputFilename);
187
+ })
188
+ );
189
+ }
190
+
191
+ /**
192
+ * gets { [templateName:String]:[templateBody:String] }
193
+ * meta: full path to meta files (default-template.html, etc)
194
+ */
195
+ async function getTemplates(meta) {
196
+ const allMetaFilenames = await recurse(meta);
197
+ const allHtmlFilenames = allMetaFilenames.filter((filename) =>
198
+ filename.match(/\.html/)
199
+ );
200
+
201
+ let templates = {};
202
+ const templatesArray = await Promise.all(
203
+ allHtmlFilenames.map(async (filename) => {
204
+ const { name } = parse(filename);
205
+ const fileContent = await readFile(filename, "utf8");
206
+ return [name, fileContent];
207
+ })
208
+ );
209
+ templatesArray.forEach(
210
+ ([templateName, templateText]) => (templates[templateName] = templateText)
211
+ );
212
+
213
+ return templates;
214
+ }
215
+
216
+ async function getMenu(allSourceFilenames, source) {
217
+ // todo: handle various incarnations of menu filename
218
+
219
+ const rawMenu = await getAutomenu(source);
220
+ const menuBody = renderFile({ fileContents: rawMenu, type: ".md" });
221
+ return menuBody;
222
+
223
+ // const allMenus = allSourceFilenames.filter((filename) =>
224
+ // filename.match(/_?menu\.(html|yml|md|txt)/)
225
+ // );
226
+ // console.log({ allMenus });
227
+ // if (allMenus.length === 0) return "";
228
+
229
+ // // pick best menu...TODO: actually apply logic here
230
+ // const bestMenu = allMenus[0];
231
+ // const rawBody = await readFile(bestMenu, "utf8");
232
+ // const type = parse(bestMenu).ext;
233
+ // const menuBody = renderFile({ fileContents: rawBody, type });
234
+
235
+ // return menuBody;
236
+ }
237
+
238
+ async function getTransformedMetadata(dirname, metadata) {
239
+ // console.log("getTransformedMetadata > ", { dirname });
240
+ // custom transform? else, use default
241
+ const customTransformFnFilename = join(dirname, "transformMetadata.js");
242
+ let transformFn = defaultTransformFn;
243
+ try {
244
+ const customTransformFn = (await import(customTransformFnFilename)).default;
245
+ if (typeof customTransformFn === "function")
246
+ transformFn = customTransformFn;
247
+ } catch (e) {
248
+ // console.error(e);
249
+ }
250
+ try {
251
+ return transformFn(metadata);
252
+ } catch (e) {
253
+ return "error transforming metadata";
254
+ }
255
+
256
+ function defaultTransformFn(metadata) {
257
+ return "default transform";
258
+ }
259
+ }
260
+
261
+ function addTrailingSlash(somePath) {
262
+ if (typeof somePath !== "string") return somePath;
263
+ if (somePath.length < 1) return somePath;
264
+ if (somePath[somePath.length - 1] == "/") return somePath;
265
+ return `${somePath}/`;
266
+ }
package/src/serve.js ADDED
@@ -0,0 +1,69 @@
1
+ import express from "express";
2
+ import watch from "node-watch";
3
+
4
+ import { generate } from "./jobs/generate.js";
5
+ import { join, resolve } from "path";
6
+
7
+ const source = resolve(process.env.SOURCE ?? join(process.cwd(), "source"));
8
+ const meta = resolve(process.env.META ?? join(process.cwd(), "meta"));
9
+ const output = resolve(process.env.OUTPUT ?? join(process.cwd(), "build"));
10
+
11
+ console.log({ source, meta, output });
12
+
13
+ await generate({ source, meta, output });
14
+ console.log("done generating. now serving...");
15
+
16
+ serve(output);
17
+
18
+ watch(meta, { recursive: true }, async (evt, name) => {
19
+ console.log("meta files changed! generating output");
20
+ await generate({ source, meta, output });
21
+ });
22
+
23
+ watch(source, { recursive: true }, async (evt, name) => {
24
+ console.log("source files changed! generating output");
25
+ await generate({ source, meta, output });
26
+ });
27
+
28
+ /**
29
+ * we're only interested in meta (and maybe, in the future, source)
30
+ * for src changes, we need the node process to restart
31
+ */
32
+ function filter(filename, skip) {
33
+ // console.log("testing ", filename);
34
+ if (/\/build/.test(filename)) return skip;
35
+ if (/\/node_modules/.test(filename)) return skip;
36
+ if (/\.git/.test(filename)) return skip;
37
+ if (/\/src/.test(filename)) return skip;
38
+ if (/\/meta/.test(filename)) return true;
39
+ return false;
40
+ }
41
+
42
+ import fs, { stat } from "fs";
43
+ import { promises } from "fs";
44
+ const { readdir } = promises;
45
+ import http from "http";
46
+ import { Server } from "node-static";
47
+
48
+ function serve(root) {
49
+ const app = express();
50
+ const port = process.env.PORT || 8080;
51
+
52
+ app.use(
53
+ express.static(output, { extensions: ["html"], index: "index.html" })
54
+ );
55
+
56
+ app.get("/", async (req, res) => {
57
+ console.log({ output });
58
+ const dir = await readdir(output);
59
+ const html = dir
60
+ .map((file) => `<li><a href="${file}">${file}</a></li>`)
61
+ .join("");
62
+ res.setHeader("Content-Type", "text/html");
63
+ res.send(html);
64
+ });
65
+
66
+ app.listen(port, () => {
67
+ console.log(`server listening on port ${port}`);
68
+ });
69
+ }
package/src/start.js ADDED
@@ -0,0 +1,8 @@
1
+ import { generate } from "./jobs/generate.js";
2
+
3
+ import { join, resolve } from "path";
4
+
5
+ const source = process.env.SOURCE ?? join(process.cwd(), "source");
6
+ const build = process.env.BUILD ?? join(process.cwd(), "build");
7
+
8
+ generate({ source, build });