bitwrench 1.2.16 → 2.0.7

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 (130) hide show
  1. package/README.md +160 -158
  2. package/bin/bitwrench.js +3 -0
  3. package/dist/bitwrench-code-edit.cjs.js +639 -0
  4. package/dist/bitwrench-code-edit.es5.js +875 -0
  5. package/dist/bitwrench-code-edit.es5.min.js +15 -0
  6. package/dist/bitwrench-code-edit.esm.js +628 -0
  7. package/dist/bitwrench-code-edit.esm.min.js +15 -0
  8. package/dist/bitwrench-code-edit.umd.js +645 -0
  9. package/dist/bitwrench-code-edit.umd.min.js +15 -0
  10. package/dist/bitwrench.cjs.js +6983 -0
  11. package/dist/bitwrench.cjs.min.js +62 -0
  12. package/dist/bitwrench.css +5100 -0
  13. package/dist/bitwrench.es5.js +8446 -0
  14. package/dist/bitwrench.es5.min.js +31 -0
  15. package/dist/bitwrench.esm.js +6981 -0
  16. package/dist/bitwrench.esm.min.js +62 -0
  17. package/dist/bitwrench.umd.js +6989 -0
  18. package/dist/bitwrench.umd.min.js +62 -0
  19. package/dist/builds.json +127 -0
  20. package/dist/sri.json +18 -0
  21. package/package.json +86 -24
  22. package/readme.html +288 -0
  23. package/src/bitwrench-code-edit.js +627 -0
  24. package/src/bitwrench-color-utils.js +311 -0
  25. package/src/bitwrench-component-base.js +736 -0
  26. package/src/bitwrench-components-inline.js +374 -0
  27. package/src/bitwrench-components-v2.js +1879 -0
  28. package/src/bitwrench-components.js +610 -0
  29. package/src/bitwrench-styles.js +3240 -0
  30. package/src/bitwrench.js +3367 -0
  31. package/src/cli/convert.js +205 -0
  32. package/src/cli/index.js +122 -0
  33. package/src/cli/inject.js +55 -0
  34. package/src/cli/layout-default.js +142 -0
  35. package/src/generate-css.js +381 -0
  36. package/src/vendor/quikdown.js +654 -0
  37. package/src/version.js +16 -0
  38. package/.eslintrc.json +0 -27
  39. package/.github/workflows/codeql-analysis.yml +0 -72
  40. package/.travis.yml +0 -34
  41. package/bitwrench.css +0 -92
  42. package/bitwrench.js +0 -3348
  43. package/bitwrench.js_sri.txt +0 -1
  44. package/bitwrench.min.js +0 -1
  45. package/bitwrench.min.js_sri.txt +0 -1
  46. package/bitwrench_ESM.js +0 -3207
  47. package/bitwrench_ESM.js_sri.txt +0 -1
  48. package/bitwrench_ESM.min.js +0 -1
  49. package/bitwrench_ESM.min.js_sri.txt +0 -1
  50. package/dev/bitwrench-todo.md +0 -215
  51. package/dev/css-arrows.md +0 -23
  52. package/dev/docStringDev.js +0 -124
  53. package/dev/docStringParseDev.js +0 -171
  54. package/dev/example11-load-mjs-page.html +0 -17
  55. package/dev/figures.html +0 -37
  56. package/dev/html_gen.js +0 -349
  57. package/dev/htmld.md +0 -250
  58. package/dev/htmldev.html +0 -45
  59. package/dev/index-old.html +0 -87
  60. package/dev/misc-notes.md +0 -21
  61. package/dev/norm.css +0 -30
  62. package/dev/notes.md +0 -2
  63. package/dev/pageData.mjs +0 -69
  64. package/dev/sizes.html +0 -49
  65. package/dev/universal-js-module.js +0 -37
  66. package/examples/example1.html +0 -78
  67. package/examples/example10.html +0 -84
  68. package/examples/example11.html +0 -17
  69. package/examples/example12.html +0 -18
  70. package/examples/example2.html +0 -44
  71. package/examples/example3.html +0 -50
  72. package/examples/example4.html +0 -22
  73. package/examples/example5.html +0 -82
  74. package/examples/example6.html +0 -128
  75. package/examples/example7.html +0 -91
  76. package/examples/example8.html +0 -27
  77. package/examples/example9.html +0 -102
  78. package/examples/examplePageData12.mjs +0 -73
  79. package/examples/pageData.mjs +0 -69
  80. package/examples/pico.min.css +0 -5
  81. package/icon/bitwrench-dark-tall.png +0 -0
  82. package/icon/bitwrench-dark.png +0 -0
  83. package/icon/bitwrench-icon-lt-grey.png +0 -0
  84. package/icon/bitwrench-icon.vsd +0 -0
  85. package/icon/bitwrench-logo-dark.png +0 -0
  86. package/icon/bitwrench-logo-full.png +0 -0
  87. package/icon/bitwrench-logo-green.png +0 -0
  88. package/icon/bitwrench-logo-grey.png +0 -0
  89. package/icon/bitwrench-logo-white.png +0 -0
  90. package/icon/bitwrench-logos-colors.png +0 -0
  91. package/icon/bitwrench-thick-logo.png +0 -0
  92. package/icon/bitwrench-thick-teal/android-chrome-192x192.png +0 -0
  93. package/icon/bitwrench-thick-teal/android-chrome-512x512.png +0 -0
  94. package/icon/bitwrench-thick-teal/apple-touch-icon.png +0 -0
  95. package/icon/bitwrench-thick-teal/browserconfig.xml +0 -9
  96. package/icon/bitwrench-thick-teal/favicon-16x16.png +0 -0
  97. package/icon/bitwrench-thick-teal/favicon-32x32.png +0 -0
  98. package/icon/bitwrench-thick-teal/favicon.ico +0 -0
  99. package/icon/bitwrench-thick-teal/mstile-144x144.png +0 -0
  100. package/icon/bitwrench-thick-teal/mstile-150x150.png +0 -0
  101. package/icon/bitwrench-thick-teal/mstile-310x150.png +0 -0
  102. package/icon/bitwrench-thick-teal/mstile-310x310.png +0 -0
  103. package/icon/bitwrench-thick-teal/mstile-70x70.png +0 -0
  104. package/icon/bitwrench-thick-teal/site.webmanifest +0 -19
  105. package/icon/bitwrench-thick-teal.ico +0 -0
  106. package/icon/bitwrench-thick-teal.svg +0 -44
  107. package/icon/bitwrench-thick-teal.zip +0 -0
  108. package/icon/favicon-test.html +0 -20
  109. package/icon/logos-test.PNG +0 -0
  110. package/images/bitwrench-512x512.png +0 -0
  111. package/images/bitwrench-logo-med.png +0 -0
  112. package/images/bitwrench-thick-logo.png +0 -0
  113. package/images/bitwrench-thick-logo.svg +0 -64
  114. package/images/bitwrench-thick-teal.ico +0 -0
  115. package/images/favicon.ico +0 -0
  116. package/index.html +0 -282
  117. package/instr_tmp/bitwrench.js +0 -1350
  118. package/karma.conf.js +0 -140
  119. package/makefile +0 -21
  120. package/quick-docs.html +0 -206
  121. package/test/bitwrench_test.js +0 -1255
  122. package/test/karma-test.js +0 -1081
  123. package/tools/bw_deprecatedNames.js +0 -19
  124. package/tools/bwconsole.js +0 -20
  125. package/tools/createSimpleHTMLPage.js +0 -41
  126. package/tools/emitreadme.sh +0 -4
  127. package/tools/export-bw-default-css.js +0 -41
  128. package/tools/umd2ModuleHack.js +0 -32
  129. package/tools/update-bw-package.js +0 -36
  130. package/tools/updatereadme.js +0 -34
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Bitwrench CLI - Single-file conversion pipeline
3
+ * Read → detect type → process → wrap in layout → write
4
+ */
5
+
6
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { resolve, extname, basename, dirname } from 'node:path';
8
+ import quikdown from '../vendor/quikdown.js';
9
+ import bw from '../bitwrench.js';
10
+ import { getAllStyles, THEME_PRESETS } from '../bitwrench-styles.js';
11
+ import { getInjectionHead, getInjectionBodyEnd } from './inject.js';
12
+ import { makePageLayout } from './layout-default.js';
13
+
14
+ /**
15
+ * Extract title from the first # heading in markdown
16
+ * @param {string} md - Markdown source
17
+ * @returns {string|null} Title text or null
18
+ */
19
+ function extractMarkdownTitle(md) {
20
+ const match = md.match(/^#\s+(.+?)(?:\s*#*)$/m);
21
+ return match ? match[1].trim() : null;
22
+ }
23
+
24
+ /**
25
+ * Extract <title> from an HTML document
26
+ * @param {string} html - HTML source
27
+ * @returns {string|null}
28
+ */
29
+ function extractHtmlTitle(html) {
30
+ const match = html.match(/<title[^>]*>([^<]+)<\/title>/i);
31
+ return match ? match[1].trim() : null;
32
+ }
33
+
34
+ /**
35
+ * Extract <body> content from a full HTML document
36
+ * @param {string} html - HTML source
37
+ * @returns {string} Body content
38
+ */
39
+ function extractHtmlBody(html) {
40
+ const match = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
41
+ return match ? match[1].trim() : html;
42
+ }
43
+
44
+ /**
45
+ * Check if HTML string is a full document (has doctype or html tag)
46
+ * @param {string} html
47
+ * @returns {boolean}
48
+ */
49
+ function isFullHtmlDoc(html) {
50
+ const trimmed = html.trimStart().toLowerCase();
51
+ return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
52
+ }
53
+
54
+ /**
55
+ * Resolve the injection mode from flags
56
+ * @param {Object} flags
57
+ * @returns {'standalone'|'cdn'|'none'}
58
+ */
59
+ function resolveInjectionMode(flags) {
60
+ if (flags.standalone) return 'standalone';
61
+ if (flags.cdn) return 'cdn';
62
+ if (flags.noBw) return 'none';
63
+ // Default for single-file: none
64
+ return 'none';
65
+ }
66
+
67
+ /**
68
+ * Resolve theme config from --theme flag value
69
+ * @param {string} themeValue - Preset name or hex colors ("primary,secondary" or "primary,secondary,tertiary")
70
+ * @returns {Object} Config for bw.generateTheme
71
+ */
72
+ function resolveTheme(themeValue) {
73
+ if (!themeValue) return null;
74
+
75
+ // Check preset names
76
+ const preset = THEME_PRESETS[themeValue.toLowerCase()];
77
+ if (preset) return preset;
78
+
79
+ // Parse hex colors: "#336699,#cc6633" or "#336699,#cc6633,#993366"
80
+ const parts = themeValue.split(',').map(s => s.trim());
81
+ if (parts.length >= 2 && parts[0].startsWith('#') && parts[1].startsWith('#')) {
82
+ const config = { primary: parts[0], secondary: parts[1] };
83
+ if (parts[2] && parts[2].startsWith('#')) {
84
+ config.tertiary = parts[2];
85
+ }
86
+ return config;
87
+ }
88
+
89
+ throw new Error(`Unknown theme: "${themeValue}". Use a preset name (${Object.keys(THEME_PRESETS).join(', ')}) or hex colors ("#primary,#secondary").`);
90
+ }
91
+
92
+ /**
93
+ * Derive output path from input path (replace extension with .html)
94
+ * @param {string} inputPath
95
+ * @returns {string}
96
+ */
97
+ function deriveOutputPath(inputPath) {
98
+ const ext = extname(inputPath);
99
+ return inputPath.slice(0, -ext.length) + '.html';
100
+ }
101
+
102
+ /**
103
+ * Convert a single file to a styled HTML page
104
+ * @param {string} inputPath - Path to input file
105
+ * @param {Object} flags - CLI flags
106
+ * @returns {string} Output file path
107
+ */
108
+ export function convertFile(inputPath, flags = {}) {
109
+ const absInput = resolve(inputPath);
110
+ const raw = readFileSync(absInput, 'utf8');
111
+ const ext = extname(absInput).toLowerCase();
112
+
113
+ let bodyHTML = '';
114
+ let autoTitle = null;
115
+
116
+ // Process based on file extension
117
+ switch (ext) {
118
+ case '.md':
119
+ case '.markdown': {
120
+ autoTitle = extractMarkdownTitle(raw);
121
+ bodyHTML = quikdown(raw, { inline_styles: false });
122
+ break;
123
+ }
124
+ case '.html':
125
+ case '.htm': {
126
+ if (isFullHtmlDoc(raw)) {
127
+ autoTitle = extractHtmlTitle(raw);
128
+ bodyHTML = extractHtmlBody(raw);
129
+ } else {
130
+ bodyHTML = raw;
131
+ }
132
+ break;
133
+ }
134
+ case '.json': {
135
+ const parsed = JSON.parse(raw);
136
+ if (parsed && typeof parsed === 'object' && parsed.t) {
137
+ // TACO object
138
+ bodyHTML = bw.html(parsed, { raw: true });
139
+ autoTitle = parsed.t === 'html' ? 'TACO Page' : null;
140
+ } else {
141
+ // Plain JSON — pretty-print as code block
142
+ bodyHTML = `<pre><code>${bw.escapeHTML(JSON.stringify(parsed, null, 2))}</code></pre>`;
143
+ }
144
+ break;
145
+ }
146
+ default:
147
+ throw new Error(`Unsupported file type: ${ext}. Supported: .md, .html, .htm, .json`);
148
+ }
149
+
150
+ // Resolve title
151
+ const title = flags.title || autoTitle || basename(absInput, ext);
152
+
153
+ // Resolve injection mode
154
+ const injectionMode = resolveInjectionMode(flags);
155
+
156
+ // Assemble CSS
157
+ let css = '';
158
+
159
+ // 1. Quikdown styles (for markdown files)
160
+ if (ext === '.md' || ext === '.markdown') {
161
+ css += quikdown.emitStyles('quikdown-', 'light');
162
+ }
163
+
164
+ // 2. Theme CSS
165
+ if (flags.theme) {
166
+ const themeConfig = resolveTheme(flags.theme);
167
+ if (themeConfig) {
168
+ const result = bw.generateTheme('', {
169
+ ...themeConfig,
170
+ inject: false
171
+ });
172
+ css += '\n' + result.css;
173
+ }
174
+ }
175
+
176
+ // 3. User CSS file
177
+ if (flags.css) {
178
+ const cssPath = resolve(flags.css);
179
+ css += '\n' + readFileSync(cssPath, 'utf8');
180
+ }
181
+
182
+ // Build the page
183
+ const headInjection = getInjectionHead(injectionMode);
184
+ const bodyEndInjection = getInjectionBodyEnd(injectionMode);
185
+
186
+ const html = makePageLayout({
187
+ title,
188
+ bodyHTML,
189
+ css,
190
+ headInjection,
191
+ bodyEndInjection,
192
+ favicon: flags.favicon || '',
193
+ highlight: !!flags.highlight
194
+ });
195
+
196
+ // Write output
197
+ const outputPath = flags.output ? resolve(flags.output) : deriveOutputPath(absInput);
198
+ const outputDir = dirname(outputPath);
199
+ mkdirSync(outputDir, { recursive: true });
200
+ writeFileSync(outputPath, html, 'utf8');
201
+
202
+ return outputPath;
203
+ }
204
+
205
+ export { THEME_PRESETS, resolveTheme, deriveOutputPath, extractMarkdownTitle, extractHtmlTitle };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Bitwrench CLI - Main entry point
3
+ * Arg parsing with util.parseArgs(), help, version, dispatch
4
+ */
5
+
6
+ import { parseArgs } from 'node:util';
7
+ import { VERSION } from '../version.js';
8
+ import { convertFile } from './convert.js';
9
+
10
+ const USAGE = `
11
+ bitwrench v${VERSION} — Document converter & static site generator
12
+
13
+ Usage:
14
+ bitwrench <file> [options] Convert a file to styled HTML
15
+ bitwrench --version Print version
16
+ bitwrench --help Print this help
17
+
18
+ Options:
19
+ -o, --output <file> Output file path (default: input with .html extension)
20
+ -c, --css <file> Include external CSS file
21
+ -t, --theme <name> Theme preset (ocean, sunset, forest, slate) or hex colors ("#pri,#sec")
22
+ -s, --standalone Embed bitwrench inline (works offline)
23
+ --cdn Link bitwrench via CDN (jsdelivr)
24
+ --no-bw Don't inject bitwrench (plain HTML output)
25
+ --title <text> Page title (default: auto-detect from content)
26
+ -f, --favicon <path> Favicon path or URL
27
+ --highlight Include highlight.js for syntax highlighting
28
+ -v, --verbose Verbose output
29
+ -h, --help Print this help
30
+ --version Print version
31
+
32
+ Examples:
33
+ bitwrench README.md Convert README.md to README.html
34
+ bitwrench README.md -o index.html Specify output file
35
+ bitwrench README.md -o out.html --theme ocean Apply ocean theme
36
+ bitwrench README.md -o out.html --standalone Self-contained offline HTML
37
+ bitwrench README.md -o out.html --highlight With syntax highlighting
38
+ bitwrench doc.md --theme "#336699,#cc6633" Custom theme colors
39
+ `.trim();
40
+
41
+ /**
42
+ * Parse CLI arguments and dispatch
43
+ * @param {string[]} argv - process.argv.slice(2)
44
+ */
45
+ export function run(argv) {
46
+ let values, positionals;
47
+
48
+ try {
49
+ const result = parseArgs({
50
+ args: argv,
51
+ strict: true,
52
+ allowPositionals: true,
53
+ options: {
54
+ output: { type: 'string', short: 'o' },
55
+ css: { type: 'string', short: 'c' },
56
+ theme: { type: 'string', short: 't' },
57
+ standalone: { type: 'boolean', short: 's' },
58
+ cdn: { type: 'boolean' },
59
+ 'no-bw': { type: 'boolean' },
60
+ title: { type: 'string' },
61
+ favicon: { type: 'string', short: 'f' },
62
+ highlight: { type: 'boolean' },
63
+ verbose: { type: 'boolean', short: 'v' },
64
+ version: { type: 'boolean' },
65
+ help: { type: 'boolean', short: 'h' }
66
+ }
67
+ });
68
+ values = result.values;
69
+ positionals = result.positionals;
70
+ } catch (err) {
71
+ console.error(`Error: ${err.message}`);
72
+ console.error('Run "bitwrench --help" for usage.');
73
+ process.exit(1);
74
+ }
75
+
76
+ // --version
77
+ if (values.version) {
78
+ console.log(`bitwrench v${VERSION}`);
79
+ return;
80
+ }
81
+
82
+ // --help
83
+ if (values.help) {
84
+ console.log(USAGE);
85
+ return;
86
+ }
87
+
88
+ // No positional args → error
89
+ if (positionals.length === 0) {
90
+ console.error('Error: No input file specified.');
91
+ console.error('Run "bitwrench --help" for usage.');
92
+ process.exit(1);
93
+ }
94
+
95
+ // Single-file conversion
96
+ const inputFile = positionals[0];
97
+ const flags = {
98
+ output: values.output || null,
99
+ css: values.css || null,
100
+ theme: values.theme || null,
101
+ standalone: !!values.standalone,
102
+ cdn: !!values.cdn,
103
+ noBw: !!values['no-bw'],
104
+ title: values.title || null,
105
+ favicon: values.favicon || null,
106
+ highlight: !!values.highlight,
107
+ verbose: !!values.verbose
108
+ };
109
+
110
+ try {
111
+ const outputPath = convertFile(inputFile, flags);
112
+ if (flags.verbose) {
113
+ console.log(`Converted: ${inputFile} → ${outputPath}`);
114
+ }
115
+ } catch (err) {
116
+ console.error(`Error: ${err.message}`);
117
+ if (flags.verbose) {
118
+ console.error(err.stack);
119
+ }
120
+ process.exit(1);
121
+ }
122
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Bitwrench CLI - Injection modes
3
+ * Handles embedding bitwrench into generated HTML pages
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs';
7
+ import { resolve, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const distDir = resolve(__dirname, '../../dist');
14
+
15
+ /**
16
+ * Get HTML to inject into <head> for the given injection mode
17
+ * @param {'standalone'|'cdn'|'none'} mode
18
+ * @returns {string} HTML string for <head>
19
+ */
20
+ export function getInjectionHead(mode) {
21
+ if (mode === 'standalone') {
22
+ const umdPath = resolve(distDir, 'bitwrench.umd.min.js');
23
+ const umdSource = readFileSync(umdPath, 'utf8');
24
+ return `<script>${umdSource}</script>`;
25
+ }
26
+
27
+ if (mode === 'cdn') {
28
+ let integrity = '';
29
+ try {
30
+ const sriPath = resolve(distDir, 'sri.json');
31
+ const sri = JSON.parse(readFileSync(sriPath, 'utf8'));
32
+ integrity = sri.files['bitwrench.umd.min.js'] || '';
33
+ } catch {
34
+ // SRI file not available — proceed without integrity
35
+ }
36
+ const integrityAttr = integrity ? ` integrity="${integrity}" crossorigin="anonymous"` : '';
37
+ return `<script src="https://cdn.jsdelivr.net/npm/bitwrench@2/dist/bitwrench.umd.min.js"${integrityAttr}></script>`;
38
+ }
39
+
40
+ // mode === 'none'
41
+ return '';
42
+ }
43
+
44
+ /**
45
+ * Get HTML to inject before </body> for the given injection mode
46
+ * @param {'standalone'|'cdn'|'none'} mode
47
+ * @returns {string} HTML string before </body>
48
+ */
49
+ export function getInjectionBodyEnd(mode) {
50
+ if (mode === 'standalone' || mode === 'cdn') {
51
+ return `<script>if(typeof bw!=='undefined'){bw.loadDefaultStyles();}</script>`;
52
+ }
53
+ // mode === 'none'
54
+ return '';
55
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Bitwrench CLI - Default page layout
3
+ * Wraps converted content in a complete HTML document
4
+ */
5
+
6
+ import bw from '../bitwrench.js';
7
+
8
+ /**
9
+ * Base page CSS for the CLI-generated pages
10
+ */
11
+ const BASE_PAGE_CSS = `
12
+ .bw-cli-page {
13
+ max-width: 48rem;
14
+ margin: 0 auto;
15
+ padding: 2rem 1.5rem;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
17
+ font-size: 1rem;
18
+ line-height: 1.6;
19
+ color: #333;
20
+ }
21
+ .bw-cli-page pre {
22
+ overflow-x: auto;
23
+ padding: 1em;
24
+ background: #f5f5f5;
25
+ border-radius: 4px;
26
+ font-size: 0.875em;
27
+ }
28
+ .bw-cli-page code {
29
+ font-family: "SF Mono", "Fira Code", "Fira Mono", "Roboto Mono", Menlo, Courier, monospace;
30
+ }
31
+ .bw-cli-page p code {
32
+ background: #f0f0f0;
33
+ padding: 0.15em 0.3em;
34
+ border-radius: 3px;
35
+ font-size: 0.875em;
36
+ }
37
+ .bw-cli-page table {
38
+ border-collapse: collapse;
39
+ width: 100%;
40
+ margin: 1em 0;
41
+ overflow-x: auto;
42
+ display: block;
43
+ }
44
+ .bw-cli-page th, .bw-cli-page td {
45
+ border: 1px solid #ddd;
46
+ padding: 0.5em 0.75em;
47
+ text-align: left;
48
+ }
49
+ .bw-cli-page th {
50
+ background: #f5f5f5;
51
+ font-weight: 600;
52
+ }
53
+ .bw-cli-page blockquote {
54
+ border-left: 4px solid #ddd;
55
+ margin-left: 0;
56
+ padding-left: 1em;
57
+ color: #666;
58
+ }
59
+ .bw-cli-page img {
60
+ max-width: 100%;
61
+ height: auto;
62
+ }
63
+ .bw-cli-page h1, .bw-cli-page h2, .bw-cli-page h3,
64
+ .bw-cli-page h4, .bw-cli-page h5, .bw-cli-page h6 {
65
+ margin-top: 1.5em;
66
+ margin-bottom: 0.5em;
67
+ line-height: 1.25;
68
+ }
69
+ .bw-cli-page h1 { font-size: 2em; }
70
+ .bw-cli-page h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
71
+ .bw-cli-page a { color: #0366d6; text-decoration: none; }
72
+ .bw-cli-page a:hover { text-decoration: underline; }
73
+ .bw-cli-page hr { border: none; border-top: 1px solid #eee; margin: 2em 0; }
74
+ @media (max-width: 600px) {
75
+ .bw-cli-page { padding: 1rem; }
76
+ }
77
+ `;
78
+
79
+ /**
80
+ * Build a complete HTML page from content and options
81
+ * @param {Object} opts
82
+ * @param {string} opts.title - Page title
83
+ * @param {string} opts.bodyHTML - Rendered HTML content for the body
84
+ * @param {string} [opts.css=''] - Additional CSS to include
85
+ * @param {string} [opts.headInjection=''] - HTML to inject into <head> (bitwrench script)
86
+ * @param {string} [opts.bodyEndInjection=''] - HTML to inject before </body>
87
+ * @param {string} [opts.favicon=''] - Favicon path or URL
88
+ * @param {boolean} [opts.highlight=false] - Include highlight.js CDN
89
+ * @returns {string} Complete HTML document
90
+ */
91
+ export function makePageLayout(opts) {
92
+ const {
93
+ title = 'Untitled',
94
+ bodyHTML = '',
95
+ css = '',
96
+ headInjection = '',
97
+ bodyEndInjection = '',
98
+ favicon = '',
99
+ highlight = false
100
+ } = opts;
101
+
102
+ const safeTitle = bw.escapeHTML(title);
103
+ const version = bw.version;
104
+
105
+ let faviconTag = '';
106
+ if (favicon) {
107
+ // Only escape quotes and angle brackets for attribute safety, not slashes
108
+ const safeFavicon = favicon.replace(/[&<>"']/g, c => ({
109
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
110
+ })[c]);
111
+ faviconTag = `<link rel="icon" href="${safeFavicon}">`;
112
+ }
113
+
114
+ let highlightHead = '';
115
+ let highlightBodyEnd = '';
116
+ if (highlight) {
117
+ highlightHead = '<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css">';
118
+ highlightBodyEnd = '<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>\n<script>hljs.highlightAll();</script>';
119
+ }
120
+
121
+ const allCSS = BASE_PAGE_CSS + (css ? '\n' + css : '');
122
+
123
+ return `<!DOCTYPE html>
124
+ <html lang="en">
125
+ <head>
126
+ <meta charset="UTF-8">
127
+ <meta name="viewport" content="width=device-width, initial-scale=1">
128
+ <meta name="generator" content="bitwrench v${version}">
129
+ <title>${safeTitle}</title>
130
+ ${faviconTag}${headInjection}${highlightHead}
131
+ <style>${allCSS}</style>
132
+ </head>
133
+ <body>
134
+ <div class="bw-cli-page">
135
+ ${bodyHTML}
136
+ </div>
137
+ ${bodyEndInjection}${highlightBodyEnd}
138
+ </body>
139
+ </html>`;
140
+ }
141
+
142
+ export { BASE_PAGE_CSS };