@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.
- package/.nvmrc +1 -0
- package/.vscode/launch.json +20 -0
- package/CHANGELOG.md +41 -0
- package/README.md +32 -0
- package/TODO.md +16 -0
- package/lib/index.js +9 -0
- package/meta/character-sheet/css/character-sheet.css +10 -0
- package/meta/character-sheet/js/components.js +37 -0
- package/meta/character-sheet/js/main.js +7 -0
- package/meta/character-sheet/js/model.js +15 -0
- package/meta/character-sheet-template.html +76 -0
- package/meta/character-sheet.css +50 -0
- package/meta/cssui.bundle.min.css +11 -0
- package/meta/default-template.html +22 -0
- package/meta/default.css +154 -0
- package/meta/goudy_bookletter_1911-webfont.woff +0 -0
- package/meta/template2.html +1 -0
- package/nodemon.json +16 -0
- package/package.json +48 -0
- package/src/commands/generate.js +25 -0
- package/src/helper/__test__/pathExtReader.test.js +38 -0
- package/src/helper/automenu.js +120 -0
- package/src/helper/fileRenderer.js +18 -0
- package/src/helper/filterAsync.js +5 -0
- package/src/helper/isDirectory.js +10 -0
- package/src/helper/markdownHelper.cjs +36 -0
- package/src/helper/metadataExtractor.js +30 -0
- package/src/helper/objToXml.js +20 -0
- package/src/helper/pathExtReader.js +12 -0
- package/src/helper/wikitextHelper.js +486 -0
- package/src/index-yargs.js +8 -0
- package/src/index.js +8 -0
- package/src/jobs/generate.js +266 -0
- package/src/serve.js +69 -0
- package/src/start.js +8 -0
|
@@ -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