hdoc-tools 0.50.1 → 0.52.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/hdoc-build.js +93 -47
- package/hdoc-help.js +4 -1
- package/hdoc-mermaid.js +204 -0
- package/hdoc-module.js +61 -4
- package/hdoc-serve.js +16 -296
- package/hdoc-validate.js +5 -2
- package/hdoc.js +4 -0
- package/npm-shrinkwrap.json +1095 -4345
- package/package.json +10 -8
- package/templates/mermaid-puppeteer-config.json +0 -6
package/hdoc-serve.js
CHANGED
|
@@ -3,16 +3,8 @@
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const mdfm = require("markdown-it-front-matter");
|
|
9
|
-
|
|
10
|
-
const { execSync } = require("child_process");
|
|
11
|
-
|
|
12
|
-
const mermaid_theme_path = path.resolve(
|
|
13
|
-
__dirname,
|
|
14
|
-
"templates",
|
|
15
|
-
"mermaid-theme.yaml",
|
|
6
|
+
const { create_content_handler } = require(
|
|
7
|
+
path.join(__dirname, "hdoc-content-routes.js"),
|
|
16
8
|
);
|
|
17
9
|
|
|
18
10
|
let port = 3000;
|
|
@@ -97,310 +89,38 @@
|
|
|
97
89
|
process.exit(1);
|
|
98
90
|
}
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
res.setHeader("Content-Type", "application/json");
|
|
111
|
-
res.send(JSON.stringify(library, null, 3));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
async function transform_markdown_and_send_html(req, res, file_path) {
|
|
115
|
-
if (!fs.existsSync(file_path)) return false;
|
|
116
|
-
|
|
117
|
-
let md_txt = hdoc.expand_variables(
|
|
118
|
-
fs.readFileSync(file_path).toString(),
|
|
119
|
-
docId,
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const includes_processed = await hdoc.process_includes(
|
|
123
|
-
file_path,
|
|
124
|
-
md_txt,
|
|
125
|
-
global_source_path,
|
|
126
|
-
);
|
|
127
|
-
md_txt = includes_processed.body;
|
|
128
|
-
const md = require("markdown-it")({
|
|
129
|
-
html: true,
|
|
130
|
-
linkify: true,
|
|
131
|
-
typographer: true,
|
|
132
|
-
highlight: function (str, lang) {
|
|
133
|
-
if (lang === "mermaid") {
|
|
134
|
-
try {
|
|
135
|
-
const tmpInput = hdoc.tmp_file_sync({ postfix: ".mmd" });
|
|
136
|
-
const tmpOutput = hdoc.tmp_file_sync({ postfix: ".svg" });
|
|
137
|
-
|
|
138
|
-
if (!str.startsWith('---')) {
|
|
139
|
-
str = '---\n' + fs.readFileSync(mermaid_theme_path, {encoding: 'utf-8'}) + `\n---\n${str}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
fs.writeFileSync(tmpInput.name, str);
|
|
143
|
-
|
|
144
|
-
let cmd = `${__dirname}/node_modules/.bin/mmdc`;
|
|
145
|
-
|
|
146
|
-
if (process.platform === "win32") {
|
|
147
|
-
cmd = `"${cmd}"`;
|
|
148
|
-
}
|
|
149
|
-
cmd = `${cmd} -i "${tmpInput.name}" -o "${tmpOutput.name}" --quiet --backgroundColor transparent`;
|
|
150
|
-
console.log(`Generating Inline Mermaid SVG found in: ${file_path}`);
|
|
151
|
-
execSync(cmd);
|
|
152
|
-
|
|
153
|
-
if (!fs.existsSync(tmpOutput.name)) {
|
|
154
|
-
throw new Error("mmdc did not generate output");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const svg = fs.readFileSync(tmpOutput.name, "utf8");
|
|
158
|
-
|
|
159
|
-
tmpInput.removeCallback();
|
|
160
|
-
tmpOutput.removeCallback();
|
|
161
|
-
|
|
162
|
-
return `<div class="text-body">${svg}</div>`;
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return `<div class="alert alert-danger mb-0 text-break"
|
|
165
|
-
role="alert"
|
|
166
|
-
data-bs-toggle="collapse"
|
|
167
|
-
data-bs-target="#alertContent"
|
|
168
|
-
aria-expanded="false"
|
|
169
|
-
aria-controls="alertContent">
|
|
170
|
-
<div>
|
|
171
|
-
<strong>Error!</strong> Mermaid was unable to generate the SVG expected here. Click for more details
|
|
172
|
-
</div>
|
|
173
|
-
<div class="collapse mt-3" id="alertContent">
|
|
174
|
-
<p class="mb-0 text-break">
|
|
175
|
-
${err.message}
|
|
176
|
-
</p>
|
|
177
|
-
</div>
|
|
178
|
-
</div>`;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
md.linkify.set({
|
|
184
|
-
fuzzyEmail: false,
|
|
185
|
-
fuzzyLink: false,
|
|
186
|
-
fuzzyIP: false,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
let frontmatter_content = "";
|
|
190
|
-
md.use(mdfm, (fm) => {
|
|
191
|
-
frontmatter_content = fm;
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const tips = require(`${__dirname}/custom_modules/tips.js`);
|
|
195
|
-
md.use(tips, { links: true });
|
|
196
|
-
|
|
197
|
-
const html_txt = md.render(md_txt.toString());
|
|
198
|
-
|
|
199
|
-
if (frontmatter_content.length) {
|
|
200
|
-
const obj = hdoc.parse_yaml(frontmatter_content);
|
|
201
|
-
const buff = Buffer.from(JSON.stringify(obj), "utf-8");
|
|
202
|
-
const base64 = buff.toString("base64");
|
|
203
|
-
res.setHeader("X-frontmatter", base64);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
res.setHeader("Content-Type", "text/html");
|
|
207
|
-
res.send(html_txt);
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
function send_content_file(req, res, file_path, redirected = false) {
|
|
213
|
-
let content_txt = hdoc.expand_variables(
|
|
214
|
-
fs.readFileSync(file_path).toString(),
|
|
215
|
-
docId,
|
|
216
|
-
);
|
|
217
|
-
if (redirected)
|
|
218
|
-
content_txt = `Redirected from ${redirected}\n\n${content_txt}`;
|
|
219
|
-
|
|
220
|
-
const contentType = hdoc.content_type_for_ext(path.extname(file_path));
|
|
221
|
-
|
|
222
|
-
if (path.extname(file_path) === ".md") {
|
|
223
|
-
res.setHeader("Content-Disposition", "inline");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
res.setHeader("Content-Type", contentType);
|
|
227
|
-
|
|
228
|
-
res.send(content_txt);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function send_file(req, res, file_path) {
|
|
232
|
-
// Need to set the content type here??
|
|
233
|
-
const contentType = hdoc.content_type_for_ext(path.extname(file_path));
|
|
234
|
-
res.setHeader("Content-Type", contentType);
|
|
235
|
-
|
|
236
|
-
const r = fs.createReadStream(file_path);
|
|
237
|
-
const ps = new stream.PassThrough();
|
|
238
|
-
stream.pipeline(r, ps, (err) => {
|
|
239
|
-
if (err) {
|
|
240
|
-
console.error(err); // No such file or any other kind of error
|
|
241
|
-
return res.sendStatus(400).send("Unexpected error");
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
ps.pipe(res);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function send_content_resource_404(req, res) {
|
|
248
|
-
res.setHeader("Content-Type", "text/html");
|
|
249
|
-
res.status(404).send("Content resource not found");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 1. If we request a file with a .html file extension, and that file DOES NOT exist,
|
|
253
|
-
// we will look for the same file but with a .md extension. If we find that
|
|
254
|
-
// corresponding markdown file, we will transform that markdown file to HTML and
|
|
255
|
-
// return the HTML content
|
|
256
|
-
//
|
|
257
|
-
// 2. If we request a file, without any file extension then we will look for that file
|
|
258
|
-
// with a .md extension, and if that file exists, we will transform that markdown
|
|
259
|
-
// file to HTML and return that file.
|
|
260
|
-
//
|
|
261
|
-
// For all other requests, we are going to look on the filesystem. If we request
|
|
262
|
-
// a specific file with its extension (including .md files), then we will simply
|
|
263
|
-
// return the file verbatim as a static file.
|
|
264
|
-
//
|
|
265
|
-
// If we request a file without an extension and that file does not exist, we will
|
|
266
|
-
// assume that is a folder, will append index.html and look for that file, if present
|
|
267
|
-
// we will send it, if not present we will look for index.md, and if thats present
|
|
268
|
-
// we will transform to HTML and return that
|
|
269
|
-
//
|
|
270
|
-
// Anything else in this handler will return a 404 error
|
|
271
|
-
|
|
272
|
-
app.get("/_books/*", (req, res) => {
|
|
273
|
-
let url = req.url.replace("/_books/", "/");
|
|
274
|
-
|
|
275
|
-
console.log("URL Requested:", url);
|
|
276
|
-
|
|
277
|
-
// Process redirect
|
|
278
|
-
if (
|
|
279
|
-
hdocbook_project.redirects &&
|
|
280
|
-
Array.isArray(hdocbook_project.redirects) &&
|
|
281
|
-
hdocbook_project.redirects.length > 0
|
|
282
|
-
) {
|
|
283
|
-
const source_url = url.indexOf("/") === 0 ? url : `/${url}`;
|
|
284
|
-
for (const redir of hdocbook_project.redirects) {
|
|
285
|
-
redir.url =
|
|
286
|
-
redir.url.indexOf("/") === 0 ? redir.url : `/${redir.url}`;
|
|
287
|
-
if (
|
|
288
|
-
redir.url === source_url &&
|
|
289
|
-
redir.location &&
|
|
290
|
-
redir.location !== ""
|
|
291
|
-
) {
|
|
292
|
-
url = `${redir.location}`;
|
|
293
|
-
console.log(`Redirecting to ${url}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const file_path = path.join(source_path, url);
|
|
299
|
-
|
|
300
|
-
if (path.extname(file_path) === ".html") {
|
|
301
|
-
// 1a. check for html files, and send/transform as required
|
|
302
|
-
if (fs.existsSync(file_path)) {
|
|
303
|
-
// HTML file exists on disk, just return it verbatim
|
|
304
|
-
res.setHeader("Content-Type", "text/html");
|
|
305
|
-
send_file(req, res, file_path);
|
|
306
|
-
return true;
|
|
307
|
-
}
|
|
308
|
-
if (fs.existsSync(file_path.replace(".html", ".md"))) {
|
|
309
|
-
if (
|
|
310
|
-
transform_markdown_and_send_html(
|
|
311
|
-
req,
|
|
312
|
-
res,
|
|
313
|
-
file_path.replace(".html", ".md"),
|
|
314
|
-
)
|
|
315
|
-
) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} else if (path.extname(file_path) === ".md") {
|
|
320
|
-
// If the markdown file exists, just send to caller as is
|
|
321
|
-
if (fs.existsSync(file_path)) {
|
|
322
|
-
send_content_file(req, res, file_path);
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
} else if (path.extname(file_path).length === 0) {
|
|
326
|
-
// 2. If we request a file, without any file extension
|
|
327
|
-
if (fs.existsSync(`${file_path}.md`)) {
|
|
328
|
-
if (transform_markdown_and_send_html(req, res, `${file_path}.md`)) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
} else if (fs.existsSync(path.join(`${file_path}index.md`))) {
|
|
332
|
-
if (
|
|
333
|
-
transform_markdown_and_send_html(
|
|
334
|
-
req,
|
|
335
|
-
res,
|
|
336
|
-
path.join(file_path, "index.md"),
|
|
337
|
-
)
|
|
338
|
-
) {
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
} else if (fs.existsSync(path.join(`${file_path}index.html`))) {
|
|
342
|
-
res.setHeader("Content-Type", "text/html");
|
|
343
|
-
send_content_file(req, res, path.join(`${file_path}index.html`));
|
|
344
|
-
return;
|
|
345
|
-
} else if (fs.existsSync(`${file_path}/index.md`)) {
|
|
346
|
-
if (
|
|
347
|
-
transform_markdown_and_send_html(req, res, `${file_path}/index.md`)
|
|
348
|
-
) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
} else if (fs.existsSync(path.join(`${file_path}/index.html`))) {
|
|
352
|
-
res.setHeader("Content-Type", "text/html");
|
|
353
|
-
send_content_file(req, res, path.join(`${file_path}/index.html`));
|
|
354
|
-
return;
|
|
355
|
-
} else if (fs.existsSync(path.join(`${file_path}.html`))) {
|
|
356
|
-
res.setHeader("Content-Type", "text/html");
|
|
357
|
-
send_content_file(req, res, path.join(`${file_path}.html`));
|
|
358
|
-
return;
|
|
359
|
-
} else if (fs.existsSync(path.join(`${file_path}.htm`))) {
|
|
360
|
-
res.setHeader("Content-Type", "text/html");
|
|
361
|
-
send_content_file(req, res, path.join(`${file_path}.htm`));
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
} else if (fs.existsSync(file_path)) {
|
|
365
|
-
if (
|
|
366
|
-
file_path.endsWith("hdocbook.json") ||
|
|
367
|
-
file_path.endsWith("hdocbook_project.json")
|
|
368
|
-
) {
|
|
369
|
-
try {
|
|
370
|
-
// Read & parse file
|
|
371
|
-
JSON.parse(fs.readFileSync(file_path));
|
|
372
|
-
} catch (e) {
|
|
373
|
-
console.error(`Error parsing hdocbook.json: ${e}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
send_file(req, res, file_path);
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Return a 404 error here
|
|
381
|
-
send_content_resource_404(req, res);
|
|
92
|
+
// Mount the shared /_books/* content + render routes. The very same
|
|
93
|
+
// handler is reused by `hdoc edit`, so live preview matches published
|
|
94
|
+
// output. The returned helpers are reused by the SPA catch-all below.
|
|
95
|
+
const content = create_content_handler({
|
|
96
|
+
source_path,
|
|
97
|
+
docId,
|
|
98
|
+
hdocbook_config,
|
|
99
|
+
hdocbook_project,
|
|
100
|
+
nav_inline,
|
|
382
101
|
});
|
|
102
|
+
content.register(app);
|
|
383
103
|
|
|
384
104
|
// Catch all
|
|
385
|
-
app.get("
|
|
105
|
+
app.get("/{*splat}", (req, res) => {
|
|
386
106
|
|
|
387
107
|
const ui_file_path = path.join(ui_path, req.url);
|
|
388
108
|
|
|
389
109
|
// To support the SPA application behavior, if there is no file extension present, then
|
|
390
110
|
// we simply return the /index.html file content to the client
|
|
391
111
|
if (path.extname(ui_file_path).length === 0) {
|
|
392
|
-
send_content_file(req, res, path.join(ui_path, "index.html"));
|
|
112
|
+
content.send_content_file(req, res, path.join(ui_path, "index.html"));
|
|
393
113
|
return;
|
|
394
114
|
}
|
|
395
115
|
|
|
396
116
|
// If the file exists, send it.
|
|
397
117
|
if (fs.existsSync(ui_file_path)) {
|
|
398
|
-
send_content_file(req, res, ui_file_path);
|
|
118
|
+
content.send_content_file(req, res, ui_file_path);
|
|
399
119
|
return;
|
|
400
120
|
}
|
|
401
121
|
|
|
402
122
|
// All else fails, we have not file to return, so return a 404 error here
|
|
403
|
-
send_content_resource_404(req, res);
|
|
123
|
+
content.send_content_resource_404(req, res);
|
|
404
124
|
});
|
|
405
125
|
|
|
406
126
|
const server = app.listen(port, "0.0.0.0", () => {
|
package/hdoc-validate.js
CHANGED
|
@@ -28,6 +28,7 @@ const { error } = require("node:console");
|
|
|
28
28
|
let _on_int_net_cached = null; // null = not yet checked; cached after first DNS lookup
|
|
29
29
|
const exclude_h1_count = {};
|
|
30
30
|
const exclude_spellcheck_output = [];
|
|
31
|
+
let global_spellcheck = [];
|
|
31
32
|
|
|
32
33
|
const excludeLink = (url) => {
|
|
33
34
|
if (exclude_links[url]) return true;
|
|
@@ -87,13 +88,13 @@ const { error } = require("node:console");
|
|
|
87
88
|
error_message = `${markdown_paths.relativePath}:${key}:${link_location.column} - ${error_message}`;
|
|
88
89
|
else
|
|
89
90
|
error_message = `${markdown_paths.relativePath}:${key} - ${error_message}`;
|
|
90
|
-
if (!excludes[source_path]) {
|
|
91
|
+
if (!excludes[source_path] && !global_spellcheck.includes(spelling.toLowerCase())) {
|
|
91
92
|
errors[sourceFile.relativePath].push(
|
|
92
93
|
`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`,
|
|
93
94
|
);
|
|
94
95
|
spelling_errors[spelling] = true;
|
|
95
96
|
} else if (
|
|
96
|
-
!excludes[source_path].includes(spelling.toLowerCase())
|
|
97
|
+
!excludes[source_path].includes(spelling.toLowerCase()) && !global_spellcheck.includes(spelling.toLowerCase())
|
|
97
98
|
) {
|
|
98
99
|
errors[sourceFile.relativePath].push(
|
|
99
100
|
`${error_message} ${spelling} should be ${translate_output[key][i][spelling].details}`,
|
|
@@ -1000,6 +1001,8 @@ const { error } = require("node:console");
|
|
|
1000
1001
|
}
|
|
1001
1002
|
}
|
|
1002
1003
|
|
|
1004
|
+
if (hdocbook_project.validation && Array.isArray(hdocbook_project.validation.spellcheckDictionary)) global_spellcheck = hdocbook_project.validation.spellcheckDictionary.map((w) => String(w).toLowerCase());
|
|
1005
|
+
|
|
1003
1006
|
// Check navigation spellings & paths exist
|
|
1004
1007
|
const nav_errors = await checkNavigation(
|
|
1005
1008
|
source_path,
|
package/hdoc.js
CHANGED
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
// Default source path to working directory
|
|
128
128
|
let source_path = process.cwd();
|
|
129
129
|
let ui_path = path.join(__dirname, "ui");
|
|
130
|
+
let editor_path = path.join(__dirname, "editor", "dist");
|
|
130
131
|
let git_token = "";
|
|
131
132
|
let command = ""; // Our command to run
|
|
132
133
|
let build_version = "";
|
|
@@ -210,6 +211,9 @@
|
|
|
210
211
|
if (command.toLowerCase() === "serve") {
|
|
211
212
|
const server = require(path.join(__dirname, "hdoc-serve.js"));
|
|
212
213
|
server.run(ui_path, source_path);
|
|
214
|
+
} else if (command.toLowerCase() === "edit") {
|
|
215
|
+
const editor = require(path.join(__dirname, "hdoc-edit.js"));
|
|
216
|
+
editor.run(editor_path, source_path);
|
|
213
217
|
} else if (command.toLowerCase() === "build") {
|
|
214
218
|
const builder = require(path.join(__dirname, "hdoc-build.js"));
|
|
215
219
|
await builder.run(
|