hdoc-tools 0.18.4 → 0.18.6

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.
Files changed (2) hide show
  1. package/hdoc-module.js +688 -593
  2. package/package.json +1 -1
package/hdoc-module.js CHANGED
@@ -1,633 +1,728 @@
1
1
  (function () {
2
- 'use strict';
3
-
4
- const axios = require('axios'),
5
- axiosRetry = require('axios-retry').default,
6
- cheerio = require('cheerio'),
7
- fs = require('fs'),
8
- https = require('https'),
9
- htmlentities = require('html-entities'),
10
- html2text = require('html-to-text'),
11
- { JSDOM } = require('jsdom'),
12
- path = require('path'),
13
- wordsCount = require('words-count').default;
14
-
15
- let includesCache = {},
16
- agent = new https.Agent({
17
- rejectUnauthorized: false
18
- }),
19
- retried = false;
20
-
21
- axiosRetry(axios, {
22
- retries: 5,
23
- shouldResetTimeout: true,
24
- retryCondition: (error) => {
25
- return !error.response.status;
26
- },
27
- onRetry: (retryCount, error, requestConfig) => {
28
- retried = true;
29
- console.info(`\n[WARNING] API call failed - ${error.message}\nEndpoint: ${requestConfig.url}\nRetrying: ${retryCount}`);
30
- },
31
- });
32
-
33
- exports.content_type_for_ext = function (ext) {
34
- switch (ext) {
35
- case '.z':
36
- return 'application/x-compress';
37
- case '.tgz':
38
- return 'application/x-compressed';
39
- case '.gz':
40
- return 'application/x-gzip';
41
- case '.zip':
42
- return 'application/x-zip-compressed';
43
- case '.xml':
44
- return 'application/xml';
45
- case '.bmp':
46
- return 'image/bmp';
47
- case '.gif':
48
- return 'image/gif';
49
- case '.jpg':
50
- return 'image/jpeg';
51
- case '.png':
52
- return 'image/png';
53
- case '.tiff':
54
- return 'image/tiff';
55
- case '.ico':
56
- return 'image/x-icon';
57
- case '.png':
58
- return 'image/png';
59
- case '.svg':
60
- return 'image/svg+xml';
61
- case '.css':
62
- return 'text/css';
63
- case '.htm':
64
- case '.html':
65
- return 'text/html';
66
- case '.txt':
67
- return 'text/plain';
68
- case '.md':
69
- return 'text/plain';
70
- case '.json':
71
- return 'application/json';
72
- case '.js':
73
- return 'application/javascript';
74
- default:
75
- return 'application/octet-stream';
76
- }
2
+ "use strict";
3
+
4
+ const axios = require("axios"),
5
+ axiosRetry = require("axios-retry").default,
6
+ cheerio = require("cheerio"),
7
+ fs = require("fs"),
8
+ https = require("https"),
9
+ htmlentities = require("html-entities"),
10
+ html2text = require("html-to-text"),
11
+ { JSDOM } = require("jsdom"),
12
+ path = require("path"),
13
+ wordsCount = require("words-count").default;
14
+
15
+ let includesCache = {},
16
+ agent = new https.Agent({
17
+ rejectUnauthorized: false,
18
+ }),
19
+ retried = false;
20
+
21
+ axiosRetry(axios, {
22
+ retries: 5,
23
+ shouldResetTimeout: true,
24
+ retryCondition: (error) => {
25
+ return !error.response.status;
26
+ },
27
+ onRetry: (retryCount, error, requestConfig) => {
28
+ retried = true;
29
+ console.info(
30
+ `\n[WARNING] API call failed - ${error.message}\nEndpoint: ${requestConfig.url}\nRetrying: ${retryCount}`
31
+ );
32
+ },
33
+ });
34
+
35
+ exports.content_type_for_ext = function (ext) {
36
+ switch (ext) {
37
+ case ".z":
38
+ return "application/x-compress";
39
+ case ".tgz":
40
+ return "application/x-compressed";
41
+ case ".gz":
42
+ return "application/x-gzip";
43
+ case ".zip":
44
+ return "application/x-zip-compressed";
45
+ case ".xml":
46
+ return "application/xml";
47
+ case ".bmp":
48
+ return "image/bmp";
49
+ case ".gif":
50
+ return "image/gif";
51
+ case ".jpg":
52
+ return "image/jpeg";
53
+ case ".png":
54
+ return "image/png";
55
+ case ".tiff":
56
+ return "image/tiff";
57
+ case ".ico":
58
+ return "image/x-icon";
59
+ case ".png":
60
+ return "image/png";
61
+ case ".svg":
62
+ return "image/svg+xml";
63
+ case ".css":
64
+ return "text/css";
65
+ case ".htm":
66
+ case ".html":
67
+ return "text/html";
68
+ case ".txt":
69
+ return "text/plain";
70
+ case ".md":
71
+ return "text/plain";
72
+ case ".json":
73
+ return "application/json";
74
+ case ".js":
75
+ return "application/javascript";
76
+ default:
77
+ return "application/octet-stream";
78
+ }
79
+ };
80
+
81
+ exports.valid_url = function (url) {
82
+ const stringIsAValidUrl = (s) => {
83
+ try {
84
+ const url_obj = new URL(s);
85
+ return url_obj;
86
+ } catch (err) {
87
+ return false;
88
+ }
77
89
  };
78
-
79
- exports.valid_url = function (url) {
80
-
81
- const stringIsAValidUrl = (s) => {
82
- try {
83
- const url_obj = new URL(s);
84
- return url_obj;
85
- } catch (err) {
86
- return false;
87
- }
88
- };
89
- return stringIsAValidUrl(url);
90
+ return stringIsAValidUrl(url);
91
+ };
92
+
93
+ exports.expand_variables = function (text, docId = "") {
94
+ if (docId !== "") {
95
+ text = text.replaceAll("{{DOC_ID}}", docId);
96
+ }
97
+ text = text.replaceAll("{{BUILD_NUMBER}}", "0");
98
+
99
+ let build_date = new Date().toISOString();
100
+ build_date = build_date.replace("T", " ");
101
+ build_date = build_date.substring(0, 19);
102
+ text = text.replaceAll("{{BUILD_DATE}}", build_date);
103
+ return text;
104
+ };
105
+
106
+ exports.process_includes = async function (file_path, body, source_path) {
107
+ let response = {
108
+ body: "",
109
+ found: 0,
110
+ success: 0,
111
+ failed: 0,
112
+ included: [],
113
+ errors: [],
90
114
  };
91
115
 
92
- exports.expand_variables = function (text, docId = '') {
93
- if (docId !== '') {
94
- text = text.replaceAll('{{DOC_ID}}', docId);
116
+ // Search body for INCLUDEs
117
+ const regexp = /\[\[INCLUDE .*]]/g;
118
+ const body_array = [...body.matchAll(regexp)];
119
+
120
+ for (let i = 0; i < body_array.length; i++) {
121
+ response.found++;
122
+
123
+ // Extract include data from array
124
+ const include_value = body_array[i][0];
125
+
126
+ let link;
127
+ try {
128
+ link = include_value.split(" ")[1];
129
+ link = link.substring(0, link.length - 2);
130
+ } catch (e) {
131
+ response.failed++;
132
+ response.errors.push(
133
+ `Error parsing INCLUDE [${include_value}] from [${file_path}]: ${err}`
134
+ );
135
+ continue;
136
+ }
137
+
138
+ if (
139
+ (link.startsWith("http://") || link.startsWith("https://")) &&
140
+ includesCache[link] !== undefined
141
+ ) {
142
+ console.log(`Serving From Cache: ${link}`);
143
+ body = body.replace(include_value, includesCache[link]);
144
+ response.success++;
145
+ continue;
146
+ }
147
+
148
+ // Validate link in INCLUDE
149
+ let file_content;
150
+ if (link.startsWith("http://") || link.startsWith("https://")) {
151
+ // Remote content to include
152
+ try {
153
+ new URL(link);
154
+ } catch (e) {
155
+ response.failed++;
156
+ response.errors.push(
157
+ `Error validating INCLUDE link [${link}] from [${file_path}]: ${e}`
158
+ );
159
+ continue;
95
160
  }
96
- text = text.replaceAll('{{BUILD_NUMBER}}', '0');
97
-
98
- let build_date = new Date().toISOString();
99
- build_date = build_date.replace('T', ' ');
100
- build_date = build_date.substring(0, 19);
101
- text = text.replaceAll('{{BUILD_DATE}}', build_date);
102
- return text;
103
- };
104
-
105
-
106
- exports.process_includes = async function (file_path, body, source_path) {
107
- let response = {
108
- body: '',
109
- found: 0,
110
- success: 0,
111
- failed: 0,
112
- included: [],
113
- errors: []
114
- };
115
-
116
- // Search body for INCLUDEs
117
- const regexp = /\[\[INCLUDE .*]]/g;
118
- const body_array = [...body.matchAll(regexp)];
119
-
120
- for (let i = 0; i < body_array.length; i++) {
121
- response.found++;
122
-
123
- // Extract include data from array
124
- const include_value = body_array[i][0];
125
-
126
- let link;
127
- try {
128
- link = include_value.split(' ')[1];
129
- link = link.substring(0, link.length - 2);
130
- } catch (e) {
131
- response.failed++;
132
- response.errors.push(`Error parsing INCLUDE [${include_value}] from [${file_path}]: ${err}`);
133
- continue;
134
- }
135
-
136
- if ((link.startsWith('http://') || link.startsWith('https://')) && includesCache[link] !== undefined) {
137
- console.log(`Serving From Cache: ${link}`);
138
- body = body.replace(include_value, includesCache[link]);
139
- response.success++;
140
- continue;
141
- }
142
161
 
143
- // Validate link in INCLUDE
144
- let file_content;
145
- if (link.startsWith('http://') || link.startsWith('https://')) {
146
- // Remote content to include
147
- try {
148
- new URL(link);
149
- } catch (e) {
150
- response.failed++;
151
- response.errors.push(`Error validating INCLUDE link [${link}] from [${file_path}]: ${e}`);
152
- continue;
153
- }
154
-
155
- try {
156
- const file_response = await axios.get(link);
157
- if (retried) {
158
- retried = false;
159
- console.log(`API call retry success!`);
160
- }
161
- if (file_response.status === 200) {
162
- file_content = file_response.data;
163
- } else {
164
- throw `Unexpected Status ${file_response.status}`;
165
- }
166
- } catch (e) {
167
- response.failed++;
168
- response.errors.push(`Error getting INCLUDE link content [${link}] from [${file_path}]: ${e}`);
169
- continue;
170
- }
171
- console.log(`Included From Remote Source: ${link}`);
172
- } else {
173
- // Local content to include
174
- try {
175
- file_content = fs.readFileSync(path.join(source_path, link), 'utf8');
176
- } catch (e) {
177
- response.failed++;
178
- response.errors.push(`Error getting INCLUDE file [${link}] from [${file_path}]: ${e}`);
179
- continue;
180
- }
181
- console.log(`Included From Local Source: ${link}`);
182
- }
183
- response.success++;
184
- includesCache[link] = file_content;
185
- body = body.replace(include_value, file_content);
162
+ try {
163
+ const file_response = await axios.get(link);
164
+ if (retried) {
165
+ retried = false;
166
+ console.log(`API call retry success!`);
167
+ }
168
+ if (file_response.status === 200) {
169
+ file_content = file_response.data;
170
+ } else {
171
+ throw `Unexpected Status ${file_response.status}`;
172
+ }
173
+ } catch (e) {
174
+ response.failed++;
175
+ response.errors.push(
176
+ `Error getting INCLUDE link content [${link}] from [${file_path}]: ${e}`
177
+ );
178
+ continue;
186
179
  }
187
- response.body = body;
188
- return response;
189
- };
190
-
191
- // Takes html, returns the first heading detected in the order provided in h_to_search
192
- // Looks for h1 tags first, then hX, hY, hZ in order
193
- exports.getFirstHTMLHeading = function (html_body, h_to_search = ['h1']) {
194
- const $ = cheerio.load(html_body);
195
- for (let i = 0; i < h_to_search.length; i++) {
196
- let heading = $(h_to_search[i]).map(function (i) {
197
- return $(this);
198
- }).get();
199
- if (heading.length > 0) {
200
- return heading[0];
201
- }
180
+ console.log(`Included From Remote Source: ${link}`);
181
+ } else {
182
+ // Local content to include
183
+ try {
184
+ file_content = fs.readFileSync(path.join(source_path, link), "utf8");
185
+ } catch (e) {
186
+ response.failed++;
187
+ response.errors.push(
188
+ `Error getting INCLUDE file [${link}] from [${file_path}]: ${e}`
189
+ );
190
+ continue;
202
191
  }
203
- return false;
204
- };
205
-
206
- const makeAnchorIdFriendly = function(str) {
207
- return 'hb-doc-anchor-' + str // Add prefix
208
- .toLowerCase() // Convert to lowercase
209
- .trim() // Trim leading and trailing spaces
210
- .replace(/[^a-z0-9\s-]/g, '') // Remove all non-alphanumeric characters except spaces and hyphens
211
- .replace(/\s+/g, '-') // Replace spaces with hyphens
212
- .replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen
213
- };
214
-
215
- // Processes HTML, wraps h2 and h3 tags and their content in divs with an id matching that of the h text
216
- exports.wrapHContent = function (htmlContent) {
217
- const dom = new JSDOM(htmlContent);
218
- const document = dom.window.document;
219
-
220
- let nodes = Array.from(document.body.childNodes); // Convert NodeList to Array for easier manipulation
221
- let newContent = document.createDocumentFragment(); // Create a document fragment to hold the new structure
222
-
223
- let currentH2Div = null;
224
- let currentH3Div = null;
225
- let standaloneH3Div = null;
226
-
227
- nodes.forEach(node => {
228
- if (node.nodeType === dom.window.Node.ELEMENT_NODE) {
229
- if (node.tagName.toLowerCase() === 'h2') {
230
- // When an <h2> is found, close the current <div> (if any) and start a new one
231
- if (currentH2Div) {
232
- newContent.appendChild(currentH2Div);
233
- }
234
- currentH2Div = document.createElement('div');
235
- currentH2Div.id = makeAnchorIdFriendly(node.textContent.trim()); // Set the id to the anchor-friendly text content of the <h2>
236
- currentH2Div.appendChild(node); // Move the <h2> into the new <div>
237
- currentH3Div = null; // Reset currentH3Div
238
- } else if (node.tagName.toLowerCase() === 'h3') {
239
- // When an <h3> is found, close the current <h3> <div> (if any) and start a new one
240
- if (currentH3Div) {
241
- if (currentH2Div) {
242
- currentH2Div.appendChild(currentH3Div);
243
- } else {
244
- newContent.appendChild(currentH3Div);
245
- }
246
- }
247
- currentH3Div = document.createElement('div');
248
- currentH3Div.id = makeAnchorIdFriendly(node.textContent.trim()); // Set the id to the anchor-friendly text content of the <h3>
249
- currentH3Div.appendChild(node); // Move the <h3> into the new <div>
250
- } else if (currentH3Div) {
251
- // Append any other nodes to the current <h3> <div> if it exists
252
- currentH3Div.appendChild(node);
253
- } else if (currentH2Div) {
254
- // Append any other nodes to the current <h2> <div> if no current <h3> <div> exists
255
- currentH2Div.appendChild(node);
256
- } else {
257
- // If there is no current <h2> or <h3> <div>, append the node directly to the fragment
258
- newContent.appendChild(node);
259
- }
260
- } else if (currentH3Div) {
261
- // Append any text nodes to the current <h3> <div> if it exists
262
- currentH3Div.appendChild(node);
263
- } else if (currentH2Div) {
264
- // Append any text nodes to the current <h2> <div> if it exists
265
- currentH2Div.appendChild(node);
266
- } else {
267
- // If there is no current <h2> or <h3> <div>, append the node directly to the fragment
268
- newContent.appendChild(node);
192
+ console.log(`Included From Local Source: ${link}`);
193
+ }
194
+ response.success++;
195
+ includesCache[link] = file_content;
196
+ body = body.replace(include_value, file_content);
197
+ }
198
+ response.body = body;
199
+ return response;
200
+ };
201
+
202
+ // Takes html, returns the first heading detected in the order provided in h_to_search
203
+ // Looks for h1 tags first, then hX, hY, hZ in order
204
+ exports.getFirstHTMLHeading = function (html_body, h_to_search = ["h1"]) {
205
+ const $ = cheerio.load(html_body);
206
+ for (let i = 0; i < h_to_search.length; i++) {
207
+ let heading = $(h_to_search[i])
208
+ .map(function (i) {
209
+ return $(this);
210
+ })
211
+ .get();
212
+ if (heading.length > 0) {
213
+ return heading[0];
214
+ }
215
+ }
216
+ return false;
217
+ };
218
+
219
+ const makeAnchorIdFriendly = function (str) {
220
+ return (
221
+ "hb-doc-anchor-" +
222
+ str // Add prefix
223
+ .toLowerCase() // Convert to lowercase
224
+ .trim() // Trim leading and trailing spaces
225
+ .replace(/[^a-z0-9\s-]/g, "") // Remove all non-alphanumeric characters except spaces and hyphens
226
+ .replace(/\s+/g, "-") // Replace spaces with hyphens
227
+ .replace(/-+/g, "-")
228
+ ); // Replace multiple hyphens with a single hyphen
229
+ };
230
+
231
+ // Processes HTML, wraps h2 and h3 tags and their content in divs with an id matching that of the h text
232
+ exports.wrapHContent = function (htmlContent) {
233
+ const dom = new JSDOM(htmlContent);
234
+ const document = dom.window.document;
235
+
236
+ let nodes = Array.from(document.body.childNodes); // Convert NodeList to Array for easier manipulation
237
+ let newContent = document.createDocumentFragment(); // Create a document fragment to hold the new structure
238
+
239
+ let currentH2Div = null;
240
+ let currentH3Div = null;
241
+
242
+ nodes.forEach((node) => {
243
+ if (node.nodeType === dom.window.Node.ELEMENT_NODE) {
244
+ if (node.tagName.toLowerCase() === "h2") {
245
+ // When an <h2> is found, close the current <h2> div (if any) and start a new one
246
+ if (currentH2Div) {
247
+ if (currentH3Div) {
248
+ currentH2Div.appendChild(currentH3Div);
249
+ currentH3Div = null;
269
250
  }
270
- });
271
-
272
- // Append the last <h3> <div> if any
273
- if (currentH3Div) {
251
+ newContent.appendChild(currentH2Div);
252
+ }
253
+ currentH2Div = document.createElement("div");
254
+ currentH2Div.id = makeAnchorIdFriendly(node.textContent.trim());
255
+ currentH2Div.appendChild(node);
256
+ } else if (node.tagName.toLowerCase() === "h3") {
257
+ // When an <h3> is found, close the current <h3> div (if any) and start a new one
258
+ if (currentH3Div) {
274
259
  if (currentH2Div) {
275
- currentH2Div.appendChild(currentH3Div);
260
+ currentH2Div.appendChild(currentH3Div);
276
261
  } else {
277
- newContent.appendChild(currentH3Div);
262
+ newContent.appendChild(currentH3Div);
278
263
  }
264
+ }
265
+ currentH3Div = document.createElement("div");
266
+ currentH3Div.id = makeAnchorIdFriendly(node.textContent.trim());
267
+ currentH3Div.appendChild(node);
268
+ } else {
269
+ if (currentH3Div) {
270
+ currentH3Div.appendChild(node);
271
+ } else if (currentH2Div) {
272
+ currentH2Div.appendChild(node);
273
+ } else {
274
+ newContent.appendChild(node);
275
+ }
279
276
  }
280
-
281
- // Append the last <h2> <div> if any
282
- if (currentH2Div) {
283
- newContent.appendChild(currentH2Div);
277
+ } else {
278
+ if (currentH3Div) {
279
+ currentH3Div.appendChild(node);
280
+ } else if (currentH2Div) {
281
+ currentH2Div.appendChild(node);
282
+ } else {
283
+ newContent.appendChild(node);
284
284
  }
285
-
286
- // Replace the old body content with the new content
287
- document.body.innerHTML = '';
288
- document.body.appendChild(newContent);
289
-
290
- // Serialize the document back to HTML and save it to a new file (for example: 'output.html')
291
- const outputHtml = dom.serialize();
292
- return outputHtml;
293
- };
285
+ }
286
+ });
294
287
 
288
+ // Append the last <h3> div if any
289
+ if (currentH3Div) {
290
+ if (currentH2Div) {
291
+ currentH2Div.appendChild(currentH3Div);
292
+ } else {
293
+ newContent.appendChild(currentH3Div);
294
+ }
295
+ }
296
+
297
+ // Append the last <h2> div if any
298
+ if (currentH2Div) {
299
+ newContent.appendChild(currentH2Div);
300
+ }
301
+
302
+ // Replace the old body content with the new content
303
+ document.body.innerHTML = "";
304
+ document.body.appendChild(newContent);
305
+
306
+ // Serialize the document back to HTML and save it to a new file (for example: 'output.html')
307
+ const outputHtml = dom.serialize();
308
+ return outputHtml;
309
+ };
310
+
311
+ exports.getIDDivs = function (html_body) {
312
+ const $ = cheerio.load(html_body, {
313
+ decodeEntities: false,
314
+ });
295
315
 
296
- exports.getIDDivs = function(html_body) {
297
- const $ = cheerio.load(html_body, {
298
- decodeEntities: false
316
+ const divs = [];
317
+
318
+ $("div").each(function (i, element) {
319
+ if (
320
+ $(this).attr("id") &&
321
+ $(this).attr("id").startsWith("hb-doc-anchor-")
322
+ ) {
323
+ divs.push({
324
+ id: $(this).attr("id"),
325
+ html: $(this).html(),
326
+ text: $(this).text(),
299
327
  });
328
+ }
329
+ });
330
+ return divs;
331
+ };
300
332
 
301
- const divs = [];
302
-
303
- $('div').each(function(i, element){
304
- if ($(this).attr('id') && $(this).attr('id').startsWith('hb-doc-anchor-')) {
305
- divs.push({id: $(this).attr('id'), html: $(this).html(), text: $(this).text()})
306
- }
307
- });
308
- return divs;
333
+ exports.getHTMLFrontmatterHeader = function (html_body) {
334
+ let response = {
335
+ fm_header: "",
336
+ fm_properties: {},
309
337
  };
310
-
311
- exports.getHTMLFrontmatterHeader = function (html_body) {
312
- let response = {
313
- fm_header: '',
314
- fm_properties: {}
315
- };
316
- const $ = cheerio.load(html_body, {
317
- decodeEntities: false
318
- });
319
- if ($._root && $._root.children && $._root.children instanceof Array && $._root.children.length > 0) {
320
- $._root.children.forEach(function (child) {
321
- if (child.type === 'comment' && child.data && child.data.startsWith('[[FRONTMATTER')) {
322
- // We have a Frontmatter header - return each property in an array
323
- const fm_properties = child.data.split(/\r?\n/);
324
- for (let i = 0; i < fm_properties.length; i++) {
325
- if (fm_properties[i].includes(':')) {
326
- const property_details = fm_properties[i].split(/:(.*)/s);
327
- if (property_details.length > 1) {
328
- let prop_val = property_details[1].trim();
329
- if (/^".*"$/.test(prop_val)) {
330
- prop_val = prop_val.substring(1, prop_val.length - 1);
331
- }
332
- if (property_details[0].trim().toLowerCase() === 'title') {
333
- prop_val = htmlentities.decode(prop_val);
334
- }
335
- response.fm_properties[property_details[0].trim().toLowerCase()] = prop_val;
336
- }
337
- }
338
- }
339
-
340
- // And return the header as a whole so it can be easily replaced
341
- response.fm_header = child.data;
338
+ const $ = cheerio.load(html_body, {
339
+ decodeEntities: false,
340
+ });
341
+ if (
342
+ $._root &&
343
+ $._root.children &&
344
+ $._root.children instanceof Array &&
345
+ $._root.children.length > 0
346
+ ) {
347
+ $._root.children.forEach(function (child) {
348
+ if (
349
+ child.type === "comment" &&
350
+ child.data &&
351
+ child.data.startsWith("[[FRONTMATTER")
352
+ ) {
353
+ // We have a Frontmatter header - return each property in an array
354
+ const fm_properties = child.data.split(/\r?\n/);
355
+ for (let i = 0; i < fm_properties.length; i++) {
356
+ if (fm_properties[i].includes(":")) {
357
+ const property_details = fm_properties[i].split(/:(.*)/s);
358
+ if (property_details.length > 1) {
359
+ let prop_val = property_details[1].trim();
360
+ if (/^".*"$/.test(prop_val)) {
361
+ prop_val = prop_val.substring(1, prop_val.length - 1);
342
362
  }
343
- });
344
- }
345
-
346
- return response;
347
- };
363
+ if (property_details[0].trim().toLowerCase() === "title") {
364
+ prop_val = htmlentities.decode(prop_val);
365
+ }
366
+ response.fm_properties[
367
+ property_details[0].trim().toLowerCase()
368
+ ] = prop_val;
369
+ }
370
+ }
371
+ }
348
372
 
349
- exports.truncate_string = function (str, n, useWordBoundary) {
350
- if (str.length <= n) {
351
- return str;
373
+ // And return the header as a whole so it can be easily replaced
374
+ response.fm_header = child.data;
352
375
  }
353
- const subString = str.slice(0, n - 1);
354
- return (useWordBoundary ? subString.slice(0, subString.lastIndexOf(" ")) : subString) + '…';
376
+ });
377
+ }
378
+
379
+ return response;
380
+ };
381
+
382
+ exports.truncate_string = function (str, n, useWordBoundary) {
383
+ if (str.length <= n) {
384
+ return str;
385
+ }
386
+ const subString = str.slice(0, n - 1);
387
+ return (
388
+ (useWordBoundary
389
+ ? subString.slice(0, subString.lastIndexOf(" "))
390
+ : subString) + "…"
391
+ );
392
+ };
393
+
394
+ exports.get_html_read_time = function (html) {
395
+ // Get word count
396
+ const text = html2text.convert(html, {
397
+ wordwrap: null,
398
+ });
399
+ const word_count = wordsCount(text);
400
+ if (word_count === 0) return 0;
401
+
402
+ // Calculate the read time - divide the word count by 200
403
+ let read_time = Math.round(word_count / 200);
404
+ if (read_time === 0) read_time = 1;
405
+ return read_time;
406
+ };
407
+
408
+ exports.get_github_api_path = function (repo, relative_path) {
409
+ if (repo) {
410
+ repo = repo.endsWith("/") ? repo.slice(0, -1) : repo;
411
+ let github_paths = {};
412
+ github_paths.api_path = repo.replace(
413
+ "https://github.com/",
414
+ "https://api.github.com/repos/"
415
+ );
416
+ github_paths.api_path +=
417
+ "/commits?path=" +
418
+ encodeURIComponent(
419
+ "/" + relative_path.replace("\\\\", "/").replace("\\", "/")
420
+ );
421
+ github_paths.edit_path =
422
+ repo +
423
+ "/blob/main/" +
424
+ relative_path.replace("\\\\", "/").replace("\\", "/");
425
+ return github_paths;
426
+ }
427
+ return "";
428
+ };
429
+
430
+ const get_github_contributors_path = function (repo) {
431
+ repo = repo.endsWith("/") ? repo.slice(0, -1) : repo;
432
+ let github_paths = {};
433
+ github_paths.api_path = repo.replace(
434
+ "https://github.com/",
435
+ "https://api.github.com/repos/"
436
+ );
437
+ github_paths.api_path += "/contributors";
438
+ return github_paths;
439
+ };
440
+
441
+ exports.get_github_contributors = async function (
442
+ github_url,
443
+ github_api_token,
444
+ repo
445
+ ) {
446
+ let response = {
447
+ success: false,
448
+ error: "",
449
+ contributors: [],
450
+ contributor_count: 0,
451
+ last_commit_date: "",
355
452
  };
356
-
357
- exports.get_html_read_time = function (html) {
358
- // Get word count
359
- const text = html2text.convert(html, {
360
- wordwrap: null
361
- });
362
- const word_count = wordsCount(text);
363
- if (word_count === 0) return 0;
364
-
365
- // Calculate the read time - divide the word count by 200
366
- let read_time = Math.round(word_count / 200);
367
- if (read_time === 0) read_time = 1;
368
- return read_time;
453
+ let contributors = {};
454
+
455
+ let request_options = {
456
+ headers: {
457
+ "User-Agent": "HornbillDocsBuild",
458
+ "Cache-Control": "no-cache",
459
+ Host: "api.github.com",
460
+ Accept: "application/json",
461
+ },
462
+ timeout: 5000,
369
463
  };
370
-
371
- exports.get_github_api_path = function (repo, relative_path) {
372
- if (repo) {
373
- repo = repo.endsWith('/') ? repo.slice(0, -1) : repo;
374
- let github_paths = {};
375
- github_paths.api_path = repo.replace('https://github.com/', 'https://api.github.com/repos/');
376
- github_paths.api_path += '/commits?path=' + encodeURIComponent('/' + relative_path.replace('\\\\', '/').replace('\\', '/'));
377
- github_paths.edit_path = repo + '/blob/main/' + relative_path.replace('\\\\', '/').replace('\\', '/');
378
- return github_paths;
464
+ if (github_api_token !== "") {
465
+ request_options.headers.authorization = `Bearer ${github_api_token}`;
466
+ }
467
+ let github_response;
468
+ try {
469
+ github_response = await axios.get(github_url, request_options);
470
+ if (retried) {
471
+ retried = false;
472
+ console.log(`API call retry success!`);
473
+ }
474
+ } catch (err) {
475
+ if (err.response) {
476
+ if (err.response.status !== 403 && err.response.status !== 401) {
477
+ response.error = err;
478
+ return response;
479
+ } else {
480
+ github_response = err.response;
379
481
  }
380
- return '';
381
- };
382
-
383
- const get_github_contributors_path = function (repo) {
384
- repo = repo.endsWith('/') ? repo.slice(0, -1) : repo;
385
- let github_paths = {};
386
- github_paths.api_path = repo.replace('https://github.com/', 'https://api.github.com/repos/');
387
- github_paths.api_path += '/contributors';
388
- return github_paths;
389
- };
390
-
391
- exports.get_github_contributors = async function (github_url, github_api_token, repo) {
392
- let response = {
393
- success: false,
394
- error: '',
395
- contributors: [],
396
- contributor_count: 0,
397
- last_commit_date: ''
398
- };
399
- let contributors = {};
400
-
401
- let request_options = {
402
- headers: {
403
- 'User-Agent': 'HornbillDocsBuild',
404
- 'Cache-Control': 'no-cache',
405
- 'Host': 'api.github.com',
406
- 'Accept': 'application/json'
407
- },
408
- timeout: 5000
409
- };
410
- if (github_api_token !== '') {
411
- request_options.headers.authorization = `Bearer ${github_api_token}`;
482
+ } else {
483
+ response.error = `Unexpected response from GitHub for [${github_url}:\n${JSON.stringify(
484
+ err
485
+ )}]`;
486
+ }
487
+ }
488
+ if (github_response.status === 200) {
489
+ response.success = true;
490
+ let commits = github_response.data;
491
+ commits.forEach(function (commit) {
492
+ if (
493
+ commit.committer &&
494
+ commit.committer.type &&
495
+ commit.committer.type.toLowerCase() === "user" &&
496
+ commit.committer.login.toLowerCase() !== "web-flow"
497
+ ) {
498
+ if (!contributors[commit.committer.id]) {
499
+ response.contributor_count++;
500
+ contributors[commit.committer.id] = {
501
+ login: commit.committer.login,
502
+ avatar_url: commit.committer.avatar_url,
503
+ html_url: commit.committer.html_url,
504
+ name: commit.commit.committer.name,
505
+ };
506
+ }
507
+ if (response.last_commit_date !== "") {
508
+ const new_commit_date = new Date(commit.commit.committer.date);
509
+ const exist_commit_date = new Date(response.last_commit_date);
510
+ if (new_commit_date > exist_commit_date)
511
+ response.last_commit_date = commit.commit.committer.date;
512
+ } else {
513
+ response.last_commit_date = commit.commit.committer.date;
514
+ }
515
+ } else if (commit.author && commit.author.id) {
516
+ if (!contributors[commit.author.id]) {
517
+ response.contributor_count++;
518
+ contributors[commit.author.id] = {
519
+ login: commit.author.login,
520
+ avatar_url: commit.author.avatar_url,
521
+ html_url: commit.author.html_url,
522
+ name: commit.commit.author.name,
523
+ };
524
+ }
525
+ if (response.last_commit_date !== "") {
526
+ const new_commit_date = new Date(commit.commit.author.date);
527
+ const exist_commit_date = new Date(response.last_commit_date);
528
+ if (new_commit_date > exist_commit_date)
529
+ response.last_commit_date = commit.commit.author.date;
530
+ } else {
531
+ response.last_commit_date = commit.commit.author.date;
532
+ }
412
533
  }
413
- let github_response;
414
- try {
415
- github_response = await axios.get(github_url, request_options);
416
- if (retried) {
417
- retried = false;
418
- console.log(`API call retry success!`);
534
+ });
535
+ for (const key in contributors) {
536
+ if (contributors.hasOwnProperty(key)) {
537
+ response.contributors.push(contributors[key]);
538
+ }
539
+ }
540
+ } else if (github_response.status === 403) {
541
+ // Private repo, fine-grained permissions don't yet support getting commits without content, get list from meta permissions
542
+ const contrib_url = get_github_contributors_path(repo).api_path;
543
+ try {
544
+ github_response = await axios.get(contrib_url, request_options);
545
+ if (retried) {
546
+ retried = false;
547
+ console.log(`API call retry success!`);
548
+ }
549
+ } catch (err) {
550
+ if (err.response && err.response.status) {
551
+ if (err.response.status !== 200) {
552
+ response.error = err;
553
+ return response;
554
+ }
555
+ } else {
556
+ response.error = `Unexpected response from GitHub for [${contrib_url}:\n${JSON.stringify(
557
+ err
558
+ )}]`;
559
+ }
560
+ }
561
+ if (github_response.status === 200) {
562
+ response.success = true;
563
+ let commits = github_response.data;
564
+ commits.forEach(function (commit) {
565
+ if (
566
+ commit.type &&
567
+ commit.type.toLowerCase() === "user" &&
568
+ commit.login.toLowerCase() !== "web-flow"
569
+ ) {
570
+ if (!contributors[commit.id]) {
571
+ response.contributor_count++;
572
+ contributors[commit.id] = {
573
+ login: commit.login,
574
+ avatar_url: commit.avatar_url,
575
+ html_url: commit.html_url,
576
+ name: commit.name ? commit.name : commit.login,
577
+ };
419
578
  }
420
- } catch (err) {
421
- if (err.response) {
422
- if (err.response.status !== 403 && err.response.status !== 401) {
423
- response.error = err;
424
- return response;
425
- } else {
426
- github_response = err.response;
427
- }
579
+ if (
580
+ response.last_commit_date !== "" &&
581
+ response.last_commit_date !== "No Commit Date Available"
582
+ ) {
583
+ const new_commit_date = new Date(commit.date);
584
+ const exist_commit_date = new Date(response.last_commit_date);
585
+ if (new_commit_date > exist_commit_date)
586
+ response.last_commit_date = commit.date;
428
587
  } else {
429
- response.error = `Unexpected response from GitHub for [${github_url}:\n${JSON.stringify(err)}]`
588
+ response.last_commit_date = commit.date
589
+ ? commit.date
590
+ : "No Commit Date Available";
430
591
  }
592
+ }
593
+ });
594
+ for (const key in contributors) {
595
+ if (contributors.hasOwnProperty(key)) {
596
+ response.contributors.push(contributors[key]);
597
+ }
598
+ }
599
+ }
600
+ } else {
601
+ response.error = `Unexpected Status: ${github_response.status}.`;
602
+ }
603
+ return response;
604
+ };
605
+
606
+ exports.strip_drafts = function (nav_items) {
607
+ let return_nav = nav_items;
608
+ recurse_nav(return_nav);
609
+ return return_nav;
610
+ };
611
+
612
+ const recurse_nav = function (nav_items) {
613
+ for (const key in nav_items) {
614
+ if (nav_items[key].draft) {
615
+ nav_items.splice(key, 1);
616
+ recurse_nav(nav_items);
617
+ } else if (nav_items[key].items) {
618
+ recurse_nav(nav_items[key].items);
619
+ }
620
+ }
621
+ };
622
+
623
+ exports.build_breadcrumbs = function (nav_items) {
624
+ let response = {
625
+ bc: {},
626
+ errors: [],
627
+ };
628
+ const buildBreadcrumb = (items, parentLinks) => {
629
+ // Process parent links
630
+ let parentlink = true;
631
+ if (parentLinks.length > 0) {
632
+ if (parentLinks[0].link === undefined || parentLinks[0].link === "")
633
+ parentlink = false;
634
+ if (
635
+ parentLinks[1] &&
636
+ parentLinks[1].link === undefined &&
637
+ items.length > 0 &&
638
+ items[0].link
639
+ ) {
640
+ parentLinks[1].link = items[0].link;
641
+ }
642
+ if (
643
+ parentLinks[2] &&
644
+ parentLinks[2].link === undefined &&
645
+ items.length > 0 &&
646
+ items[0].link
647
+ ) {
648
+ parentLinks[2].link = items[0].link;
649
+ }
650
+ }
651
+
652
+ // Loop through items, build breadcrumb
653
+ for (let i = 0; i < items.length; i++) {
654
+ if (!items[i].text) {
655
+ response.errors.push(
656
+ `The following Nav Item is missing its text property: ${JSON.stringify(
657
+ items[i]
658
+ )}`
659
+ );
431
660
  }
432
- if (github_response.status === 200) {
433
- response.success = true;
434
- let commits = github_response.data;
435
- commits.forEach(function (commit) {
436
- if (commit.committer && commit.committer.type && commit.committer.type.toLowerCase() === 'user' && commit.committer.login.toLowerCase() !== 'web-flow') {
437
- if (!contributors[commit.committer.id]) {
438
- response.contributor_count++;
439
- contributors[commit.committer.id] = {
440
- login: commit.committer.login,
441
- avatar_url: commit.committer.avatar_url,
442
- html_url: commit.committer.html_url,
443
- name: commit.commit.committer.name
444
- };
445
- }
446
- if (response.last_commit_date !== '') {
447
- const new_commit_date = new Date(commit.commit.committer.date);
448
- const exist_commit_date = new Date(response.last_commit_date);
449
- if (new_commit_date > exist_commit_date) response.last_commit_date = commit.commit.committer.date;
450
- } else {
451
- response.last_commit_date = commit.commit.committer.date;
452
- }
453
- } else if (commit.author && commit.author.id) {
454
- if (!contributors[commit.author.id]) {
455
- response.contributor_count++;
456
- contributors[commit.author.id] = {
457
- login: commit.author.login,
458
- avatar_url: commit.author.avatar_url,
459
- html_url: commit.author.html_url,
460
- name: commit.commit.author.name
461
- };
462
- }
463
- if (response.last_commit_date !== '') {
464
- const new_commit_date = new Date(commit.commit.author.date);
465
- const exist_commit_date = new Date(response.last_commit_date);
466
- if (new_commit_date > exist_commit_date) response.last_commit_date = commit.commit.author.date;
467
- } else {
468
- response.last_commit_date = commit.commit.author.date;
469
- }
470
- }
471
- });
472
- for (const key in contributors) {
473
- if (contributors.hasOwnProperty(key)) {
474
- response.contributors.push(contributors[key]);
475
- }
476
- }
477
- } else if (github_response.status === 403) {
478
- // Private repo, fine-grained permissions don't yet support getting commits without content, get list from meta permissions
479
- const contrib_url = get_github_contributors_path(repo).api_path;
480
- try {
481
- github_response = await axios.get(contrib_url, request_options);
482
- if (retried) {
483
- retried = false;
484
- console.log(`API call retry success!`);
485
- }
486
- } catch (err) {
487
- if (err.response && err.response.status) {
488
- if (err.response.status !== 200) {
489
- response.error = err;
490
- return response;
491
- }
492
- } else {
493
- response.error = `Unexpected response from GitHub for [${contrib_url}:\n${JSON.stringify(err)}]`
494
- }
495
- }
496
- if (github_response.status === 200) {
497
- response.success = true;
498
- let commits = github_response.data;
499
- commits.forEach(function (commit) {
500
- if (commit.type && commit.type.toLowerCase() === 'user' && commit.login.toLowerCase() !== 'web-flow') {
501
- if (!contributors[commit.id]) {
502
- response.contributor_count++;
503
- contributors[commit.id] = {
504
- login: commit.login,
505
- avatar_url: commit.avatar_url,
506
- html_url: commit.html_url,
507
- name: commit.name ? commit.name : commit.login
508
- };
509
- }
510
- if (response.last_commit_date !== '' && response.last_commit_date !== 'No Commit Date Available') {
511
- const new_commit_date = new Date(commit.date);
512
- const exist_commit_date = new Date(response.last_commit_date);
513
- if (new_commit_date > exist_commit_date) response.last_commit_date = commit.date;
514
- } else {
515
- response.last_commit_date = commit.date ? commit.date : 'No Commit Date Available';
516
- }
517
- }
518
- });
519
- for (const key in contributors) {
520
- if (contributors.hasOwnProperty(key)) {
521
- response.contributors.push(contributors[key]);
522
- }
523
- }
524
- }
525
661
 
526
- } else {
527
- response.error = `Unexpected Status: ${github_response.status}.`;
662
+ if (!items[i].link && !items[i].items) {
663
+ response.errors.push(
664
+ `The following Nav Item has no link or items property: ${JSON.stringify(
665
+ items[i]
666
+ )}`
667
+ );
528
668
  }
529
- return response;
530
- };
669
+ const item = items[i];
670
+ if (!parentlink && item.link) {
671
+ parentLinks[0].link = item.link;
672
+ parentlink = true;
673
+ }
674
+ const { text, link, items: subItems } = item;
675
+ const breadcrumb = [...parentLinks, { text, link }];
531
676
 
532
- exports.strip_drafts = function (nav_items) {
533
- let return_nav = nav_items;
534
- recurse_nav(return_nav);
535
- return return_nav;
536
- };
677
+ if (link) {
678
+ response.bc[link] = breadcrumb;
679
+ }
537
680
 
538
- const recurse_nav = function (nav_items) {
539
- for (const key in nav_items) {
540
- if (nav_items[key].draft) {
541
- nav_items.splice(key, 1);
542
- recurse_nav(nav_items);
543
- } else if (nav_items[key].items) {
544
- recurse_nav(nav_items[key].items);
545
- }
681
+ if (subItems) {
682
+ buildBreadcrumb(subItems, breadcrumb);
546
683
  }
684
+ }
547
685
  };
548
686
 
549
- exports.build_breadcrumbs = function (nav_items) {
550
- let response = {
551
- bc: {},
552
- errors: []
553
- };
554
- const buildBreadcrumb = (items, parentLinks) => {
555
-
556
- // Process parent links
557
- let parentlink = true;
558
- if (parentLinks.length > 0) {
559
- if (parentLinks[0].link === undefined || parentLinks[0].link === '') parentlink = false;
560
- if (parentLinks[1] && parentLinks[1].link === undefined && items.length > 0 && items[0].link) {
561
- parentLinks[1].link = items[0].link;
562
- }
563
- if (parentLinks[2] && parentLinks[2].link === undefined && items.length > 0 && items[0].link) {
564
- parentLinks[2].link = items[0].link;
565
- }
566
- }
687
+ buildBreadcrumb(nav_items, []);
688
+ return response;
689
+ };
567
690
 
568
- // Loop through items, build breadcrumb
569
- for (let i = 0; i < items.length; i++) {
570
- if (!items[i].text) {
571
- response.errors.push(`The following Nav Item is missing its text property: ${JSON.stringify(items[i])}`);
572
- }
573
-
574
- if (!items[i].link && !items[i].items) {
575
- response.errors.push(`The following Nav Item has no link or items property: ${JSON.stringify(items[i])}`);
576
- }
577
- const item = items[i];
578
- if (!parentlink && item.link) {
579
- parentLinks[0].link = item.link;
580
- parentlink = true;
581
- }
582
- const { text, link, items: subItems } = item;
583
- const breadcrumb = [...parentLinks, { text, link }];
584
-
585
- if (link) {
586
- response.bc[link] = breadcrumb;
587
- }
588
-
589
- if (subItems) {
590
- buildBreadcrumb(subItems, breadcrumb);
591
- }
592
- }
593
- };
594
-
595
- buildBreadcrumb(nav_items, []);
596
- return response;
691
+ exports.load_product_families = async function () {
692
+ let response = {
693
+ success: false,
694
+ prod_families: {},
695
+ prods_supported: [],
696
+ errors: "",
597
697
  };
598
-
599
- exports.load_product_families = async function () {
600
- let response = {
601
- success: false,
602
- prod_families: {},
603
- prods_supported: [],
604
- errors: ''
605
- };
606
- const prod_families_url = 'https://docs.hornbill.com/_books/products.json';
607
- for (let i = 1; i < 4; i++) {
608
- try {
609
- const prods = await axios.get(prod_families_url, {
610
- httpsAgent: agent,
611
- timeout: 5000
612
- });
613
- if (prods.status === 200) {
614
- response.prod_families = prods.data;
615
- response.prods_supported = [];
616
- for (let i = 0; i < response.prod_families.products.length; i++) {
617
- response.prods_supported.push(response.prod_families.products[i].id);
618
- }
619
- response.success = true;
620
- break;
621
- } else {
622
- throw `Unexpected status - ${prods.status} ${prods.statusText}`;
623
- }
624
- } catch (e) {
625
- if (response.errors === '') response.errors = `Request to ${prod_families_url} failed:`;
626
- response.errors += `\nAttempt ${i} - Error returning product families: ${e}`;
627
- // Wait 2 seconds and try again
628
- await new Promise(r => setTimeout(r, 2000));
629
- }
698
+ const prod_families_url = "https://docs.hornbill.com/_books/products.json";
699
+ for (let i = 1; i < 4; i++) {
700
+ try {
701
+ const prods = await axios.get(prod_families_url, {
702
+ httpsAgent: agent,
703
+ timeout: 5000,
704
+ });
705
+ if (prods.status === 200) {
706
+ response.prod_families = prods.data;
707
+ response.prods_supported = [];
708
+ for (let i = 0; i < response.prod_families.products.length; i++) {
709
+ response.prods_supported.push(
710
+ response.prod_families.products[i].id
711
+ );
712
+ }
713
+ response.success = true;
714
+ break;
715
+ } else {
716
+ throw `Unexpected status - ${prods.status} ${prods.statusText}`;
630
717
  }
631
- return response;
632
- };
633
- })();
718
+ } catch (e) {
719
+ if (response.errors === "")
720
+ response.errors = `Request to ${prod_families_url} failed:`;
721
+ response.errors += `\nAttempt ${i} - Error returning product families: ${e}`;
722
+ // Wait 2 seconds and try again
723
+ await new Promise((r) => setTimeout(r, 2000));
724
+ }
725
+ }
726
+ return response;
727
+ };
728
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hdoc-tools",
3
- "version": "0.18.4",
3
+ "version": "0.18.6",
4
4
  "description": "Hornbill HDocBook Development Support Tool",
5
5
  "main": "hdoc.js",
6
6
  "bin": {