hdoc-tools 0.47.5 → 0.50.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-onyx.js +134 -0
- package/hdoc-build.js +29 -1
- package/hdoc-help.js +1 -0
- package/hdoc-validate-config.js +321 -0
- package/hdoc.js +89 -53
- package/package.json +4 -1
- package/schemas/hdocbook-project.schema.json +102 -0
- package/schemas/hdocbook.schema.json +146 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
|
|
5
|
+
// Replicates the URL normalisation logic from populate_index so page keys match.
|
|
6
|
+
function to_page_path(file) {
|
|
7
|
+
let p = file.relative_path.replaceAll("\\", "/");
|
|
8
|
+
if (
|
|
9
|
+
p.endsWith("/index.md") ||
|
|
10
|
+
p.endsWith("/index.html") ||
|
|
11
|
+
p.endsWith("/index.htm")
|
|
12
|
+
) {
|
|
13
|
+
p = p.substring(0, p.lastIndexOf("/"));
|
|
14
|
+
}
|
|
15
|
+
return `/${p.replace(path.extname(file.relative_path), "")}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Groups index_records into one entry per page, preserving section order.
|
|
19
|
+
function build_pages(index_records) {
|
|
20
|
+
const pages = new Map();
|
|
21
|
+
for (const file of index_records) {
|
|
22
|
+
if (file.inline) continue;
|
|
23
|
+
const page_path = to_page_path(file);
|
|
24
|
+
if (!pages.has(page_path)) {
|
|
25
|
+
pages.set(page_path, {
|
|
26
|
+
page_path,
|
|
27
|
+
title: file.index_html.fm_props.title || "",
|
|
28
|
+
keywords: file.keywords || "",
|
|
29
|
+
status: file.status || "release",
|
|
30
|
+
lastmod: file.lastmod || null,
|
|
31
|
+
sections: [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const text = (file.index_html.text || "").trim();
|
|
35
|
+
if (text) pages.get(page_path).sections.push(text);
|
|
36
|
+
}
|
|
37
|
+
return Array.from(pages.values());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Returns an ISO 8601 string, or null if the value is absent or unparseable.
|
|
41
|
+
function safe_iso(value) {
|
|
42
|
+
if (!value) return null;
|
|
43
|
+
const d = new Date(value);
|
|
44
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
exports.populate_onyx_index = (
|
|
48
|
+
work_path_content,
|
|
49
|
+
doc_id,
|
|
50
|
+
book_config,
|
|
51
|
+
index_records,
|
|
52
|
+
base_url = "",
|
|
53
|
+
verbose = false,
|
|
54
|
+
) => {
|
|
55
|
+
const response = {
|
|
56
|
+
success: false,
|
|
57
|
+
document_count: 0,
|
|
58
|
+
error: "",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const pages = build_pages(index_records);
|
|
62
|
+
const canonical_base = base_url.replace(/\/$/, "");
|
|
63
|
+
const metadata_entries = [];
|
|
64
|
+
|
|
65
|
+
for (const page of pages) {
|
|
66
|
+
// e.g. onyx/getting-started/index.txt or onyx/reference/api.txt
|
|
67
|
+
const txt_rel = `_onyx${page.page_path}.txt`;
|
|
68
|
+
const txt_abs = path.join(work_path_content, txt_rel);
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(path.dirname(txt_abs), { recursive: true });
|
|
71
|
+
|
|
72
|
+
// Plain-text body: title, keywords hint, then each section separated by
|
|
73
|
+
// a blank line so Onyx's chunker sees natural paragraph boundaries.
|
|
74
|
+
const lines = [];
|
|
75
|
+
if (page.title) lines.push(page.title, "");
|
|
76
|
+
if (page.keywords) lines.push(`Keywords: ${page.keywords}`, "");
|
|
77
|
+
for (const section of page.sections) {
|
|
78
|
+
lines.push(section, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
fs.writeFileSync(txt_abs, lines.join("\n").trimEnd(), "utf8");
|
|
83
|
+
} catch (e) {
|
|
84
|
+
response.error = `Failed to write ${txt_rel}: ${e.message}`;
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (verbose) console.log(`Onyx: wrote ${txt_rel}`);
|
|
89
|
+
|
|
90
|
+
const entry = {
|
|
91
|
+
filename: txt_rel.replaceAll("\\", "/"),
|
|
92
|
+
link: canonical_base
|
|
93
|
+
? `${canonical_base}${page.page_path}`
|
|
94
|
+
: page.page_path,
|
|
95
|
+
primary_owners: [],
|
|
96
|
+
secondary_owners: [],
|
|
97
|
+
metadata: {
|
|
98
|
+
book_id: doc_id,
|
|
99
|
+
product_family: book_config.productFamily || "",
|
|
100
|
+
audience: Array.isArray(book_config.audience)
|
|
101
|
+
? book_config.audience.join(",")
|
|
102
|
+
: "",
|
|
103
|
+
tags: Array.isArray(book_config.tags)
|
|
104
|
+
? book_config.tags.join(",")
|
|
105
|
+
: "",
|
|
106
|
+
status: page.status,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const updated_at = safe_iso(page.lastmod);
|
|
110
|
+
if (updated_at) entry.doc_updated_at = updated_at;
|
|
111
|
+
metadata_entries.push(entry);
|
|
112
|
+
|
|
113
|
+
response.document_count++;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const meta_path = path.join(work_path_content, ".onyx_metadata.json");
|
|
117
|
+
try {
|
|
118
|
+
fs.writeFileSync(
|
|
119
|
+
meta_path,
|
|
120
|
+
JSON.stringify(metadata_entries, null, 2),
|
|
121
|
+
"utf8",
|
|
122
|
+
);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
response.error = `Failed to write .onyx_metadata.json: ${e.message}`;
|
|
125
|
+
return response;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
response.success = true;
|
|
129
|
+
console.log(
|
|
130
|
+
`\nOnyx Index Build Complete: ${response.document_count} document records created.`,
|
|
131
|
+
);
|
|
132
|
+
return response;
|
|
133
|
+
};
|
|
134
|
+
})();
|
package/hdoc-build.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
const path = require("node:path");
|
|
6
6
|
const puppeteer = require("puppeteer");
|
|
7
7
|
const hdoc_validate = require(path.join(__dirname, "hdoc-validate.js"));
|
|
8
|
+
const hdoc_validate_config = require(path.join(__dirname, "hdoc-validate-config.js"));
|
|
8
9
|
const hdoc = require(path.join(__dirname, "hdoc-module.js"));
|
|
9
10
|
const hdoc_build_db = require(path.join(__dirname, "hdoc-build-db.js"));
|
|
11
|
+
const hdoc_build_onyx = require(path.join(__dirname, "hdoc-build-onyx.js"));
|
|
10
12
|
const hdoc_build_pdf = require(path.join(__dirname, "hdoc-build-pdf.js"));
|
|
11
13
|
const hdoc_index = require(path.join(__dirname, "hdoc-db.js"));
|
|
12
14
|
const archiver = require("archiver");
|
|
@@ -976,6 +978,17 @@
|
|
|
976
978
|
);
|
|
977
979
|
process.exit(1);
|
|
978
980
|
}
|
|
981
|
+
|
|
982
|
+
const project_config_errors = hdoc_validate_config.validate_project(hdocbook_project);
|
|
983
|
+
if (project_config_errors.length > 0) {
|
|
984
|
+
console.log("\r\n-----------------------");
|
|
985
|
+
console.log(" Validation Output ");
|
|
986
|
+
console.log("-----------------------");
|
|
987
|
+
for (const err of project_config_errors) console.error(`- ${err}`);
|
|
988
|
+
console.error(`\r\n${project_config_errors.length} Configuration Error${project_config_errors.length !== 1 ? 's' : ''} Found in hdocbook-project.json`);
|
|
989
|
+
process.exit(1);
|
|
990
|
+
}
|
|
991
|
+
|
|
979
992
|
doc_id = hdocbook_project.docId;
|
|
980
993
|
|
|
981
994
|
if (
|
|
@@ -1006,7 +1019,12 @@
|
|
|
1006
1019
|
const work_path = path.join(source_path, "_work");
|
|
1007
1020
|
const work_hdocbook_path = path.join(work_path, doc_id, "hdocbook.json");
|
|
1008
1021
|
|
|
1009
|
-
|
|
1022
|
+
try {
|
|
1023
|
+
hdocbook_config = require(hdocbook_path);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
console.error(`File not found: ${doc_id}/hdocbook.json\n`);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
}
|
|
1010
1028
|
if (build_version !== "") {
|
|
1011
1029
|
if (build_version.match(regex_version)) {
|
|
1012
1030
|
hdocbook_config.version = build_version;
|
|
@@ -1017,6 +1035,16 @@
|
|
|
1017
1035
|
}
|
|
1018
1036
|
}
|
|
1019
1037
|
|
|
1038
|
+
const book_config_errors = hdoc_validate_config.validate_book(hdocbook_config, doc_id);
|
|
1039
|
+
if (book_config_errors.length > 0) {
|
|
1040
|
+
console.log("\r\n-----------------------");
|
|
1041
|
+
console.log(" Validation Output ");
|
|
1042
|
+
console.log("-----------------------");
|
|
1043
|
+
for (const err of book_config_errors) console.error(`- ${err}`);
|
|
1044
|
+
console.error(`\r\n${book_config_errors.length} Configuration Error${book_config_errors.length !== 1 ? 's' : ''} Found in ${doc_id}/hdocbook.json`);
|
|
1045
|
+
process.exit(1);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1020
1048
|
if (!hdocbook_config.version.match(regex_version)) {
|
|
1021
1049
|
console.error(
|
|
1022
1050
|
`ERROR: Version number does not match required format - ${hdocbook_config.version}\n`,
|
package/hdoc-help.js
CHANGED
|
@@ -35,6 +35,7 @@ Commands
|
|
|
35
35
|
- Use the '--set-version 1.2.3' argument to set the version number of the built book.
|
|
36
36
|
- Use the '--no-color' argument to remove any color control characters from the output.
|
|
37
37
|
- Use the '--no-links' argument to skip link output to CLI during validation.
|
|
38
|
+
- Use the '--quiet' argument to suppress most console output, and only output validation errors if they are found.
|
|
38
39
|
|
|
39
40
|
- bump
|
|
40
41
|
Updates the semantic version number of the current book. If no options are specified, then the default of patch is applied:
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const regex_doc_id = /^[a-z][a-z0-9-]+$/;
|
|
4
|
+
const regex_version = /^[0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,6}$/;
|
|
5
|
+
|
|
6
|
+
const hdocbook_schema = require(path.join(__dirname, "schemas", "hdocbook.schema.json"));
|
|
7
|
+
const valid_audience = hdocbook_schema.properties.audience.items.enum;
|
|
8
|
+
const valid_product_families = hdocbook_schema.properties.productFamily.enum;
|
|
9
|
+
|
|
10
|
+
const project_schema = require(path.join(__dirname, "schemas", "hdocbook-project.schema.json"));
|
|
11
|
+
const project_top_keys = Object.keys(project_schema.properties);
|
|
12
|
+
const project_pdf_keys = Object.keys(project_schema.properties.pdfGeneration.properties);
|
|
13
|
+
const project_validation_keys = Object.keys(project_schema.properties.validation.properties);
|
|
14
|
+
const project_redirect_keys = Object.keys(project_schema.properties.redirects.items.properties);
|
|
15
|
+
const redirect_valid_codes = project_schema.properties.redirects.items.properties.code.enum;
|
|
16
|
+
|
|
17
|
+
// Reports any keys in obj that are not in the allowed set.
|
|
18
|
+
const check_extra_keys = (obj, allowed, path, file, errors) => {
|
|
19
|
+
for (const key of Object.keys(obj)) {
|
|
20
|
+
if (!allowed.includes(key)) {
|
|
21
|
+
errors.push(`${file}: "${path ? path + '.' : ''}${key}" is not a recognised property`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const validate_nav_item = (item, path, errors, depth) => {
|
|
27
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
28
|
+
errors.push(`hdocbook.json: "${path}" must be an object`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
check_extra_keys(item, ['text', 'link', 'expand', 'draft', 'items'], path, 'hdocbook.json', errors);
|
|
32
|
+
if (!item.text || typeof item.text !== 'string') {
|
|
33
|
+
errors.push(`hdocbook.json: "${path}.text" is required and must be a non-empty string`);
|
|
34
|
+
}
|
|
35
|
+
if (item.link !== undefined && typeof item.link !== 'string') {
|
|
36
|
+
errors.push(`hdocbook.json: "${path}.link" must be a string`);
|
|
37
|
+
}
|
|
38
|
+
if (item.expand !== undefined && typeof item.expand !== 'boolean') {
|
|
39
|
+
errors.push(`hdocbook.json: "${path}.expand" must be a boolean`);
|
|
40
|
+
}
|
|
41
|
+
if (item.draft !== undefined && typeof item.draft !== 'boolean') {
|
|
42
|
+
errors.push(`hdocbook.json: "${path}.draft" must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
if (item.items !== undefined) {
|
|
45
|
+
if (!Array.isArray(item.items)) {
|
|
46
|
+
errors.push(`hdocbook.json: "${path}.items" must be an array`);
|
|
47
|
+
} else if (depth >= 3) {
|
|
48
|
+
errors.push(`hdocbook.json: "${path}.items" exceeds the maximum navigation nesting depth of 3 levels`);
|
|
49
|
+
} else {
|
|
50
|
+
for (let i = 0; i < item.items.length; i++) {
|
|
51
|
+
validate_nav_item(item.items[i], `${path}.items[${i}]`, errors, depth + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Validates the structure of hdocbook-project.json
|
|
58
|
+
const validate_project = (config) => {
|
|
59
|
+
const errors = [];
|
|
60
|
+
const file = 'hdocbook-project.json';
|
|
61
|
+
|
|
62
|
+
check_extra_keys(config, project_top_keys, '', file, errors);
|
|
63
|
+
|
|
64
|
+
if (!config.docId || typeof config.docId !== 'string') {
|
|
65
|
+
errors.push(`${file}: "docId" is required and must be a string`);
|
|
66
|
+
} else if (!regex_doc_id.test(config.docId)) {
|
|
67
|
+
errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (config.pdfGeneration !== undefined) {
|
|
71
|
+
if (typeof config.pdfGeneration !== 'object' || Array.isArray(config.pdfGeneration)) {
|
|
72
|
+
errors.push(`${file}: "pdfGeneration" must be an object`);
|
|
73
|
+
} else {
|
|
74
|
+
check_extra_keys(config.pdfGeneration, project_pdf_keys, 'pdfGeneration', file, errors);
|
|
75
|
+
if (config.pdfGeneration.enable === undefined) {
|
|
76
|
+
errors.push(`${file}: "pdfGeneration.enable" is required`);
|
|
77
|
+
} else if (typeof config.pdfGeneration.enable !== 'boolean') {
|
|
78
|
+
errors.push(`${file}: "pdfGeneration.enable" must be a boolean`);
|
|
79
|
+
}
|
|
80
|
+
if (config.pdfGeneration.exclude_paths !== undefined) {
|
|
81
|
+
if (!Array.isArray(config.pdfGeneration.exclude_paths)) {
|
|
82
|
+
errors.push(`${file}: "pdfGeneration.exclude_paths" must be an array`);
|
|
83
|
+
} else {
|
|
84
|
+
for (let i = 0; i < config.pdfGeneration.exclude_paths.length; i++) {
|
|
85
|
+
if (typeof config.pdfGeneration.exclude_paths[i] !== 'string') {
|
|
86
|
+
errors.push(`${file}: "pdfGeneration.exclude_paths[${i}]" must be a string`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (config.validation !== undefined) {
|
|
95
|
+
if (typeof config.validation !== 'object' || Array.isArray(config.validation)) {
|
|
96
|
+
errors.push(`${file}: "validation" must be an object`);
|
|
97
|
+
} else {
|
|
98
|
+
check_extra_keys(config.validation, project_validation_keys, 'validation', file, errors);
|
|
99
|
+
if (config.validation.exclude_links !== undefined) {
|
|
100
|
+
if (!Array.isArray(config.validation.exclude_links)) {
|
|
101
|
+
errors.push(`${file}: "validation.exclude_links" must be an array`);
|
|
102
|
+
} else {
|
|
103
|
+
for (let i = 0; i < config.validation.exclude_links.length; i++) {
|
|
104
|
+
if (typeof config.validation.exclude_links[i] !== 'string') {
|
|
105
|
+
errors.push(`${file}: "validation.exclude_links[${i}]" must be a string`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (config.validation.exclude_spellcheck !== undefined) {
|
|
111
|
+
if (!Array.isArray(config.validation.exclude_spellcheck)) {
|
|
112
|
+
errors.push(`${file}: "validation.exclude_spellcheck" must be an array`);
|
|
113
|
+
} else {
|
|
114
|
+
for (let i = 0; i < config.validation.exclude_spellcheck.length; i++) {
|
|
115
|
+
const item = config.validation.exclude_spellcheck[i];
|
|
116
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
117
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}]" must be an object`);
|
|
118
|
+
} else {
|
|
119
|
+
check_extra_keys(item, ['document_path', 'words'], `validation.exclude_spellcheck[${i}]`, file, errors);
|
|
120
|
+
if (!item.document_path || typeof item.document_path !== 'string') {
|
|
121
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].document_path" is required and must be a non-empty string`);
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(item.words)) {
|
|
124
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].words" is required and must be an array`);
|
|
125
|
+
} else {
|
|
126
|
+
for (let j = 0; j < item.words.length; j++) {
|
|
127
|
+
if (typeof item.words[j] !== 'string') {
|
|
128
|
+
errors.push(`${file}: "validation.exclude_spellcheck[${i}].words[${j}]" must be a string`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (config.validation.exclude_h1_count !== undefined) {
|
|
137
|
+
if (!Array.isArray(config.validation.exclude_h1_count)) {
|
|
138
|
+
errors.push(`${file}: "validation.exclude_h1_count" must be an array`);
|
|
139
|
+
} else {
|
|
140
|
+
for (let i = 0; i < config.validation.exclude_h1_count.length; i++) {
|
|
141
|
+
if (typeof config.validation.exclude_h1_count[i] !== 'string') {
|
|
142
|
+
errors.push(`${file}: "validation.exclude_h1_count[${i}]" must be a string`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (config.validation.external_link_warnings !== undefined &&
|
|
148
|
+
typeof config.validation.external_link_warnings !== 'boolean') {
|
|
149
|
+
errors.push(`${file}: "validation.external_link_warnings" must be a boolean`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (config.redirects !== undefined) {
|
|
155
|
+
if (!Array.isArray(config.redirects)) {
|
|
156
|
+
errors.push(`${file}: "redirects" must be an array`);
|
|
157
|
+
} else {
|
|
158
|
+
for (let i = 0; i < config.redirects.length; i++) {
|
|
159
|
+
const r = config.redirects[i];
|
|
160
|
+
if (typeof r !== 'object' || Array.isArray(r) || r === null) {
|
|
161
|
+
errors.push(`${file}: "redirects[${i}]" must be an object`);
|
|
162
|
+
} else {
|
|
163
|
+
check_extra_keys(r, project_redirect_keys, `redirects[${i}]`, file, errors);
|
|
164
|
+
if (!r.url || typeof r.url !== 'string') {
|
|
165
|
+
errors.push(`${file}: "redirects[${i}].url" is required and must be a non-empty string`);
|
|
166
|
+
}
|
|
167
|
+
if (r.code === undefined) {
|
|
168
|
+
errors.push(`${file}: "redirects[${i}].code" is required`);
|
|
169
|
+
} else if (!redirect_valid_codes.includes(r.code)) {
|
|
170
|
+
errors.push(`${file}: "redirects[${i}].code" must be ${redirect_valid_codes.join(', ')} (got ${r.code})`);
|
|
171
|
+
}
|
|
172
|
+
if (r.location !== undefined && typeof r.location !== 'string') {
|
|
173
|
+
errors.push(`${file}: "redirects[${i}].location" must be a string`);
|
|
174
|
+
}
|
|
175
|
+
if (r.skip_location_validation !== undefined && typeof r.skip_location_validation !== 'boolean') {
|
|
176
|
+
errors.push(`${file}: "redirects[${i}].skip_location_validation" must be a boolean`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return errors;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Validates the structure of hdocbook.json
|
|
187
|
+
const validate_book = (config, expected_doc_id) => {
|
|
188
|
+
const errors = [];
|
|
189
|
+
const file = 'hdocbook.json';
|
|
190
|
+
|
|
191
|
+
check_extra_keys(config,
|
|
192
|
+
['docId', 'title', 'description', 'publicSource', 'version', 'productFamily',
|
|
193
|
+
'coverImage', 'tags', 'audience', 'languages', 'readingTime', 'navigation', 'inline'],
|
|
194
|
+
'', file, errors);
|
|
195
|
+
|
|
196
|
+
if (!config.docId || typeof config.docId !== 'string') {
|
|
197
|
+
errors.push(`${file}: "docId" is required and must be a string`);
|
|
198
|
+
} else {
|
|
199
|
+
if (!regex_doc_id.test(config.docId)) {
|
|
200
|
+
errors.push(`${file}: "docId" must use kebab-case (lowercase letters, numbers, hyphens): "${config.docId}"`);
|
|
201
|
+
}
|
|
202
|
+
if (expected_doc_id && config.docId !== expected_doc_id) {
|
|
203
|
+
errors.push(`${file}: "docId" value "${config.docId}" does not match the folder name and the docId in hdocbook-project.json ("${expected_doc_id}")`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!config.title || typeof config.title !== 'string') {
|
|
208
|
+
errors.push(`${file}: "title" is required and must be a non-empty string`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!config.version || typeof config.version !== 'string') {
|
|
212
|
+
errors.push(`${file}: "version" is required and must be a string`);
|
|
213
|
+
} else if (!regex_version.test(config.version)) {
|
|
214
|
+
errors.push(`${file}: "version" must be in X.Y.Z format (got "${config.version}")`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!config.productFamily || typeof config.productFamily !== 'string') {
|
|
218
|
+
errors.push(`${file}: "productFamily" is required and must be a string`);
|
|
219
|
+
} else if (!valid_product_families.includes(config.productFamily)) {
|
|
220
|
+
errors.push(`${file}: "productFamily" value "${config.productFamily}" is not recognised. Valid values: ${valid_product_families.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!config.audience || !Array.isArray(config.audience) || config.audience.length === 0) {
|
|
224
|
+
errors.push(`${file}: "audience" is required and must be a non-empty array`);
|
|
225
|
+
} else {
|
|
226
|
+
for (let i = 0; i < config.audience.length; i++) {
|
|
227
|
+
if (typeof config.audience[i] !== 'string') {
|
|
228
|
+
errors.push(`${file}: "audience[${i}]" must be a string`);
|
|
229
|
+
} else if (!valid_audience.includes(config.audience[i])) {
|
|
230
|
+
errors.push(`${file}: "audience[${i}]" value "${config.audience[i]}" is not recognised. Valid values: ${valid_audience.join(', ')}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!config.navigation || typeof config.navigation !== 'object' || Array.isArray(config.navigation)) {
|
|
236
|
+
errors.push(`${file}: "navigation" is required and must be an object`);
|
|
237
|
+
} else {
|
|
238
|
+
check_extra_keys(config.navigation, ['items'], 'navigation', file, errors);
|
|
239
|
+
if (!Array.isArray(config.navigation.items)) {
|
|
240
|
+
errors.push(`${file}: "navigation.items" is required and must be an array`);
|
|
241
|
+
} else if (config.navigation.items.length === 0) {
|
|
242
|
+
errors.push(`${file}: "navigation.items" must not be empty`);
|
|
243
|
+
} else {
|
|
244
|
+
for (let i = 0; i < config.navigation.items.length; i++) {
|
|
245
|
+
validate_nav_item(config.navigation.items[i], `navigation.items[${i}]`, errors, 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (config.description !== undefined && typeof config.description !== 'string') {
|
|
251
|
+
errors.push(`${file}: "description" must be a string`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (config.publicSource !== undefined && config.publicSource !== '') {
|
|
255
|
+
if (typeof config.publicSource !== 'string') {
|
|
256
|
+
errors.push(`${file}: "publicSource" must be a string`);
|
|
257
|
+
} else if (config.publicSource.toLowerCase() === '--publicsource--') {
|
|
258
|
+
errors.push(`${file}: "publicSource" is still set to its default template value`);
|
|
259
|
+
} else if (
|
|
260
|
+
!config.publicSource.startsWith('https://github.com') &&
|
|
261
|
+
!config.publicSource.startsWith('https://api.github.com')
|
|
262
|
+
) {
|
|
263
|
+
errors.push(`${file}: "publicSource" must be a GitHub URL starting with https://github.com or https://api.github.com (got "${config.publicSource}")`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (config.coverImage !== undefined && typeof config.coverImage !== 'string') {
|
|
268
|
+
errors.push(`${file}: "coverImage" must be a string`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (config.tags !== undefined) {
|
|
272
|
+
if (!Array.isArray(config.tags)) {
|
|
273
|
+
errors.push(`${file}: "tags" must be an array`);
|
|
274
|
+
} else {
|
|
275
|
+
for (let i = 0; i < config.tags.length; i++) {
|
|
276
|
+
if (typeof config.tags[i] !== 'string') {
|
|
277
|
+
errors.push(`${file}: "tags[${i}]" must be a string`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (config.languages !== undefined) {
|
|
284
|
+
if (!Array.isArray(config.languages)) {
|
|
285
|
+
errors.push(`${file}: "languages" must be an array`);
|
|
286
|
+
} else {
|
|
287
|
+
for (let i = 0; i < config.languages.length; i++) {
|
|
288
|
+
if (typeof config.languages[i] !== 'string') {
|
|
289
|
+
errors.push(`${file}: "languages[${i}]" must be a string`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (config.inline !== undefined) {
|
|
296
|
+
if (!Array.isArray(config.inline)) {
|
|
297
|
+
errors.push(`${file}: "inline" must be an array`);
|
|
298
|
+
} else {
|
|
299
|
+
for (let i = 0; i < config.inline.length; i++) {
|
|
300
|
+
const item = config.inline[i];
|
|
301
|
+
if (typeof item !== 'object' || Array.isArray(item) || item === null) {
|
|
302
|
+
errors.push(`${file}: "inline[${i}]" must be an object`);
|
|
303
|
+
} else {
|
|
304
|
+
check_extra_keys(item, ['title', 'link'], `inline[${i}]`, file, errors);
|
|
305
|
+
if (!item.title || typeof item.title !== 'string') {
|
|
306
|
+
errors.push(`${file}: "inline[${i}].title" is required and must be a non-empty string`);
|
|
307
|
+
}
|
|
308
|
+
if (!item.link || typeof item.link !== 'string') {
|
|
309
|
+
errors.push(`${file}: "inline[${i}].link" is required and must be a non-empty string`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return errors;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
exports.validate_project = validate_project;
|
|
320
|
+
exports.validate_book = validate_book;
|
|
321
|
+
})();
|
package/hdoc.js
CHANGED
|
@@ -13,69 +13,98 @@
|
|
|
13
13
|
|
|
14
14
|
let console_color = true;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return arg;
|
|
28
|
-
});
|
|
29
|
-
// Use the original console.log with escaped arguments
|
|
30
|
-
originalConsoleLog(...escapedArgs);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
16
|
+
// Quiet mode: suppress all output, show a spinner, print only errors (or success) at exit
|
|
17
|
+
const _quietMode = process.argv.includes('--quiet') && process.argv[2] === 'validate';
|
|
18
|
+
const _quietErrors = [];
|
|
19
|
+
|
|
20
|
+
if (_quietMode) {
|
|
21
|
+
const _spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
22
|
+
let _spinnerIdx = 0;
|
|
23
|
+
const _spinnerInterval = setInterval(() => {
|
|
24
|
+
process.stderr.write(`\r${_spinnerFrames[_spinnerIdx++ % _spinnerFrames.length]} Validating...`);
|
|
25
|
+
}, 100);
|
|
26
|
+
_spinnerInterval.unref(); // Don't prevent natural process exit
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
console.
|
|
28
|
+
console.log = () => {};
|
|
29
|
+
console.info = () => {};
|
|
30
|
+
console.error = (...args) => {
|
|
31
|
+
_quietErrors.push(args.map(a => (typeof a === 'string' ? a : String(a))).join(' '));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
process.on('exit', (code) => {
|
|
35
|
+
clearInterval(_spinnerInterval);
|
|
36
|
+
process.stderr.write('\r\x1b[K');
|
|
37
|
+
if (_quietErrors.length > 0) {
|
|
38
|
+
for (const e of _quietErrors) originalConsoleError(`${e}\n`);
|
|
39
|
+
} else if (code === 0) {
|
|
40
|
+
originalConsoleLog('No validation errors found');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
36
43
|
} else {
|
|
37
|
-
console.log
|
|
38
|
-
|
|
44
|
+
console.log = (...args) => {
|
|
45
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
46
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
47
|
+
originalConsoleLog(...args);
|
|
39
48
|
|
|
40
|
-
|
|
49
|
+
} else {
|
|
50
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
51
|
+
const escapedArgs = args.map(arg => {
|
|
52
|
+
if (typeof arg === 'string') {
|
|
53
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
54
|
+
}
|
|
55
|
+
return arg;
|
|
56
|
+
});
|
|
57
|
+
// Use the original console.log with escaped arguments
|
|
58
|
+
originalConsoleLog(...escapedArgs);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
41
61
|
|
|
42
|
-
if (process.env.GITHUB_ACTIONS
|
|
43
|
-
|
|
44
|
-
if (console_color) originalConsoleInfo(`\x1b[33m${args}\x1b[0m`);
|
|
45
|
-
else originalConsoleInfo(...args);
|
|
62
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
63
|
+
console.log("\nRunning in GitHub Actions environment\n");
|
|
46
64
|
} else {
|
|
47
|
-
|
|
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);
|
|
65
|
+
console.log("\nRunning in non-GitHub Actions environment\n");
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
|
|
68
|
+
console.info = (...args) => {
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
71
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
72
|
+
if (console_color) originalConsoleInfo(`\x1b[33m${args}\x1b[0m`);
|
|
73
|
+
else originalConsoleInfo(...args);
|
|
74
|
+
} else {
|
|
75
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
76
|
+
const escapedArgs = args.map(arg => {
|
|
77
|
+
if (typeof arg === 'string') {
|
|
78
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
79
|
+
}
|
|
80
|
+
return arg;
|
|
81
|
+
});
|
|
82
|
+
// Use the original console.log with escaped arguments
|
|
83
|
+
originalConsoleInfo(...escapedArgs);
|
|
84
|
+
}
|
|
61
85
|
|
|
62
|
-
|
|
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 {
|
|
86
|
+
};
|
|
67
87
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
console.error = (...args) => {
|
|
89
|
+
|
|
90
|
+
if (process.env.GITHUB_ACTIONS !== 'true') {
|
|
91
|
+
// If not running in GitHub Actions, send args to the original console.log
|
|
92
|
+
if (console_color) originalConsoleError(`\x1b[33m${args}\x1b[0m`);
|
|
93
|
+
else originalConsoleError(...args);
|
|
94
|
+
} else {
|
|
95
|
+
|
|
96
|
+
// If running in GitHub Actions, escape % and \ characters so printf doesn't throw an error
|
|
97
|
+
const escapedArgs = args.map(arg => {
|
|
98
|
+
if (typeof arg === 'string') {
|
|
99
|
+
return arg.replace(/%/g, '%%').replace(/\\/g, '\\\\');
|
|
100
|
+
}
|
|
101
|
+
return arg;
|
|
102
|
+
});
|
|
103
|
+
// Use the original console.log with escaped arguments
|
|
104
|
+
originalConsoleError(...escapedArgs);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
79
108
|
|
|
80
109
|
const getHdocPackageVersion = (packagePath) => {
|
|
81
110
|
if (fs.existsSync(packagePath)) {
|
|
@@ -105,6 +134,7 @@
|
|
|
105
134
|
let gen_exclude = false;
|
|
106
135
|
let bump_type = "patch"; // To generate spellcheck exclusions for all files
|
|
107
136
|
let output_links = true;
|
|
137
|
+
let onyx_index = false;
|
|
108
138
|
|
|
109
139
|
// Get options from command args
|
|
110
140
|
for (let x = 0; x < process.argv.length; x++) {
|
|
@@ -151,6 +181,10 @@
|
|
|
151
181
|
console_color = false;
|
|
152
182
|
} else if (process.argv[x].toLowerCase() === "--no-links") {
|
|
153
183
|
output_links = false;
|
|
184
|
+
} else if (process.argv[x].toLowerCase() === "--onyx") {
|
|
185
|
+
onyx_index = true;
|
|
186
|
+
} else if (process.argv[x].toLowerCase() === "--quiet") {
|
|
187
|
+
// handled at startup via _quietMode
|
|
154
188
|
}
|
|
155
189
|
}
|
|
156
190
|
source_path = hdoc.true_case_path_sync(source_path);
|
|
@@ -186,6 +220,7 @@
|
|
|
186
220
|
gen_exclude,
|
|
187
221
|
build_version,
|
|
188
222
|
output_links,
|
|
223
|
+
onyx_index,
|
|
189
224
|
);
|
|
190
225
|
} else if (command.toLowerCase() === "createdocs") {
|
|
191
226
|
const creator = require(path.join(__dirname, "hdoc-create.js"));
|
|
@@ -200,6 +235,7 @@
|
|
|
200
235
|
gen_exclude,
|
|
201
236
|
build_version,
|
|
202
237
|
output_links,
|
|
238
|
+
onyx_index,
|
|
203
239
|
);
|
|
204
240
|
} else if (command.toLowerCase() === "stats") {
|
|
205
241
|
const stats = require(path.join(__dirname, "hdoc-stats.js"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hdoc-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Hornbill HDocBook Development Support Tool",
|
|
5
5
|
"main": "hdoc.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"hdoc-db.js",
|
|
12
12
|
"hdoc-build.js",
|
|
13
13
|
"hdoc-build-db.js",
|
|
14
|
+
"hdoc-build-onyx.js",
|
|
14
15
|
"hdoc-build-pdf.js",
|
|
15
16
|
"hdoc-bump.js",
|
|
16
17
|
"hdoc-create.js",
|
|
@@ -20,10 +21,12 @@
|
|
|
20
21
|
"hdoc-serve.js",
|
|
21
22
|
"hdoc-stats.js",
|
|
22
23
|
"hdoc-validate.js",
|
|
24
|
+
"hdoc-validate-config.js",
|
|
23
25
|
"hdoc-ver.js",
|
|
24
26
|
"validateNodeVer.js",
|
|
25
27
|
"ui",
|
|
26
28
|
"custom_modules",
|
|
29
|
+
"schemas",
|
|
27
30
|
"templates",
|
|
28
31
|
"templates/init/.npmignore",
|
|
29
32
|
"templates/init/gitignore",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "hdocbook-project.schema.json",
|
|
4
|
+
"title": "HDocBook Project Configuration",
|
|
5
|
+
"description": "Schema for hdocbook-project.json - project-level build and validation configuration",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["docId"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"docId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The document ID. Must match the book folder name and use kebab-case.",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]+$"
|
|
14
|
+
},
|
|
15
|
+
"pdfGeneration": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"description": "Controls PDF generation during the book build.",
|
|
18
|
+
"required": ["enable"],
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"properties": {
|
|
21
|
+
"enable": {
|
|
22
|
+
"type": "boolean",
|
|
23
|
+
"description": "Whether to generate PDFs for page content during the build."
|
|
24
|
+
},
|
|
25
|
+
"exclude_paths": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"description": "Paths to exclude from PDF generation. Supports a trailing * wildcard.",
|
|
28
|
+
"items": { "type": "string" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"validation": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "Controls book validation behaviour.",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"properties": {
|
|
37
|
+
"exclude_links": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"description": "Links to skip during link validation. Supports a trailing * wildcard.",
|
|
40
|
+
"items": { "type": "string" }
|
|
41
|
+
},
|
|
42
|
+
"exclude_spellcheck": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"description": "Per-document spellcheck word exclusions.",
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["document_path", "words"],
|
|
48
|
+
"additionalProperties": false,
|
|
49
|
+
"properties": {
|
|
50
|
+
"document_path": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Root-relative path to the document, without file extension."
|
|
53
|
+
},
|
|
54
|
+
"words": {
|
|
55
|
+
"type": "array",
|
|
56
|
+
"description": "Words to exclude from the British English spellcheck for this document.",
|
|
57
|
+
"items": { "type": "string" }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"exclude_h1_count": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "Root-relative paths of documents allowed to have more than one H1 tag.",
|
|
65
|
+
"items": { "type": "string" }
|
|
66
|
+
},
|
|
67
|
+
"external_link_warnings": {
|
|
68
|
+
"type": "boolean",
|
|
69
|
+
"description": "When true, external link failures are reported as warnings rather than errors."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"redirects": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"description": "Permanent redirects for pages that have moved or been deleted.",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"required": ["url", "code"],
|
|
79
|
+
"additionalProperties": false,
|
|
80
|
+
"properties": {
|
|
81
|
+
"url": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "The path being redirected from."
|
|
84
|
+
},
|
|
85
|
+
"code": {
|
|
86
|
+
"type": "integer",
|
|
87
|
+
"description": "HTTP redirect code. 301 = moved permanently, 308 = permanent redirect, 410 = gone.",
|
|
88
|
+
"enum": [301, 308, 410]
|
|
89
|
+
},
|
|
90
|
+
"location": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"description": "Destination path for 301/308 redirects. Not applicable for 410."
|
|
93
|
+
},
|
|
94
|
+
"skip_location_validation": {
|
|
95
|
+
"type": "boolean",
|
|
96
|
+
"description": "Skip validation of the redirect destination path."
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "hdocbook.schema.json",
|
|
4
|
+
"title": "HDocBook Configuration",
|
|
5
|
+
"description": "Schema for hdocbook.json - book metadata, navigation, and publishing configuration",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["docId", "title", "description", "version", "productFamily", "audience", "navigation"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"docId": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The unique document ID. Must match the folder name that contains this file, using kebab-case.",
|
|
13
|
+
"pattern": "^[a-z][a-z0-9-]+$"
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "The full title of the book as presented without other context."
|
|
18
|
+
},
|
|
19
|
+
"description": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "A short description of the book's purpose and contents."
|
|
22
|
+
},
|
|
23
|
+
"publicSource": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "URL to the public GitHub repository for this book. Omit or leave empty for private books.",
|
|
26
|
+
"pattern": "^https://(github\\.com|api\\.github\\.com)/"
|
|
27
|
+
},
|
|
28
|
+
"version": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Book version in X.Y.Z format. Controls automated publishing — increment to trigger a new publish.",
|
|
31
|
+
"pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,6}$"
|
|
32
|
+
},
|
|
33
|
+
"productFamily": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "The ID of the product family this book belongs to.",
|
|
36
|
+
"enum": [
|
|
37
|
+
"esp",
|
|
38
|
+
"com.hornbill.docmanager",
|
|
39
|
+
"com.hornbill.boardmanager",
|
|
40
|
+
"com.hornbill.customermanager",
|
|
41
|
+
"com.hornbill.grc",
|
|
42
|
+
"com.hornbill.itom",
|
|
43
|
+
"com.hornbill.livechat",
|
|
44
|
+
"com.hornbill.projectmanager",
|
|
45
|
+
"com.hornbill.servicemanager",
|
|
46
|
+
"com.hornbill.suppliermanager",
|
|
47
|
+
"com.hornbill.timesheetmanager",
|
|
48
|
+
"com.hornbill.collaboration",
|
|
49
|
+
"appdev",
|
|
50
|
+
"hdocs"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
"coverImage": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "Root-relative path to a cover image used for gallery and social sharing. Optional."
|
|
56
|
+
},
|
|
57
|
+
"tags": {
|
|
58
|
+
"type": "array",
|
|
59
|
+
"description": "Tags associated with this book.",
|
|
60
|
+
"items": { "type": "string" }
|
|
61
|
+
},
|
|
62
|
+
"audience": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"description": "Target audiences for this book. At least one value is required.",
|
|
65
|
+
"minItems": 1,
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": [
|
|
69
|
+
"public",
|
|
70
|
+
"private",
|
|
71
|
+
"private.cloud",
|
|
72
|
+
"private.dev.platform",
|
|
73
|
+
"private.dev.apps",
|
|
74
|
+
"private.hdocs",
|
|
75
|
+
"private.elearning"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"languages": {
|
|
80
|
+
"type": "array",
|
|
81
|
+
"description": "Language codes for the languages available in this book (e.g. 'en').",
|
|
82
|
+
"items": { "type": "string" }
|
|
83
|
+
},
|
|
84
|
+
"readingTime": {
|
|
85
|
+
"type": "number",
|
|
86
|
+
"description": "Total estimated reading time in minutes. Calculated and written automatically during build — do not set manually."
|
|
87
|
+
},
|
|
88
|
+
"navigation": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"description": "Defines the navigation tree displayed alongside book content.",
|
|
91
|
+
"required": ["items"],
|
|
92
|
+
"additionalProperties": false,
|
|
93
|
+
"properties": {
|
|
94
|
+
"items": {
|
|
95
|
+
"type": "array",
|
|
96
|
+
"description": "Top-level navigation items. Maximum three levels of nesting are supported.",
|
|
97
|
+
"minItems": 1,
|
|
98
|
+
"items": { "$ref": "#/$defs/navigationItem" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"inline": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"description": "Inline content sources — content embedded from other books.",
|
|
105
|
+
"items": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["title", "link"],
|
|
108
|
+
"additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"title": { "type": "string" },
|
|
111
|
+
"link": { "type": "string" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"$defs": {
|
|
117
|
+
"navigationItem": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"required": ["text"],
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"properties": {
|
|
122
|
+
"text": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Display text for the navigation item."
|
|
125
|
+
},
|
|
126
|
+
"link": {
|
|
127
|
+
"type": "string",
|
|
128
|
+
"description": "Path to the content page. Ignored when 'items' is present."
|
|
129
|
+
},
|
|
130
|
+
"expand": {
|
|
131
|
+
"type": "boolean",
|
|
132
|
+
"description": "Whether this item is expanded by default."
|
|
133
|
+
},
|
|
134
|
+
"draft": {
|
|
135
|
+
"type": "boolean",
|
|
136
|
+
"description": "When true, this item is excluded from published output."
|
|
137
|
+
},
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "array",
|
|
140
|
+
"description": "Child navigation items.",
|
|
141
|
+
"items": { "$ref": "#/$defs/navigationItem" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|