@forwardimpact/pathway 0.25.15 → 0.25.21

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 (34) hide show
  1. package/bin/fit-pathway.js +62 -54
  2. package/package.json +1 -3
  3. package/src/commands/agent-io.js +120 -0
  4. package/src/commands/agent.js +266 -349
  5. package/src/commands/init.js +2 -2
  6. package/src/commands/job.js +237 -183
  7. package/src/components/comparison-radar.js +118 -103
  8. package/src/components/progression-table.js +244 -208
  9. package/src/formatters/index.js +0 -19
  10. package/src/formatters/interview/markdown.js +100 -88
  11. package/src/formatters/job/description.js +76 -75
  12. package/src/formatters/job/dom.js +113 -97
  13. package/src/formatters/level/dom.js +87 -102
  14. package/src/formatters/questions/markdown.js +37 -33
  15. package/src/formatters/questions/shared.js +142 -75
  16. package/src/formatters/skill/dom.js +102 -93
  17. package/src/lib/comparison-radar-chart.js +256 -0
  18. package/src/lib/radar-utils.js +199 -0
  19. package/src/lib/radar.js +25 -662
  20. package/src/pages/agent-builder-download.js +170 -0
  21. package/src/pages/agent-builder-preview.js +344 -0
  22. package/src/pages/agent-builder.js +6 -550
  23. package/src/pages/progress-comparison.js +110 -0
  24. package/src/pages/progress.js +11 -111
  25. package/src/pages/self-assessment-steps.js +494 -0
  26. package/src/pages/self-assessment.js +54 -504
  27. package/src/formatters/behaviour/microdata.js +0 -106
  28. package/src/formatters/discipline/microdata.js +0 -117
  29. package/src/formatters/driver/microdata.js +0 -91
  30. package/src/formatters/level/microdata.js +0 -141
  31. package/src/formatters/microdata-shared.js +0 -184
  32. package/src/formatters/skill/microdata.js +0 -151
  33. package/src/formatters/stage/microdata.js +0 -116
  34. package/src/formatters/track/microdata.js +0 -111
@@ -1,106 +0,0 @@
1
- /**
2
- * Behaviour formatting for microdata HTML output
3
- *
4
- * Generates clean, class-less HTML with microdata aligned with behaviour.schema.json
5
- * RDF vocab: https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- import {
9
- openTag,
10
- prop,
11
- propRaw,
12
- metaTag,
13
- section,
14
- dl,
15
- ul,
16
- escapeHtml,
17
- formatLevelName,
18
- htmlDocument,
19
- } from "../microdata-shared.js";
20
- import { prepareBehavioursList, prepareBehaviourDetail } from "./shared.js";
21
-
22
- /**
23
- * Format behaviour list as microdata HTML
24
- * @param {Array} behaviours - Raw behaviour entities
25
- * @returns {string} HTML with microdata
26
- */
27
- export function behaviourListToMicrodata(behaviours) {
28
- const { items } = prepareBehavioursList(behaviours);
29
-
30
- const content = items
31
- .map(
32
- (
33
- behaviour,
34
- ) => `${openTag("article", { itemtype: "Behaviour", itemid: `#${behaviour.id}` })}
35
- ${prop("h2", "name", behaviour.name)}
36
- ${prop("p", "description", behaviour.truncatedDescription)}
37
- </article>`,
38
- )
39
- .join("\n");
40
-
41
- return htmlDocument(
42
- "Behaviours",
43
- `<main>
44
- <h1>Behaviours</h1>
45
- ${content}
46
- </main>`,
47
- );
48
- }
49
-
50
- /**
51
- * Format behaviour detail as microdata HTML
52
- * @param {Object} behaviour - Raw behaviour entity
53
- * @param {Object} context - Additional context
54
- * @param {Array} context.drivers - All drivers
55
- * @returns {string} HTML with microdata
56
- */
57
- export function behaviourToMicrodata(behaviour, { drivers }) {
58
- const view = prepareBehaviourDetail(behaviour, { drivers });
59
-
60
- if (!view) return "";
61
-
62
- const sections = [];
63
-
64
- // Maturity descriptions - uses MaturityDescriptions itemtype
65
- const maturityPairs = Object.entries(view.maturityDescriptions).map(
66
- ([maturity, desc]) => ({
67
- term: formatLevelName(maturity),
68
- definition: desc,
69
- itemprop: `${maturity.replace(/_([a-z])/g, (_, c) => c.toUpperCase())}Description`,
70
- }),
71
- );
72
- sections.push(
73
- section(
74
- "Maturity Levels",
75
- `${openTag("div", { itemtype: "MaturityDescriptions", itemprop: "maturityDescriptions" })}
76
- ${dl(maturityPairs)}
77
- </div>`,
78
- 2,
79
- ),
80
- );
81
-
82
- // Related drivers
83
- if (view.relatedDrivers.length > 0) {
84
- const driverItems = view.relatedDrivers.map(
85
- (d) => `<a href="#${escapeHtml(d.id)}">${escapeHtml(d.name)}</a>`,
86
- );
87
- sections.push(section("Linked to Drivers", ul(driverItems), 2));
88
- }
89
-
90
- const body = `<main>
91
- ${openTag("article", { itemtype: "Behaviour", itemid: `#${view.id}` })}
92
- ${prop("h1", "name", view.name)}
93
- ${metaTag("id", view.id)}
94
- ${propRaw(
95
- "div",
96
- "human",
97
- `${openTag("div", { itemtype: "BehaviourHumanSection" })}
98
- ${prop("p", "description", view.description)}
99
- ${sections.join("\n")}
100
- </div>`,
101
- )}
102
- </article>
103
- </main>`;
104
-
105
- return htmlDocument(view.name, body);
106
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * Discipline formatting for microdata HTML output
3
- *
4
- * Generates clean, class-less HTML with microdata aligned with discipline.schema.json
5
- * RDF vocab: https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- import {
9
- openTag,
10
- prop,
11
- metaTag,
12
- linkTag,
13
- section,
14
- ul,
15
- escapeHtml,
16
- htmlDocument,
17
- } from "../microdata-shared.js";
18
- import { prepareDisciplinesList, prepareDisciplineDetail } from "./shared.js";
19
-
20
- /**
21
- * Format discipline list as microdata HTML
22
- * @param {Array} disciplines - Raw discipline entities
23
- * @returns {string} HTML with microdata
24
- */
25
- export function disciplineListToMicrodata(disciplines) {
26
- const { items } = prepareDisciplinesList(disciplines);
27
-
28
- const content = items
29
- .map(
30
- (
31
- d,
32
- ) => `${openTag("article", { itemtype: "Discipline", itemid: `#${d.id}` })}
33
- ${prop("h2", "specialization", d.name)}
34
- <p>Core: ${d.coreSkillsCount} | Supporting: ${d.supportingSkillsCount} | Broad: ${d.broadSkillsCount}</p>
35
- </article>`,
36
- )
37
- .join("\n");
38
-
39
- return htmlDocument(
40
- "Disciplines",
41
- `<main>
42
- <h1>Disciplines</h1>
43
- ${content}
44
- </main>`,
45
- );
46
- }
47
-
48
- /**
49
- * Format discipline detail as microdata HTML
50
- * @param {Object} discipline - Raw discipline entity
51
- * @param {Object} context - Additional context
52
- * @param {Array} context.skills - All skills
53
- * @param {Array} context.behaviours - All behaviours
54
- * @param {boolean} [context.showBehaviourModifiers=true] - Whether to show behaviour modifiers section
55
- * @returns {string} HTML with microdata
56
- */
57
- export function disciplineToMicrodata(
58
- discipline,
59
- { skills, behaviours, showBehaviourModifiers = true } = {},
60
- ) {
61
- const view = prepareDisciplineDetail(discipline, { skills, behaviours });
62
-
63
- if (!view) return "";
64
-
65
- const sections = [];
66
-
67
- // Core skills - using coreSkills property
68
- if (view.coreSkills.length > 0) {
69
- const skillLinks = view.coreSkills.map(
70
- (s) =>
71
- `${openTag("span", { itemprop: "coreSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
72
- );
73
- sections.push(section("Core Skills", ul(skillLinks), 2));
74
- }
75
-
76
- // Supporting skills - using supportingSkills property
77
- if (view.supportingSkills.length > 0) {
78
- const skillLinks = view.supportingSkills.map(
79
- (s) =>
80
- `${openTag("span", { itemprop: "supportingSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
81
- );
82
- sections.push(section("Supporting Skills", ul(skillLinks), 2));
83
- }
84
-
85
- // Broad skills - using broadSkills property
86
- if (view.broadSkills.length > 0) {
87
- const skillLinks = view.broadSkills.map(
88
- (s) =>
89
- `${openTag("span", { itemprop: "broadSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
90
- );
91
- sections.push(section("Broad Skills", ul(skillLinks), 2));
92
- }
93
-
94
- // Behaviour modifiers - using BehaviourModifier itemtype
95
- if (showBehaviourModifiers && view.behaviourModifiers.length > 0) {
96
- const modifierItems = view.behaviourModifiers.map((b) => {
97
- const modifierStr = b.modifier > 0 ? `+${b.modifier}` : `${b.modifier}`;
98
- return `${openTag("span", { itemtype: "BehaviourModifier", itemprop: "behaviourModifiers" })}
99
- ${linkTag("targetBehaviour", `#${b.id}`)}
100
- <a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a>: ${openTag("span", { itemprop: "modifierValue" })}${modifierStr}</span>
101
- </span>`;
102
- });
103
- sections.push(section("Behaviour Modifiers", ul(modifierItems), 2));
104
- }
105
-
106
- const body = `<main>
107
- ${openTag("article", { itemtype: "Discipline", itemid: `#${view.id}` })}
108
- ${prop("h1", "specialization", view.name)}
109
- ${metaTag("id", view.id)}
110
- ${discipline.roleTitle ? prop("p", "roleTitle", discipline.roleTitle) : ""}
111
- ${prop("p", "description", view.description)}
112
- ${sections.join("\n")}
113
- </article>
114
- </main>`;
115
-
116
- return htmlDocument(view.name, body);
117
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Driver formatting for microdata HTML output
3
- *
4
- * Generates clean, class-less HTML with microdata aligned with drivers.schema.json
5
- * RDF vocab: https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- import {
9
- openTag,
10
- prop,
11
- metaTag,
12
- section,
13
- ul,
14
- escapeHtml,
15
- htmlDocument,
16
- } from "../microdata-shared.js";
17
- import { prepareDriversList, prepareDriverDetail } from "./shared.js";
18
-
19
- /**
20
- * Format driver list as microdata HTML
21
- * @param {Array} drivers - Raw driver entities
22
- * @returns {string} HTML with microdata
23
- */
24
- export function driverListToMicrodata(drivers) {
25
- const { items } = prepareDriversList(drivers);
26
-
27
- const content = items
28
- .map(
29
- (
30
- driver,
31
- ) => `${openTag("article", { itemtype: "Driver", itemid: `#${driver.id}` })}
32
- ${prop("h2", "name", driver.name)}
33
- ${prop("p", "description", driver.truncatedDescription)}
34
- <p>Skills: ${driver.contributingSkillsCount} | Behaviours: ${driver.contributingBehavioursCount}</p>
35
- </article>`,
36
- )
37
- .join("\n");
38
-
39
- return htmlDocument(
40
- "Drivers",
41
- `<main>
42
- <h1>Drivers</h1>
43
- ${content}
44
- </main>`,
45
- );
46
- }
47
-
48
- /**
49
- * Format driver detail as microdata HTML
50
- * @param {Object} driver - Raw driver entity
51
- * @param {Object} context - Additional context
52
- * @param {Array} context.skills - All skills
53
- * @param {Array} context.behaviours - All behaviours
54
- * @returns {string} HTML with microdata
55
- */
56
- export function driverToMicrodata(driver, { skills, behaviours }) {
57
- const view = prepareDriverDetail(driver, { skills, behaviours });
58
-
59
- if (!view) return "";
60
-
61
- const sections = [];
62
-
63
- // Contributing skills - using contributingSkills property
64
- if (view.contributingSkills.length > 0) {
65
- const skillLinks = view.contributingSkills.map(
66
- (s) =>
67
- `${openTag("span", { itemprop: "contributingSkills" })}<a href="#${escapeHtml(s.id)}">${escapeHtml(s.name)}</a></span>`,
68
- );
69
- sections.push(section("Contributing Skills", ul(skillLinks), 2));
70
- }
71
-
72
- // Contributing behaviours - using contributingBehaviours property
73
- if (view.contributingBehaviours.length > 0) {
74
- const behaviourLinks = view.contributingBehaviours.map(
75
- (b) =>
76
- `${openTag("span", { itemprop: "contributingBehaviours" })}<a href="#${escapeHtml(b.id)}">${escapeHtml(b.name)}</a></span>`,
77
- );
78
- sections.push(section("Contributing Behaviours", ul(behaviourLinks), 2));
79
- }
80
-
81
- const body = `<main>
82
- ${openTag("article", { itemtype: "Driver", itemid: `#${view.id}` })}
83
- ${prop("h1", "name", view.name)}
84
- ${metaTag("id", view.id)}
85
- ${prop("p", "description", view.description)}
86
- ${sections.join("\n")}
87
- </article>
88
- </main>`;
89
-
90
- return htmlDocument(view.name, body);
91
- }
@@ -1,141 +0,0 @@
1
- /**
2
- * Level formatting for microdata HTML output
3
- *
4
- * Generates clean, class-less HTML with microdata aligned with levels.schema.json
5
- * RDF vocab: https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- import {
9
- openTag,
10
- prop,
11
- metaTag,
12
- linkTag,
13
- section,
14
- dl,
15
- escapeHtml,
16
- formatLevelName,
17
- htmlDocument,
18
- } from "../microdata-shared.js";
19
- import { prepareLevelsList, prepareLevelDetail } from "./shared.js";
20
-
21
- /**
22
- * Format level list as microdata HTML
23
- * @param {Array} levels - Raw level entities
24
- * @returns {string} HTML with microdata
25
- */
26
- export function levelListToMicrodata(levels) {
27
- const { items } = prepareLevelsList(levels);
28
-
29
- const content = items
30
- .map(
31
- (g) => `${openTag("article", { itemtype: "Level", itemid: `#${g.id}` })}
32
- ${prop("h2", "id", g.id)}
33
- <p>${escapeHtml(g.displayName)}</p>
34
- ${g.typicalExperienceRange ? prop("p", "typicalExperienceRange", g.typicalExperienceRange) : ""}
35
- ${metaTag("ordinalRank", String(g.ordinalRank))}
36
- </article>`,
37
- )
38
- .join("\n");
39
-
40
- return htmlDocument(
41
- "Levels",
42
- `<main>
43
- <h1>Levels</h1>
44
- ${content}
45
- </main>`,
46
- );
47
- }
48
-
49
- /**
50
- * Format level detail as microdata HTML
51
- * @param {Object} level - Raw level entity
52
- * @returns {string} HTML with microdata
53
- */
54
- export function levelToMicrodata(level) {
55
- const view = prepareLevelDetail(level);
56
-
57
- if (!view) return "";
58
-
59
- const sections = [];
60
-
61
- // Titles section
62
- if (view.professionalTitle || view.managementTitle) {
63
- const titlePairs = [];
64
- if (view.professionalTitle) {
65
- titlePairs.push({
66
- term: "Professional Track",
67
- definition: view.professionalTitle,
68
- itemprop: "professionalTitle",
69
- });
70
- }
71
- if (view.managementTitle) {
72
- titlePairs.push({
73
- term: "Management Track",
74
- definition: view.managementTitle,
75
- itemprop: "managementTitle",
76
- });
77
- }
78
- sections.push(section("Titles", dl(titlePairs), 2));
79
- }
80
-
81
- // Base skill proficiencies - using BaseSkillProficiencies itemtype
82
- if (
83
- view.baseSkillProficiencies &&
84
- Object.keys(view.baseSkillProficiencies).length > 0
85
- ) {
86
- const levelPairs = Object.entries(view.baseSkillProficiencies).map(
87
- ([type, level]) => ({
88
- term: formatLevelName(type),
89
- definition: formatLevelName(level),
90
- itemprop: type,
91
- }),
92
- );
93
- sections.push(
94
- section(
95
- "Base Skill Proficiencies",
96
- `${openTag("div", { itemtype: "BaseSkillProficiencies", itemprop: "baseSkillProficiencies" })}
97
- ${dl(levelPairs)}
98
- </div>`,
99
- 2,
100
- ),
101
- );
102
- }
103
-
104
- // Base behaviour maturity - link to BehaviourMaturity
105
- if (view.baseBehaviourMaturity) {
106
- const maturityContent = `${linkTag("baseBehaviourMaturity", `#${level.baseBehaviourMaturity}`)}
107
- <p>${formatLevelName(level.baseBehaviourMaturity)}</p>`;
108
- sections.push(section("Base Behaviour Maturity", maturityContent, 2));
109
- }
110
-
111
- // Expectations - using LevelExpectations itemtype
112
- if (view.expectations && Object.keys(view.expectations).length > 0) {
113
- const expectationPairs = Object.entries(view.expectations).map(
114
- ([key, value]) => ({
115
- term: formatLevelName(key),
116
- definition: value,
117
- itemprop: key,
118
- }),
119
- );
120
- sections.push(
121
- section(
122
- "Expectations",
123
- `${openTag("div", { itemtype: "LevelExpectations", itemprop: "expectations" })}
124
- ${dl(expectationPairs)}
125
- </div>`,
126
- 2,
127
- ),
128
- );
129
- }
130
-
131
- const body = `<main>
132
- ${openTag("article", { itemtype: "Level", itemid: `#${view.id}` })}
133
- <h1>${prop("span", "id", view.id)} — ${escapeHtml(view.displayName)}</h1>
134
- ${metaTag("ordinalRank", String(view.ordinalRank))}
135
- ${view.typicalExperienceRange ? prop("p", "typicalExperienceRange", `Experience: ${view.typicalExperienceRange}`) : ""}
136
- ${sections.join("\n")}
137
- </article>
138
- </main>`;
139
-
140
- return htmlDocument(`${view.id} - ${view.displayName}`, body);
141
- }
@@ -1,184 +0,0 @@
1
- /**
2
- * Shared microdata HTML utilities
3
- *
4
- * Helper functions for generating clean, class-less HTML with microdata attributes
5
- * aligned with the RDF schema at https://www.forwardimpact.team/schema/rdf/
6
- */
7
-
8
- const VOCAB_BASE = "https://www.forwardimpact.team/schema/rdf/";
9
-
10
- /**
11
- * Create an opening tag with microdata attributes
12
- * @param {string} tag - HTML tag name
13
- * @param {Object} [attrs] - Optional attributes
14
- * @param {string} [attrs.itemtype] - Microdata type (without vocab prefix)
15
- * @param {string} [attrs.itemprop] - Microdata property name
16
- * @param {string} [attrs.itemid] - Microdata item ID
17
- * @returns {string}
18
- */
19
- export function openTag(tag, attrs = {}) {
20
- const parts = [tag];
21
-
22
- if (attrs.itemtype) {
23
- parts.push(`itemscope`);
24
- parts.push(`itemtype="${VOCAB_BASE}${attrs.itemtype}"`);
25
- }
26
-
27
- if (attrs.itemprop) {
28
- parts.push(`itemprop="${attrs.itemprop}"`);
29
- }
30
-
31
- if (attrs.itemid) {
32
- parts.push(`itemid="${attrs.itemid}"`);
33
- }
34
-
35
- return `<${parts.join(" ")}>`;
36
- }
37
-
38
- /**
39
- * Create a self-closing meta element with microdata
40
- * @param {string} itemprop - Property name
41
- * @param {string} content - Content value
42
- * @returns {string}
43
- */
44
- export function metaTag(itemprop, content) {
45
- return `<meta itemprop="${itemprop}" content="${escapeAttr(content)}">`;
46
- }
47
-
48
- /**
49
- * Create a link element with microdata
50
- * @param {string} itemprop - Property name
51
- * @param {string} href - Link target
52
- * @returns {string}
53
- */
54
- export function linkTag(itemprop, href) {
55
- return `<link itemprop="${itemprop}" href="${escapeAttr(href)}">`;
56
- }
57
-
58
- /**
59
- * Wrap content in an element with itemprop
60
- * @param {string} tag - HTML tag name
61
- * @param {string} itemprop - Property name
62
- * @param {string} content - Content to wrap
63
- * @returns {string}
64
- */
65
- export function prop(tag, itemprop, content) {
66
- return `<${tag} itemprop="${itemprop}">${escapeHtml(content)}</${tag}>`;
67
- }
68
-
69
- /**
70
- * Wrap raw HTML content in an element with itemprop (no escaping)
71
- * @param {string} tag - HTML tag name
72
- * @param {string} itemprop - Property name
73
- * @param {string} html - HTML content to wrap
74
- * @returns {string}
75
- */
76
- export function propRaw(tag, itemprop, html) {
77
- return `<${tag} itemprop="${itemprop}">${html}</${tag}>`;
78
- }
79
-
80
- /**
81
- * Create a section with optional heading
82
- * @param {string} heading - Section heading text
83
- * @param {string} content - Section content
84
- * @param {number} [level=2] - Heading level (2-6)
85
- * @returns {string}
86
- */
87
- export function section(heading, content, level = 2) {
88
- const hTag = `h${Math.min(Math.max(level, 1), 6)}`;
89
- return `<section>
90
- <${hTag}>${escapeHtml(heading)}</${hTag}>
91
- ${content}
92
- </section>`;
93
- }
94
-
95
- /**
96
- * Create an unordered list
97
- * @param {string[]} items - List items (already HTML)
98
- * @param {string} [itemprop] - Optional property for list items
99
- * @returns {string}
100
- */
101
- export function ul(items, itemprop) {
102
- if (!items.length) return "";
103
- const lis = items
104
- .map((item) =>
105
- itemprop ? `<li itemprop="${itemprop}">${item}</li>` : `<li>${item}</li>`,
106
- )
107
- .join("\n");
108
- return `<ul>\n${lis}\n</ul>`;
109
- }
110
-
111
- /**
112
- * Create a definition list from key-value pairs
113
- * @param {Array<{term: string, definition: string, itemprop?: string}>} pairs
114
- * @returns {string}
115
- */
116
- export function dl(pairs) {
117
- if (!pairs.length) return "";
118
- const content = pairs
119
- .map(({ term, definition, itemprop }) => {
120
- const dd = itemprop
121
- ? `<dd itemprop="${itemprop}">${escapeHtml(definition)}</dd>`
122
- : `<dd>${escapeHtml(definition)}</dd>`;
123
- return `<dt>${escapeHtml(term)}</dt>\n${dd}`;
124
- })
125
- .join("\n");
126
- return `<dl>\n${content}\n</dl>`;
127
- }
128
-
129
- /**
130
- * Escape HTML special characters
131
- * @param {string} str
132
- * @returns {string}
133
- */
134
- export function escapeHtml(str) {
135
- if (str == null) return "";
136
- return String(str)
137
- .replace(/&/g, "&amp;")
138
- .replace(/</g, "&lt;")
139
- .replace(/>/g, "&gt;");
140
- }
141
-
142
- /**
143
- * Escape attribute value
144
- * @param {string} str
145
- * @returns {string}
146
- */
147
- export function escapeAttr(str) {
148
- if (str == null) return "";
149
- return String(str)
150
- .replace(/&/g, "&amp;")
151
- .replace(/"/g, "&quot;")
152
- .replace(/</g, "&lt;")
153
- .replace(/>/g, "&gt;");
154
- }
155
-
156
- /**
157
- * Format level name for display (capitalize, replace underscores)
158
- * @param {string} level
159
- * @returns {string}
160
- */
161
- export function formatLevelName(level) {
162
- if (!level) return "";
163
- return level.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
164
- }
165
-
166
- /**
167
- * Generate a full microdata HTML document
168
- * @param {string} title - Document title
169
- * @param {string} body - Body content
170
- * @returns {string}
171
- */
172
- export function htmlDocument(title, body) {
173
- return `<!DOCTYPE html>
174
- <html lang="en">
175
- <head>
176
- <meta charset="UTF-8">
177
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
- <title>${escapeHtml(title)}</title>
179
- </head>
180
- <body>
181
- ${body}
182
- </body>
183
- </html>`;
184
- }