hdoc-tools 0.39.1 → 0.41.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 CHANGED
@@ -13,6 +13,9 @@
13
13
  const archiver = require("archiver");
14
14
  const xmlFormat = require("xml-formatter");
15
15
 
16
+ const { execSync } = require("child_process");
17
+ const tmp = require("tmp");
18
+
16
19
  const h_tags_to_search = ["h1", "h2", "h3"];
17
20
  const image_extensions = ["png", "svg", "jpg"];
18
21
  const doc_header_template_path = path.join(
@@ -35,6 +38,13 @@
35
38
  "templates",
36
39
  "pdf-header-non-git.html",
37
40
  );
41
+
42
+ const mermaid_theme_path = path.resolve(
43
+ __dirname,
44
+ "templates",
45
+ "mermaid-theme.yaml",
46
+ );
47
+
38
48
  const pdf_template_path = path.join(__dirname, "templates", "pdf");
39
49
  const ui_css_path = path.join(__dirname, "ui", "css");
40
50
  const pdf_template_file_path = path.join(pdf_template_path, "template.html");
@@ -52,6 +62,7 @@
52
62
  const md_files_delete = [];
53
63
  const redirects = {};
54
64
  const static_html_files = [];
65
+ const mermaid_failures = [];
55
66
 
56
67
  let bc = {}; // Breadcrumbs map
57
68
  let book_read_time = 0;
@@ -508,6 +519,38 @@
508
519
  html: true,
509
520
  linkify: true,
510
521
  typographer: true,
522
+ highlight: function (str, lang) {
523
+ if (lang === "mermaid") {
524
+ try {
525
+ const tmpInput = tmp.fileSync({ postfix: ".mmd" });
526
+ const tmpOutput = tmp.fileSync({ postfix: ".svg" });
527
+
528
+ if (!str.startsWith('---')) {
529
+ str = '---\n' + fs.readFileSync(mermaid_theme_path, {encoding: 'utf-8'}) + `\n---\n${str}`;
530
+ }
531
+
532
+ fs.writeFileSync(tmpInput.name, str);
533
+
534
+ const cmd = `${__dirname}/node_modules/.bin/mmdc -i "${tmpInput.name}" -o "${tmpOutput.name}" --quiet --backgroundColor transparent"`;
535
+ console.log(`Generating Inline Mermaid SVG found in: ${file_path.relativePath}`);
536
+ execSync(cmd);
537
+
538
+ if (!fs.existsSync(tmpOutput.name)) {
539
+ throw new Error("mmdc did not generate output");
540
+ }
541
+
542
+ const svg = fs.readFileSync(tmpOutput.name, "utf8");
543
+
544
+ tmpInput.removeCallback();
545
+ tmpOutput.removeCallback();
546
+
547
+ return `<div class="text-body">${svg}</div>`;
548
+ } catch (err) {
549
+ mermaid_failures.push({path: file_path.relativePath, error: err.message});
550
+ return ``;
551
+ }
552
+ }
553
+ }
511
554
  });
512
555
  md.linkify.set({
513
556
  fuzzyEmail: false,
@@ -1396,6 +1439,21 @@
1396
1439
  console.log(` PDF Files Created: ${pdf_created}\n`);
1397
1440
  }
1398
1441
 
1442
+
1443
+ if (mermaid_failures.length > 0) {
1444
+ console.log("\r\n-----------------------");
1445
+ console.log(" Validation Output ");
1446
+ console.log("-----------------------");
1447
+ console.log(
1448
+ `\n${mermaid_failures.length} errors found when processing markdown SVG generation:\n`,
1449
+ );
1450
+ for (const key in mermaid_failures) {
1451
+ console.error(` - ${mermaid_failures[key].path}:\n${mermaid_failures[key].error}`);
1452
+ }
1453
+ console.log("\n");
1454
+ process.exit(1);
1455
+ }
1456
+
1399
1457
  // Validate content
1400
1458
  const validation_success = await hdoc_validate.run(
1401
1459
  work_path,
package/hdoc-serve.js CHANGED
@@ -6,7 +6,16 @@
6
6
  const stream = require("node:stream");
7
7
 
8
8
  const yaml = require("js-yaml");
9
- const mdfm = require("markdown-it-front-matter");
9
+ const mdfm = require("markdown-it-front-matter");
10
+
11
+ const { execSync } = require("child_process");
12
+ const tmp = require("tmp");
13
+
14
+ const mermaid_theme_path = path.resolve(
15
+ __dirname,
16
+ "templates",
17
+ "mermaid-theme.yaml",
18
+ );
10
19
 
11
20
  let port = 3000;
12
21
  let docId;
@@ -105,78 +114,98 @@
105
114
  });
106
115
 
107
116
  async function transform_markdown_and_send_html(req, res, file_path) {
108
- if (fs.existsSync(file_path)) {
109
- // we have a markdown representation of the requested HTML file, transform and return
110
- // it to the caller
111
-
112
- // Load markdown file
113
- let md_txt = hdoc.expand_variables(
114
- fs.readFileSync(file_path).toString(),
115
- docId,
116
- );
117
-
118
- // Pull in external includes
119
- const includes_processed = await hdoc.process_includes(
120
- file_path,
121
- md_txt,
122
- global_source_path,
123
- );
124
- md_txt = includes_processed.body;
125
- if (includes_processed.errors && includes_processed.errors.length > 0) {
126
- console.error(`Error(s) when processing includes in ${file_path}`);
127
- for (let i = 0; i < includes_processed.errors.length; i++) {
128
- console.error(includes_processed.errors[i]);
129
- }
130
- } else {
131
- if (includes_processed.success > 0) {
132
- console.log(
133
- `Includes injected into document: ${includes_processed.success}`,
134
- );
135
- }
136
- }
137
-
138
- const md = require("markdown-it")({
139
- // enable everything
140
- html: true,
141
- linkify: true,
142
- typographer: true,
143
- });
117
+ if (!fs.existsSync(file_path)) return false;
144
118
 
145
- md.linkify.set({
146
- fuzzyEmail: false,
147
- fuzzyLink: false,
148
- fuzzyIP: false,
149
- });
150
- let frontmatter_content = "";
151
- md.use(mdfm, (fm) => {
152
- frontmatter_content = fm;
153
- });
154
-
155
- const tips = require(`${__dirname}/custom_modules/tips.js`);
156
- md.use(tips, {
157
- links: true,
158
- });
159
-
160
- // Render markdown into HTML
161
- const html_txt = md.render(md_txt.toString());
119
+ let md_txt = hdoc.expand_variables(
120
+ fs.readFileSync(file_path).toString(),
121
+ docId,
122
+ );
162
123
 
163
- if (frontmatter_content.length) {
164
- const obj = yaml.load(frontmatter_content);
124
+ const includes_processed = await hdoc.process_includes(
125
+ file_path,
126
+ md_txt,
127
+ global_source_path,
128
+ );
129
+ md_txt = includes_processed.body;
130
+ const md = require("markdown-it")({
131
+ html: true,
132
+ linkify: true,
133
+ typographer: true,
134
+ highlight: function (str, lang) {
135
+ if (lang === "mermaid") {
136
+ try {
137
+ const tmpInput = tmp.fileSync({ postfix: ".mmd" });
138
+ const tmpOutput = tmp.fileSync({ postfix: ".svg" });
139
+
140
+ if (!str.startsWith('---')) {
141
+ str = '---\n' + fs.readFileSync(mermaid_theme_path, {encoding: 'utf-8'}) + `\n---\n${str}`;
142
+ }
143
+
144
+ fs.writeFileSync(tmpInput.name, str);
145
+
146
+ const cmd = `${__dirname}/node_modules/.bin/mmdc -i "${tmpInput.name}" -o "${tmpOutput.name}" --quiet --backgroundColor transparent"`;
147
+ console.log(`Generating Inline Mermaid SVG found in: ${file_path}`);
148
+ execSync(cmd);
149
+
150
+ if (!fs.existsSync(tmpOutput.name)) {
151
+ throw new Error("mmdc did not generate output");
152
+ }
153
+
154
+ const svg = fs.readFileSync(tmpOutput.name, "utf8");
155
+
156
+ tmpInput.removeCallback();
157
+ tmpOutput.removeCallback();
158
+
159
+ return `<div class="text-body">${svg}</div>`;
160
+ } catch (err) {
161
+ return `<div class="alert alert-danger mb-0 text-break"
162
+ role="alert"
163
+ data-bs-toggle="collapse"
164
+ data-bs-target="#alertContent"
165
+ aria-expanded="false"
166
+ aria-controls="alertContent">
167
+ <div>
168
+ <strong>Error!</strong> Mermaid was unable to generate the SVG expected here. Click for more details
169
+ </div>
170
+ <div class="collapse mt-3" id="alertContent">
171
+ <p class="mb-0 text-break">
172
+ ${err.message}
173
+ </p>
174
+ </div>
175
+ </div>`;
176
+ }
177
+ }
178
+ }
179
+ });
180
+ md.linkify.set({
181
+ fuzzyEmail: false,
182
+ fuzzyLink: false,
183
+ fuzzyIP: false,
184
+ });
165
185
 
166
- const buff = Buffer.from(JSON.stringify(obj), "utf-8");
186
+ let frontmatter_content = "";
187
+ md.use(mdfm, (fm) => {
188
+ frontmatter_content = fm;
189
+ });
167
190
 
168
- const base64 = buff.toString("base64");
191
+ const tips = require(`${__dirname}/custom_modules/tips.js`);
192
+ md.use(tips, { links: true });
169
193
 
170
- res.setHeader("X-frontmatter", base64);
171
- }
194
+ const html_txt = md.render(md_txt.toString());
172
195
 
173
- res.setHeader("Content-Type", "text/html");
174
- res.send(html_txt);
175
- return true;
196
+ if (frontmatter_content.length) {
197
+ const obj = yaml.load(frontmatter_content);
198
+ const buff = Buffer.from(JSON.stringify(obj), "utf-8");
199
+ const base64 = buff.toString("base64");
200
+ res.setHeader("X-frontmatter", base64);
176
201
  }
177
- return false;
202
+
203
+ res.setHeader("Content-Type", "text/html");
204
+ res.send(html_txt);
205
+ return true;
178
206
  }
179
207
 
208
+
180
209
  function send_content_file(req, res, file_path, redirected = false) {
181
210
  let content_txt = hdoc.expand_variables(
182
211
  fs.readFileSync(file_path).toString(),
@@ -386,4 +415,5 @@
386
415
  }
387
416
  });
388
417
  };
418
+
389
419
  })();
package/hdoc-validate.js CHANGED
@@ -10,6 +10,7 @@ const e = require("express");
10
10
  const hdoc = require(path.join(__dirname, "hdoc-module.js"));
11
11
  const translator = require("american-british-english-translator");
12
12
  const { trueCasePathSync } = require("true-case-path");
13
+ const puppeteer = require("puppeteer");
13
14
 
14
15
  const spellcheck_options = {
15
16
  british: true,
@@ -470,15 +471,14 @@ const e = require("express");
470
471
  const markdown_paths = getMDPathFromHtmlPath(htmlFile);
471
472
  const markdown_content = fs.readFileSync(markdown_paths.markdownPath, 'utf8');
472
473
 
473
- // Use Puppeteer to validate link address works
474
- const page = await browser.newPage();
475
-
474
+
476
475
  for (let i = 0; i < links.length; i++) {
477
476
  // Validate that link is a valid URL first
478
- if (exclude_links[links[i]]) continue;
479
477
  console.log(` - ${links[i]}`);
478
+ if (exclude_links[links[i]]) continue;
480
479
  if (global_links_checked.includes(links[i])) continue;
481
480
  global_links_checked.push(links[i]);
481
+
482
482
  const valid_url = hdoc.valid_url(links[i]);
483
483
  if (!valid_url) {
484
484
  // Could be a relative path, check
@@ -585,6 +585,8 @@ const e = require("express");
585
585
  continue;
586
586
  }
587
587
 
588
+ // Use Puppeteer to validate link address works
589
+ const page = await browser.newPage();
588
590
 
589
591
  try {
590
592
  // Set a user-agent to mimic a real browser
@@ -610,9 +612,7 @@ const e = require("express");
610
612
  });
611
613
 
612
614
  // Try loading the URL
613
- response = await page.goto(links[i], { waitUntil: 'networkidle2' }).catch(() => {
614
- // Ignore rendering errors (likely binary files like PDFs)
615
- });
615
+ response = await page.goto(links[i], { waitUntil: 'networkidle2', timeout: 10000 });
616
616
 
617
617
  if (response) {
618
618
  let status = response.status();
@@ -626,7 +626,7 @@ const e = require("express");
626
626
  }, links[i]);
627
627
  }
628
628
  if ((status < 200 || status > 299) && status !== 304) {
629
- if (status === 403 && links[i].includes(".hornbill.com")) {
629
+ if (process.env.GITHUB_ACTIONS === 'true' && status === 403 && links[i].includes(".hornbill.com")) {
630
630
  // STEVEG - do nothing here, as it always returns a 403 for Hornbill sites when accessing through GitHub Actions
631
631
  // Works totally fine locally or in hdocpub, still trying to work out what's causing this in GitHub
632
632
  } else {
@@ -648,16 +648,16 @@ const e = require("express");
648
648
  } else {
649
649
  error_message = processErrorMessage(`Issue with external link [${links[i]}]: ${e}`, markdown_paths.relativePath, markdown_content, links[i]);
650
650
  }
651
- if (hdocbook_project.validation.external_link_warnings)
651
+ if (hdocbook_project.validation.external_link_warnings || process.env.GITHUB_ACTIONS === 'true')
652
652
  warnings[htmlFile.relativePath].push(error_message);
653
653
  else
654
654
  errors[htmlFile.relativePath].push(error_message);
655
655
 
656
656
  }
657
+ // Close the headless browser tab
658
+ page.close();
657
659
  }
658
660
  }
659
- // Close the headless browser tab
660
- page.close();
661
661
  };
662
662
 
663
663
  const checkHostExistsInDNS = async (hostname) => {
@@ -1060,7 +1060,8 @@ const e = require("express");
1060
1060
 
1061
1061
 
1062
1062
  const global_links_checked = [];
1063
-
1063
+ const validateBrowser = await puppeteer.launch({ args: ['--no-sandbox'] });
1064
+
1064
1065
  for (const key in html_to_validate) {
1065
1066
  const file = html_to_validate[key];
1066
1067
  // Check for British spellings in static HTML content
@@ -1085,7 +1086,7 @@ const e = require("express");
1085
1086
  messages[file.relativePath].push("No links found in file");
1086
1087
  } else {
1087
1088
  console.log(`\r\nChecking Links in ${file.relativePath}`);
1088
- await checkLinks(source_path, file, links.href, hdocbook_config, hdocbook_project, browser, global_links_checked);
1089
+ await checkLinks(source_path, file, links.href, hdocbook_config, hdocbook_project, validateBrowser, global_links_checked);
1089
1090
  }
1090
1091
  if (links.img.length === 0) {
1091
1092
  messages[file.relativePath].push("No images found in file");
@@ -1096,6 +1097,9 @@ const e = require("express");
1096
1097
  // Check for multiple H1 tags
1097
1098
  await checkTags(file);
1098
1099
  }
1100
+
1101
+ // Close the Chromium browser instance
1102
+ await validateBrowser.close();
1099
1103
 
1100
1104
  if (gen_exclude) console.log(JSON.stringify(excl_output, null, 2));
1101
1105
 
package/hdoc.js CHANGED
@@ -8,6 +8,10 @@
8
8
  const packageFile = path.join(__dirname, "package.json");
9
9
 
10
10
  const originalConsoleLog = console.log;
11
+ const originalConsoleError = console.error;
12
+ const originalConsoleInfo = console.info;
13
+
14
+ let console_color = true;
11
15
 
12
16
  console.log = (...args) => {
13
17
  if (process.env.GITHUB_ACTIONS !== 'true') {
@@ -33,18 +37,44 @@
33
37
  console.log("\nRunning in non-GitHub Actions environment\n");
34
38
  }
35
39
 
36
- let console_color = true;
40
+ console.info = (...args) => {
41
+
42
+ if (process.env.GITHUB_ACTIONS !== 'true') {
43
+ // If not running in GitHub Actions, send args to the original console.log
44
+ if (console_color) originalConsoleInfo(`\x1b[33m${args}\x1b[0m`);
45
+ else originalConsoleInfo(...args);
46
+ } else {
47
+ // If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
48
+ const escapedArgs = args.map(arg => {
49
+ if (typeof arg === 'string') {
50
+ return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
51
+ }
52
+ return arg;
53
+ });
54
+ // Use the original console.log with escaped arguments
55
+ originalConsoleInfo(...escapedArgs);
56
+ }
37
57
 
38
- const { info, error } = console;
39
- console.info = (arg) => {
40
- if (console_color)
41
- info.call(console, `\x1b[33m${arg}\x1b[0m`);
42
- else info.call(console, arg);
43
58
  };
44
- console.error = (arg) => {
45
- if (console_color)
46
- error.call(console, `\x1b[31m${arg}\x1b[0m`);
47
- else error.call(console, arg);
59
+
60
+ console.error = (...args) => {
61
+
62
+ if (process.env.GITHUB_ACTIONS !== 'true') {
63
+ // If not running in GitHub Actions, send args to the original console.log
64
+ if (console_color) originalConsoleError(`\x1b[33m${args}\x1b[0m`);
65
+ else originalConsoleError(...args);
66
+ } else {
67
+
68
+ // If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
69
+ const escapedArgs = args.map(arg => {
70
+ if (typeof arg === 'string') {
71
+ return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
72
+ }
73
+ return arg;
74
+ });
75
+ // Use the original console.log with escaped arguments
76
+ originalConsoleError(...escapedArgs);
77
+ }
48
78
  };
49
79
 
50
80
  const getHdocPackageVersion = (packagePath) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hdoc-tools",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "Hornbill HDocBook Development Support Tool",
5
5
  "main": "hdoc.js",
6
6
  "bin": {
@@ -51,10 +51,12 @@
51
51
  "markdown-it": "14.1.0",
52
52
  "markdown-it-container": "^4.0.0",
53
53
  "markdown-it-front-matter": "^0.2.4",
54
+ "@mermaid-js/mermaid-cli": "11.6.0",
54
55
  "mime-types": "^2.1.35",
55
56
  "prompt": "^1.3.0",
56
- "puppeteer": "^22.12.1",
57
+ "puppeteer": "^23.11.1",
57
58
  "stream": "0.0.3",
59
+ "tmp": "^0.2.3",
58
60
  "true-case-path": "^2.2.1",
59
61
  "words-count": "^2.0.2",
60
62
  "xml-formatter": "^3.6.2"
@@ -0,0 +1,28 @@
1
+ config:
2
+ theme: 'base'
3
+ themeVariables:
4
+ background: transparent
5
+ fontSize: 14px
6
+ primaryColor: "#85D2FF" # Medium blue
7
+ primaryColorLight: "#85D2FF" # Light accent blue
8
+ primaryColorDark: "#0065CB" # Medium-dark blue
9
+
10
+ secondaryColor: "#219BFF" # Medium blue
11
+ secondaryColorLight: "#85D2FF" # Light accent blue
12
+ secondaryColorDark: "#0C4C8C" # Medium-dark blue
13
+
14
+ tertiaryColor: "#219BFF" # Medium blue
15
+ tertiaryColorLight: "#85D2FF" # Light accent blue
16
+ tertiaryColorDark: "#0C4C8C" # Medium-dark blue
17
+
18
+ lineColor: "#808080" # Medium grey for lines
19
+ border1: "#606060" # Darker grey for borders
20
+ textColor: "#A0A0A0" # Light grey text (visible on both)
21
+ titleColor: "#A0A0A0"
22
+ labelColor: "#404040"
23
+ loopTextColor: "#A0A0A0"
24
+ nodeTextColor: "#404040"
25
+ labelTextColor: "#404040"
26
+ pieSectionTextColor: "#404040"
27
+ nodeBorder: "#219BFF" # Match node border to primary blue
28
+ nodeBg: transparent