hdoc-tools 0.52.1 → 0.54.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 +83 -12
- package/hdoc-content-routes.js +343 -0
- package/hdoc-edit.js +5 -0
- package/hdoc-serve.js +11 -0
- package/npm-shrinkwrap.json +66 -2
- package/package.json +3 -1
- package/ui/js/doc.hornbill.js +99 -0
- package/ui/js/mermaid.min.js +3435 -0
package/hdoc-build.js
CHANGED
|
@@ -97,11 +97,32 @@
|
|
|
97
97
|
|
|
98
98
|
// Mutable references updated immediately before each md.render() call so
|
|
99
99
|
// the highlight and frontmatter plugin callbacks can identify the current
|
|
100
|
-
// file.
|
|
101
|
-
//
|
|
100
|
+
// file. md.render() is synchronous, so these are safe to READ from within
|
|
101
|
+
// the render itself (e.g. the highlight callback reading currentMdFilePath).
|
|
102
|
+
// They are NOT safe to read back AFTER an await: transform_file runs up to 8
|
|
103
|
+
// concurrently, so a concurrent render can clobber these while we're awaiting.
|
|
104
|
+
// Anything needed past the first await must be snapshotted into a local first
|
|
105
|
+
// (see file_frontmatter in transform_file).
|
|
102
106
|
let currentMdFilePath = "";
|
|
103
107
|
let currentFrontmatter = "";
|
|
104
108
|
|
|
109
|
+
// Escape a Mermaid definition for safe embedding as the text content of a
|
|
110
|
+
// <pre class="mermaid"> element. The viewer's client-side plugin reads the
|
|
111
|
+
// element's textContent, so only HTML-text safety is required here.
|
|
112
|
+
const escape_html_text = (str) =>
|
|
113
|
+
str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
114
|
+
|
|
115
|
+
// PDF-only: replace each client-render <pre class="mermaid" data-pdf-svg="…">
|
|
116
|
+
// block with the baked <img> pointing at its server-rendered SVG. Runs on the
|
|
117
|
+
// PDF HTML variant just before generate_pdf; the published HTML keeps the
|
|
118
|
+
// <pre> block for client rendering.
|
|
119
|
+
const pdfify_mermaid_blocks = (html) =>
|
|
120
|
+
html.replace(
|
|
121
|
+
/<pre class="mermaid" data-pdf-svg="([^"]+)">[\s\S]*?<\/pre>/g,
|
|
122
|
+
(_m, src) =>
|
|
123
|
+
`<img class="mermaid-diagram" src="${src}" alt="Mermaid Diagram">`,
|
|
124
|
+
);
|
|
125
|
+
|
|
105
126
|
// Shared markdown-it instance — created once, reused for every file.
|
|
106
127
|
// Previously recreated per file because the highlight callback closed over
|
|
107
128
|
// the file_path parameter; it now reads currentMdFilePath instead.
|
|
@@ -111,12 +132,28 @@
|
|
|
111
132
|
linkify: true,
|
|
112
133
|
typographer: true,
|
|
113
134
|
highlight: function (str, lang) {
|
|
114
|
-
if (lang === "mermaid"
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// the
|
|
119
|
-
//
|
|
135
|
+
if (lang === "mermaid") {
|
|
136
|
+
// Published HTML (and serve) render Mermaid client-side via the
|
|
137
|
+
// viewer's Mermaid plugin: we emit the raw definition inside
|
|
138
|
+
// <pre class="mermaid"> and the viewer calls mermaid.run() after
|
|
139
|
+
// injecting the fragment (theme applied via mermaid.initialize).
|
|
140
|
+
//
|
|
141
|
+
// PDFs have no JS runtime, so when PDF generation is enabled we ALSO
|
|
142
|
+
// queue a server-rendered SVG (headless puppeteer, batched after all
|
|
143
|
+
// markdown — see flush_mermaid_queue) and tag the block with
|
|
144
|
+
// data-pdf-svg. The PDF-only transform (pdfify_mermaid_blocks) swaps
|
|
145
|
+
// those tagged blocks for the baked <img> before generate_pdf; the
|
|
146
|
+
// attribute is inert for the client-rendered HTML.
|
|
147
|
+
//
|
|
148
|
+
// Skip the SVG bake entirely when this file won't produce a PDF
|
|
149
|
+
// (PDF disabled, or this path is PDF-excluded) — otherwise we'd
|
|
150
|
+
// render headless SVGs that nothing ever consumes.
|
|
151
|
+
const needs_pdf_svg =
|
|
152
|
+
pdf_enable && !pdf_path_excluded(currentMdFilePath.relativePath);
|
|
153
|
+
if (!needs_pdf_svg) {
|
|
154
|
+
return `<pre class="mermaid">${escape_html_text(str)}</pre>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
120
157
|
const outputFileName = `mermaid-${crypto.createHash("sha256").update(str).digest("hex").slice(0, 16)}.svg`;
|
|
121
158
|
const outputPath = path.join(mermaid_images_path, outputFileName);
|
|
122
159
|
const outputLink = `/_books/${doc_id}/mermaid-images/${outputFileName}`;
|
|
@@ -135,7 +172,7 @@
|
|
|
135
172
|
});
|
|
136
173
|
}
|
|
137
174
|
|
|
138
|
-
return `<
|
|
175
|
+
return `<pre class="mermaid" data-pdf-svg="${outputLink}">${escape_html_text(str)}</pre>`;
|
|
139
176
|
}
|
|
140
177
|
}
|
|
141
178
|
});
|
|
@@ -270,6 +307,16 @@
|
|
|
270
307
|
// Render markdown into HTML
|
|
271
308
|
html_txt = md.render(md_txt);
|
|
272
309
|
|
|
310
|
+
// Capture this file's frontmatter into a local NOW, before any await.
|
|
311
|
+
// currentFrontmatter is shared module-level state populated by the mdfm
|
|
312
|
+
// callback during the (synchronous) md.render() above. transform_file runs
|
|
313
|
+
// up to 8-at-a-time (see the chunked Promise.all in run()), so the await
|
|
314
|
+
// below yields the event loop and lets a concurrent transform_file reset
|
|
315
|
+
// and overwrite currentFrontmatter — if we read it back after the await we
|
|
316
|
+
// would parse another file's frontmatter. Snapshot it while it is still
|
|
317
|
+
// guaranteed to be ours.
|
|
318
|
+
const file_frontmatter = currentFrontmatter;
|
|
319
|
+
|
|
273
320
|
// md.render() synchronously queued any Mermaid diagrams in this file.
|
|
274
321
|
// Render them to SVG now (memoized/deduped) so the files exist before
|
|
275
322
|
// PDF generation below reads them, and before validation/zipping.
|
|
@@ -285,7 +332,7 @@
|
|
|
285
332
|
let fm_contains_reading_time = false;
|
|
286
333
|
let fm_contains_description = false;
|
|
287
334
|
|
|
288
|
-
const fm_content =
|
|
335
|
+
const fm_content = file_frontmatter.split(/\r?\n/);
|
|
289
336
|
if (fm_content.length >= 0) {
|
|
290
337
|
for (fm_prop of fm_content) {
|
|
291
338
|
const fm_id = fm_prop.slice(0, fm_prop.indexOf(":"));
|
|
@@ -672,7 +719,12 @@
|
|
|
672
719
|
|
|
673
720
|
let pdf_size = 0;
|
|
674
721
|
if (pdf_enable && !pdf_path_excluded(file_path.relativePath)) {
|
|
675
|
-
|
|
722
|
+
// Swap client-render Mermaid <pre> blocks for baked SVG <img> (PDF has
|
|
723
|
+
// no JS runtime), then inline the SVG files via process_images.
|
|
724
|
+
let pdf_txt = await hdoc_build_pdf.process_images(
|
|
725
|
+
file_path,
|
|
726
|
+
pdfify_mermaid_blocks(html_txt),
|
|
727
|
+
);
|
|
676
728
|
pdf_txt = `${pdf_header}\n${pdf_txt}`;
|
|
677
729
|
|
|
678
730
|
// Generate PDF file from HTML
|
|
@@ -696,6 +748,11 @@
|
|
|
696
748
|
if (inline_content) html_txt = `${fm_header_str}\n${html_txt}`;
|
|
697
749
|
else html_txt = `${fm_header_str}\n${doc_header}\n${html_txt}`;
|
|
698
750
|
|
|
751
|
+
// The data-pdf-svg attr was only needed by the PDF transform (already run
|
|
752
|
+
// above). Strip it from the published HTML so the markup is clean and has no
|
|
753
|
+
// dangling reference to the mermaid-images/ dir (which is deleted before zip).
|
|
754
|
+
html_txt = html_txt.replace(/ data-pdf-svg="[^"]*"/g, "");
|
|
755
|
+
|
|
699
756
|
// Determine output file path (.md → .html for markdown; same path for static HTML)
|
|
700
757
|
const target_file = is_markdown
|
|
701
758
|
? file_path.path.replace(path.extname(file_path.path), ".html")
|
|
@@ -716,7 +773,13 @@
|
|
|
716
773
|
relative_path = relative_path.replace("/index.html", "");
|
|
717
774
|
}
|
|
718
775
|
|
|
719
|
-
|
|
776
|
+
// Drop Mermaid source from the search index — it's diagram markup, not
|
|
777
|
+
// prose, and would otherwise pollute results.
|
|
778
|
+
const index_html = html_txt.replace(
|
|
779
|
+
/<pre class="mermaid"[^>]*>[\s\S]*?<\/pre>/g,
|
|
780
|
+
"",
|
|
781
|
+
);
|
|
782
|
+
const index_data = hdoc_index.transform_html_for_index(index_html);
|
|
720
783
|
for (const section of index_data.sections) {
|
|
721
784
|
index_records.push({
|
|
722
785
|
relative_path: relative_path,
|
|
@@ -1304,6 +1367,14 @@
|
|
|
1304
1367
|
// transform_file (no-op if all are done — rendering is memoized per diagram).
|
|
1305
1368
|
await flush_mermaid_queue();
|
|
1306
1369
|
|
|
1370
|
+
// Mermaid SVGs are only an intermediate for baking diagrams into PDFs
|
|
1371
|
+
// (process_images inlines them as base64 data URIs at PDF-gen time, already
|
|
1372
|
+
// done above). Published HTML renders Mermaid client-side and references no
|
|
1373
|
+
// SVG, so the directory is dead weight in the output — drop it before zip.
|
|
1374
|
+
if (mermaid_images_path && fs.existsSync(mermaid_images_path)) {
|
|
1375
|
+
fs.rmSync(mermaid_images_path, { recursive: true, force: true });
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1307
1378
|
// Output to console
|
|
1308
1379
|
console.log(`\n MD files found: ${conversion_attempted}`);
|
|
1309
1380
|
console.log(`Successfully converted to HTML: ${conversion_success}`);
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// Shared /_books/* content + render routes.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from hdoc-serve.js so that both `hdoc serve` (read-only preview)
|
|
4
|
+
// and `hdoc edit` (the upcoming editor surface) mount the SAME content
|
|
5
|
+
// pipeline and renderer. This guarantees preview fidelity == published output
|
|
6
|
+
// for any UI built on top of it.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// const { create_content_handler } = require("./hdoc-content-routes.js");
|
|
10
|
+
// const content = create_content_handler({
|
|
11
|
+
// source_path, docId, hdocbook_config, hdocbook_project, nav_inline,
|
|
12
|
+
// });
|
|
13
|
+
// content.register(app);
|
|
14
|
+
// // ...and reuse content.send_content_file / content.send_content_resource_404
|
|
15
|
+
// // for your own catch-all (SPA) route.
|
|
16
|
+
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
const path = require("node:path");
|
|
19
|
+
const stream = require("node:stream");
|
|
20
|
+
|
|
21
|
+
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
22
|
+
const mdfm = require("markdown-it-front-matter");
|
|
23
|
+
|
|
24
|
+
// Escape a Mermaid definition for safe embedding as the text content of a
|
|
25
|
+
// <pre> element. The viewer's client-side Mermaid plugin reads the element's
|
|
26
|
+
// textContent and renders it in the browser, so we only need HTML-text safety
|
|
27
|
+
// here (no theme/frontmatter injection — theme is applied via mermaid.initialize
|
|
28
|
+
// in the viewer).
|
|
29
|
+
function escape_html(str) {
|
|
30
|
+
return str
|
|
31
|
+
.replace(/&/g, "&")
|
|
32
|
+
.replace(/</g, "<")
|
|
33
|
+
.replace(/>/g, ">");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build a content handler bound to a single book's context.
|
|
37
|
+
//
|
|
38
|
+
// ctx:
|
|
39
|
+
// source_path - absolute path to the document source root
|
|
40
|
+
// docId - id of the hdocbook being served
|
|
41
|
+
// hdocbook_config - parsed hdocbook.json
|
|
42
|
+
// hdocbook_project - parsed hdocbook-project.json (used for redirects)
|
|
43
|
+
// nav_inline - inline-help nav fragment for library.json
|
|
44
|
+
exports.create_content_handler = (ctx) => {
|
|
45
|
+
const {
|
|
46
|
+
source_path,
|
|
47
|
+
docId,
|
|
48
|
+
hdocbook_config,
|
|
49
|
+
hdocbook_project,
|
|
50
|
+
nav_inline,
|
|
51
|
+
} = ctx;
|
|
52
|
+
|
|
53
|
+
// process_includes resolves include paths relative to the source root.
|
|
54
|
+
const global_source_path = source_path;
|
|
55
|
+
|
|
56
|
+
// Render markdown SOURCE to HTML through the same pipeline as published output:
|
|
57
|
+
// expand_variables -> process_includes -> markdown-it (+ mermaid, tips, frontmatter).
|
|
58
|
+
//
|
|
59
|
+
// file_path anchors include resolution (relative to it) and mermaid logging.
|
|
60
|
+
// md_source is the raw markdown to render — read from disk for serve/publish,
|
|
61
|
+
// or supplied from the editor buffer for live preview (so preview == published).
|
|
62
|
+
// Returns { html, frontmatter } where frontmatter is the parsed object or null.
|
|
63
|
+
async function render_markdown(file_path, md_source) {
|
|
64
|
+
let md_txt = hdoc.expand_variables(md_source.toString(), docId);
|
|
65
|
+
|
|
66
|
+
const includes_processed = await hdoc.process_includes(
|
|
67
|
+
file_path,
|
|
68
|
+
md_txt,
|
|
69
|
+
global_source_path,
|
|
70
|
+
);
|
|
71
|
+
md_txt = includes_processed.body;
|
|
72
|
+
|
|
73
|
+
// Mermaid diagrams are rendered client-side by the viewer's Mermaid plugin.
|
|
74
|
+
// We emit the raw definition inside <pre class="mermaid"> and the viewer
|
|
75
|
+
// calls mermaid.run() after injecting the fragment (theme applied there via
|
|
76
|
+
// mermaid.initialize). This keeps serve == published and drops the
|
|
77
|
+
// per-request headless browser entirely.
|
|
78
|
+
const md = require("markdown-it")({
|
|
79
|
+
html: true,
|
|
80
|
+
linkify: true,
|
|
81
|
+
typographer: true,
|
|
82
|
+
highlight: function (str, lang) {
|
|
83
|
+
if (lang === "mermaid") {
|
|
84
|
+
return `<pre class="mermaid">${escape_html(str)}</pre>`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
md.linkify.set({
|
|
89
|
+
fuzzyEmail: false,
|
|
90
|
+
fuzzyLink: false,
|
|
91
|
+
fuzzyIP: false,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let frontmatter_content = "";
|
|
95
|
+
md.use(mdfm, (fm) => {
|
|
96
|
+
frontmatter_content = fm;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const tips = require(`${__dirname}/custom_modules/tips.js`);
|
|
100
|
+
md.use(tips, { links: true });
|
|
101
|
+
|
|
102
|
+
const html = md.render(md_txt.toString());
|
|
103
|
+
|
|
104
|
+
const frontmatter = frontmatter_content.length
|
|
105
|
+
? hdoc.parse_yaml(frontmatter_content)
|
|
106
|
+
: null;
|
|
107
|
+
return { html, frontmatter };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function transform_markdown_and_send_html(req, res, file_path) {
|
|
111
|
+
if (!fs.existsSync(file_path)) return false;
|
|
112
|
+
|
|
113
|
+
const { html, frontmatter } = await render_markdown(
|
|
114
|
+
file_path,
|
|
115
|
+
fs.readFileSync(file_path).toString(),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (frontmatter) {
|
|
119
|
+
const base64 = Buffer.from(JSON.stringify(frontmatter), "utf-8").toString(
|
|
120
|
+
"base64",
|
|
121
|
+
);
|
|
122
|
+
res.setHeader("X-frontmatter", base64);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
res.setHeader("Content-Type", "text/html");
|
|
126
|
+
res.send(html);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
function send_content_file(req, res, file_path, redirected = false) {
|
|
132
|
+
let content_txt = hdoc.expand_variables(
|
|
133
|
+
fs.readFileSync(file_path).toString(),
|
|
134
|
+
docId,
|
|
135
|
+
);
|
|
136
|
+
if (redirected)
|
|
137
|
+
content_txt = `Redirected from ${redirected}\n\n${content_txt}`;
|
|
138
|
+
|
|
139
|
+
const contentType = hdoc.content_type_for_ext(path.extname(file_path));
|
|
140
|
+
|
|
141
|
+
if (path.extname(file_path) === ".md") {
|
|
142
|
+
res.setHeader("Content-Disposition", "inline");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
res.setHeader("Content-Type", contentType);
|
|
146
|
+
|
|
147
|
+
res.send(content_txt);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function send_file(req, res, file_path) {
|
|
151
|
+
// Need to set the content type here??
|
|
152
|
+
const contentType = hdoc.content_type_for_ext(path.extname(file_path));
|
|
153
|
+
res.setHeader("Content-Type", contentType);
|
|
154
|
+
|
|
155
|
+
// The vendored Mermaid bundle is large (~4.5MB) and content-stable, so cache
|
|
156
|
+
// it hard — without this the browser re-downloads it on every full page load
|
|
157
|
+
// when clicking through to diagram pages. Other dev assets stay uncached so
|
|
158
|
+
// edits to the viewer JS are picked up on refresh.
|
|
159
|
+
if (path.basename(file_path) === "mermaid.min.js") {
|
|
160
|
+
res.setHeader("Cache-Control", "public, max-age=604800, immutable");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const r = fs.createReadStream(file_path);
|
|
164
|
+
const ps = new stream.PassThrough();
|
|
165
|
+
stream.pipeline(r, ps, (err) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
console.error(err); // No such file or any other kind of error
|
|
168
|
+
return res.sendStatus(400).send("Unexpected error");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
ps.pipe(res);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function send_content_resource_404(req, res) {
|
|
175
|
+
res.setHeader("Content-Type", "text/html");
|
|
176
|
+
res.status(404).send("Content resource not found");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 1. If we request a file with a .html file extension, and that file DOES NOT exist,
|
|
180
|
+
// we will look for the same file but with a .md extension. If we find that
|
|
181
|
+
// corresponding markdown file, we will transform that markdown file to HTML and
|
|
182
|
+
// return the HTML content
|
|
183
|
+
//
|
|
184
|
+
// 2. If we request a file, without any file extension then we will look for that file
|
|
185
|
+
// with a .md extension, and if that file exists, we will transform that markdown
|
|
186
|
+
// file to HTML and return that file.
|
|
187
|
+
//
|
|
188
|
+
// For all other requests, we are going to look on the filesystem. If we request
|
|
189
|
+
// a specific file with its extension (including .md files), then we will simply
|
|
190
|
+
// return the file verbatim as a static file.
|
|
191
|
+
//
|
|
192
|
+
// If we request a file without an extension and that file does not exist, we will
|
|
193
|
+
// assume that is a folder, will append index.html and look for that file, if present
|
|
194
|
+
// we will send it, if not present we will look for index.md, and if thats present
|
|
195
|
+
// we will transform to HTML and return that
|
|
196
|
+
//
|
|
197
|
+
// Anything else in this handler will return a 404 error
|
|
198
|
+
function handle_books_request(req, res) {
|
|
199
|
+
let url = req.url.replace("/_books/", "/");
|
|
200
|
+
|
|
201
|
+
console.log("URL Requested:", url);
|
|
202
|
+
|
|
203
|
+
// Process redirect
|
|
204
|
+
if (
|
|
205
|
+
hdocbook_project.redirects &&
|
|
206
|
+
Array.isArray(hdocbook_project.redirects) &&
|
|
207
|
+
hdocbook_project.redirects.length > 0
|
|
208
|
+
) {
|
|
209
|
+
const source_url = url.indexOf("/") === 0 ? url : `/${url}`;
|
|
210
|
+
for (const redir of hdocbook_project.redirects) {
|
|
211
|
+
redir.url =
|
|
212
|
+
redir.url.indexOf("/") === 0 ? redir.url : `/${redir.url}`;
|
|
213
|
+
if (
|
|
214
|
+
redir.url === source_url &&
|
|
215
|
+
redir.location &&
|
|
216
|
+
redir.location !== ""
|
|
217
|
+
) {
|
|
218
|
+
url = `${redir.location}`;
|
|
219
|
+
console.log(`Redirecting to ${url}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const file_path = path.join(source_path, url);
|
|
225
|
+
|
|
226
|
+
if (path.extname(file_path) === ".html") {
|
|
227
|
+
// 1a. check for html files, and send/transform as required
|
|
228
|
+
if (fs.existsSync(file_path)) {
|
|
229
|
+
// HTML file exists on disk, just return it verbatim
|
|
230
|
+
res.setHeader("Content-Type", "text/html");
|
|
231
|
+
send_file(req, res, file_path);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (fs.existsSync(file_path.replace(".html", ".md"))) {
|
|
235
|
+
if (
|
|
236
|
+
transform_markdown_and_send_html(
|
|
237
|
+
req,
|
|
238
|
+
res,
|
|
239
|
+
file_path.replace(".html", ".md"),
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if (path.extname(file_path) === ".md") {
|
|
246
|
+
// If the markdown file exists, just send to caller as is
|
|
247
|
+
if (fs.existsSync(file_path)) {
|
|
248
|
+
send_content_file(req, res, file_path);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
} else if (path.extname(file_path).length === 0) {
|
|
252
|
+
// 2. If we request a file, without any file extension
|
|
253
|
+
if (fs.existsSync(`${file_path}.md`)) {
|
|
254
|
+
if (transform_markdown_and_send_html(req, res, `${file_path}.md`)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
} else if (fs.existsSync(path.join(`${file_path}index.md`))) {
|
|
258
|
+
if (
|
|
259
|
+
transform_markdown_and_send_html(
|
|
260
|
+
req,
|
|
261
|
+
res,
|
|
262
|
+
path.join(file_path, "index.md"),
|
|
263
|
+
)
|
|
264
|
+
) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} else if (fs.existsSync(path.join(`${file_path}index.html`))) {
|
|
268
|
+
res.setHeader("Content-Type", "text/html");
|
|
269
|
+
send_content_file(req, res, path.join(`${file_path}index.html`));
|
|
270
|
+
return;
|
|
271
|
+
} else if (fs.existsSync(`${file_path}/index.md`)) {
|
|
272
|
+
if (
|
|
273
|
+
transform_markdown_and_send_html(req, res, `${file_path}/index.md`)
|
|
274
|
+
) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
} else if (fs.existsSync(path.join(`${file_path}/index.html`))) {
|
|
278
|
+
res.setHeader("Content-Type", "text/html");
|
|
279
|
+
send_content_file(req, res, path.join(`${file_path}/index.html`));
|
|
280
|
+
return;
|
|
281
|
+
} else if (fs.existsSync(path.join(`${file_path}.html`))) {
|
|
282
|
+
res.setHeader("Content-Type", "text/html");
|
|
283
|
+
send_content_file(req, res, path.join(`${file_path}.html`));
|
|
284
|
+
return;
|
|
285
|
+
} else if (fs.existsSync(path.join(`${file_path}.htm`))) {
|
|
286
|
+
res.setHeader("Content-Type", "text/html");
|
|
287
|
+
send_content_file(req, res, path.join(`${file_path}.htm`));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
} else if (fs.existsSync(file_path)) {
|
|
291
|
+
if (
|
|
292
|
+
file_path.endsWith("hdocbook.json") ||
|
|
293
|
+
file_path.endsWith("hdocbook_project.json")
|
|
294
|
+
) {
|
|
295
|
+
try {
|
|
296
|
+
// Read & parse file
|
|
297
|
+
JSON.parse(fs.readFileSync(file_path));
|
|
298
|
+
} catch (e) {
|
|
299
|
+
console.error(`Error parsing hdocbook.json: ${e}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
send_file(req, res, file_path);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Return a 404 error here
|
|
307
|
+
send_content_resource_404(req, res);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function handle_library_request(req, res) {
|
|
311
|
+
const library = {
|
|
312
|
+
books: [
|
|
313
|
+
{
|
|
314
|
+
docId: hdocbook_config.docId,
|
|
315
|
+
title: hdocbook_config.title,
|
|
316
|
+
nav_inline: nav_inline,
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
res.setHeader("Content-Type", "application/json");
|
|
321
|
+
res.send(JSON.stringify(library, null, 3));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function register(app) {
|
|
325
|
+
app.get("/_books/library.json", handle_library_request);
|
|
326
|
+
app.get("/_books/*splat", handle_books_request);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
register,
|
|
331
|
+
// Exposed so host servers can reuse the same helpers for their own
|
|
332
|
+
// (SPA / editor) catch-all routes, and the editor's live-preview endpoint.
|
|
333
|
+
// handle_books_request / handle_library_request are exposed so the editor
|
|
334
|
+
// can register delegating /_books routes that follow a workspace switch.
|
|
335
|
+
render_markdown,
|
|
336
|
+
transform_markdown_and_send_html,
|
|
337
|
+
send_content_file,
|
|
338
|
+
send_file,
|
|
339
|
+
send_content_resource_404,
|
|
340
|
+
handle_books_request,
|
|
341
|
+
handle_library_request,
|
|
342
|
+
};
|
|
343
|
+
};
|
package/hdoc-edit.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
(() => {
|
|
17
17
|
const express = require("express");
|
|
18
|
+
const compression = require("compression");
|
|
18
19
|
const fs = require("node:fs");
|
|
19
20
|
const path = require("node:path");
|
|
20
21
|
const crypto = require("node:crypto");
|
|
@@ -55,6 +56,10 @@
|
|
|
55
56
|
|
|
56
57
|
const app = express();
|
|
57
58
|
|
|
59
|
+
// gzip all responses (Accept-Encoding aware). Matters most for the ~4.5MB
|
|
60
|
+
// client Mermaid bundle, which compresses to ~1.2MB on the wire.
|
|
61
|
+
app.use(compression());
|
|
62
|
+
|
|
58
63
|
// --- Active workspace (book) ---
|
|
59
64
|
// The book context is MUTABLE so the editor can switch which local clone
|
|
60
65
|
// it edits at runtime (the GitHub-Desktop "Open" flow). Every route below
|
package/hdoc-serve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const express = require("express");
|
|
3
|
+
const compression = require("compression");
|
|
3
4
|
const fs = require("node:fs");
|
|
4
5
|
const path = require("node:path");
|
|
5
6
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
@@ -78,6 +79,10 @@
|
|
|
78
79
|
// Get an express server instance
|
|
79
80
|
const app = express();
|
|
80
81
|
|
|
82
|
+
// gzip all responses (Accept-Encoding aware). Matters most for the ~4.5MB
|
|
83
|
+
// client Mermaid bundle, which compresses to ~1.2MB on the wire.
|
|
84
|
+
app.use(compression());
|
|
85
|
+
|
|
81
86
|
// Get the path of the book.json file
|
|
82
87
|
const hdocbook_path = path.join(source_path, docId, "hdocbook.json");
|
|
83
88
|
|
|
@@ -115,6 +120,12 @@
|
|
|
115
120
|
|
|
116
121
|
// If the file exists, send it.
|
|
117
122
|
if (fs.existsSync(ui_file_path)) {
|
|
123
|
+
// Stream the large Mermaid bundle (skips per-request variable
|
|
124
|
+
// expansion over ~4.5MB) and let send_file set a long cache header.
|
|
125
|
+
if (path.basename(ui_file_path) === "mermaid.min.js") {
|
|
126
|
+
content.send_file(req, res, ui_file_path);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
118
129
|
content.send_content_file(req, res, ui_file_path);
|
|
119
130
|
return;
|
|
120
131
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hdoc-tools",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.53.0",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"archiver": "8.0.0",
|
|
16
16
|
"better-sqlite3": "12.11.1",
|
|
17
17
|
"cheerio": "1.2.0",
|
|
18
|
+
"compression": "^1.8.1",
|
|
18
19
|
"express": "5.2.1",
|
|
19
20
|
"markdown-it": "14.2.0",
|
|
20
21
|
"markdown-it-container": "4.0.0",
|
|
@@ -954,6 +955,60 @@
|
|
|
954
955
|
"node": ">=18"
|
|
955
956
|
}
|
|
956
957
|
},
|
|
958
|
+
"node_modules/compressible": {
|
|
959
|
+
"version": "2.0.18",
|
|
960
|
+
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
|
961
|
+
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
|
962
|
+
"license": "MIT",
|
|
963
|
+
"dependencies": {
|
|
964
|
+
"mime-db": ">= 1.43.0 < 2"
|
|
965
|
+
},
|
|
966
|
+
"engines": {
|
|
967
|
+
"node": ">= 0.6"
|
|
968
|
+
}
|
|
969
|
+
},
|
|
970
|
+
"node_modules/compression": {
|
|
971
|
+
"version": "1.8.1",
|
|
972
|
+
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
|
973
|
+
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
|
974
|
+
"license": "MIT",
|
|
975
|
+
"dependencies": {
|
|
976
|
+
"bytes": "3.1.2",
|
|
977
|
+
"compressible": "~2.0.18",
|
|
978
|
+
"debug": "2.6.9",
|
|
979
|
+
"negotiator": "~0.6.4",
|
|
980
|
+
"on-headers": "~1.1.0",
|
|
981
|
+
"safe-buffer": "5.2.1",
|
|
982
|
+
"vary": "~1.1.2"
|
|
983
|
+
},
|
|
984
|
+
"engines": {
|
|
985
|
+
"node": ">= 0.8.0"
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
"node_modules/compression/node_modules/debug": {
|
|
989
|
+
"version": "2.6.9",
|
|
990
|
+
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
|
991
|
+
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
|
992
|
+
"license": "MIT",
|
|
993
|
+
"dependencies": {
|
|
994
|
+
"ms": "2.0.0"
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
"node_modules/compression/node_modules/ms": {
|
|
998
|
+
"version": "2.0.0",
|
|
999
|
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
|
1000
|
+
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
|
1001
|
+
"license": "MIT"
|
|
1002
|
+
},
|
|
1003
|
+
"node_modules/compression/node_modules/negotiator": {
|
|
1004
|
+
"version": "0.6.4",
|
|
1005
|
+
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
|
1006
|
+
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
|
1007
|
+
"license": "MIT",
|
|
1008
|
+
"engines": {
|
|
1009
|
+
"node": ">= 0.6"
|
|
1010
|
+
}
|
|
1011
|
+
},
|
|
957
1012
|
"node_modules/content-disposition": {
|
|
958
1013
|
"version": "1.1.0",
|
|
959
1014
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
|
@@ -2639,6 +2694,15 @@
|
|
|
2639
2694
|
"node": ">= 0.8"
|
|
2640
2695
|
}
|
|
2641
2696
|
},
|
|
2697
|
+
"node_modules/on-headers": {
|
|
2698
|
+
"version": "1.1.0",
|
|
2699
|
+
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
|
2700
|
+
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
|
2701
|
+
"license": "MIT",
|
|
2702
|
+
"engines": {
|
|
2703
|
+
"node": ">= 0.8"
|
|
2704
|
+
}
|
|
2705
|
+
},
|
|
2642
2706
|
"node_modules/once": {
|
|
2643
2707
|
"version": "1.4.0",
|
|
2644
2708
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Hornbill HDocBook Development Support Tool",
|
|
5
5
|
"main": "hdoc.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"hdoc-build-onyx.js",
|
|
15
15
|
"hdoc-build-pdf.js",
|
|
16
16
|
"hdoc-bump.js",
|
|
17
|
+
"hdoc-content-routes.js",
|
|
17
18
|
"hdoc-create.js",
|
|
18
19
|
"hdoc-edit.js",
|
|
19
20
|
"hdoc-help.js",
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"archiver": "8.0.0",
|
|
50
51
|
"better-sqlite3": "12.11.1",
|
|
51
52
|
"cheerio": "1.2.0",
|
|
53
|
+
"compression": "1.8.1",
|
|
52
54
|
"express": "5.2.1",
|
|
53
55
|
"markdown-it": "14.2.0",
|
|
54
56
|
"markdown-it-container": "4.0.0",
|