portapack 0.3.1 → 0.3.3
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/.eslintrc.json +67 -8
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +14 -22
- package/LICENSE.md +21 -0
- package/README.md +22 -53
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +183 -98
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +178 -97
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +38 -33
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/architecture.md +186 -0
- package/docs/cli.md +23 -23
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +12 -11
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +13 -13
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/roadmap.md +233 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +5 -30
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +253 -222
- package/src/core/extractor.ts +632 -565
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +793 -549
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +1 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -9
- package/typedoc.json +5 -9
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/src/core/bundler.ts
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
/**
|
2
2
|
* @file bundler.ts
|
3
3
|
* @description Core bundling functions to handle both single and multi-page HTML documents. This includes asset extraction, optional minification, and full inlining into a self-contained HTML file.
|
4
|
-
* @version 1.3.0 // Assuming version based on previous context
|
5
4
|
*/
|
6
5
|
|
7
6
|
import { dirname, resolve, sep as pathSeparator } from 'path';
|
@@ -22,31 +21,33 @@ import { sanitizeSlug } from '../utils/slugify';
|
|
22
21
|
* @returns The resolved base URL, ending in a trailing slash.
|
23
22
|
*/
|
24
23
|
function determineBaseUrl(input: string, logger?: Logger): string {
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
// Use logger?.error correctly
|
46
|
-
logger?.error(`💀 Failed to determine base URL for "${input}": ${error.message}`);
|
47
|
-
// Return a default relative base URL on error
|
48
|
-
return './';
|
24
|
+
try {
|
25
|
+
if (input.startsWith('http://') || input.startsWith('https://')) {
|
26
|
+
const url = new URL(input);
|
27
|
+
// Go up to the last '/' in the pathname
|
28
|
+
url.pathname = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);
|
29
|
+
url.search = ''; // Remove query string
|
30
|
+
url.hash = ''; // Remove fragment
|
31
|
+
const baseUrl = url.toString();
|
32
|
+
logger?.debug(`Determined remote base URL: ${baseUrl}`);
|
33
|
+
return baseUrl;
|
34
|
+
} else {
|
35
|
+
// Handle local file path
|
36
|
+
const absoluteDir = dirname(resolve(input));
|
37
|
+
// Ensure trailing separator for directory URL conversion
|
38
|
+
const dirPathWithSeparator = absoluteDir.endsWith(pathSeparator)
|
39
|
+
? absoluteDir
|
40
|
+
: absoluteDir + pathSeparator;
|
41
|
+
const baseUrl = pathToFileURL(dirPathWithSeparator).href;
|
42
|
+
logger?.debug(`Determined local base URL: ${baseUrl}`);
|
43
|
+
return baseUrl;
|
49
44
|
}
|
45
|
+
} catch (error: any) {
|
46
|
+
// Use logger?.error correctly
|
47
|
+
logger?.error(`💀 Failed to determine base URL for "${input}": ${error.message}`);
|
48
|
+
// Return a default relative base URL on error
|
49
|
+
return './';
|
50
|
+
}
|
50
51
|
}
|
51
52
|
|
52
53
|
/**
|
@@ -59,54 +60,67 @@ function determineBaseUrl(input: string, logger?: Logger): string {
|
|
59
60
|
* @returns A fully inlined and bundled HTML string.
|
60
61
|
*/
|
61
62
|
export async function bundleSingleHTML(
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
parsedHtml: ParsedHTML,
|
64
|
+
inputPathOrUrl: string, // Renamed parameter for clarity
|
65
|
+
options: BundleOptions = {},
|
66
|
+
logger?: Logger
|
66
67
|
): Promise<string> {
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
68
|
+
// Define comprehensive defaults
|
69
|
+
const defaultOptions: Required<Omit<BundleOptions, 'logLevel' | 'loggerInstance'>> = {
|
70
|
+
// Omit non-serializable/runtime options from defaults
|
71
|
+
embedAssets: true,
|
72
|
+
minifyHtml: true,
|
73
|
+
minifyJs: true,
|
74
|
+
minifyCss: true,
|
75
|
+
baseUrl: '',
|
76
|
+
verbose: false, // Default verbosity usually controlled by logger level
|
77
|
+
dryRun: false,
|
78
|
+
recursive: false, // Default non-recursive for single bundle
|
79
|
+
output: '', // Default handled elsewhere or not relevant here
|
80
|
+
// Omit logLevel from defaults, use logger?.level
|
81
|
+
};
|
80
82
|
|
81
|
-
|
82
|
-
|
83
|
+
// Merge provided options over defaults
|
84
|
+
const mergedOptions = { ...defaultOptions, ...options };
|
83
85
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
86
|
+
// Determine base URL only if not explicitly provided
|
87
|
+
if (!mergedOptions.baseUrl) {
|
88
|
+
mergedOptions.baseUrl = determineBaseUrl(inputPathOrUrl, logger);
|
89
|
+
}
|
88
90
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
91
|
+
try {
|
92
|
+
logger?.debug(`Starting HTML bundling for ${inputPathOrUrl}`);
|
93
|
+
// Use logger?.level safely
|
94
|
+
const effectiveLogLevel =
|
95
|
+
logger && typeof logger.level === 'number' ? logger.level : LogLevel.INFO; // Default to INFO if logger undefined or level wrong type
|
96
|
+
logger?.debug(
|
97
|
+
`Effective options: ${JSON.stringify(
|
98
|
+
{
|
99
|
+
...mergedOptions,
|
100
|
+
logLevel: effectiveLogLevel, // Include actual log level if needed
|
101
|
+
},
|
102
|
+
null,
|
103
|
+
2
|
104
|
+
)}`
|
105
|
+
);
|
97
106
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
107
|
+
// Execute the bundling pipeline
|
108
|
+
const extracted = await extractAssets(
|
109
|
+
parsedHtml,
|
110
|
+
mergedOptions.embedAssets,
|
111
|
+
mergedOptions.baseUrl,
|
112
|
+
logger
|
113
|
+
);
|
114
|
+
const minified = await minifyAssets(extracted, mergedOptions, logger);
|
115
|
+
const result = packHTML(minified, logger);
|
102
116
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
117
|
+
logger?.info(`Single HTML bundling complete for: ${inputPathOrUrl}`);
|
118
|
+
return result;
|
119
|
+
} catch (error: any) {
|
120
|
+
logger?.error(`Error during single HTML bundling for ${inputPathOrUrl}: ${error.message}`);
|
121
|
+
// Re-throw to allow higher-level handling
|
122
|
+
throw error;
|
123
|
+
}
|
110
124
|
}
|
111
125
|
|
112
126
|
/**
|
@@ -118,108 +132,122 @@ export async function bundleSingleHTML(
|
|
118
132
|
* @throws {Error} If the input is invalid or contains no usable pages.
|
119
133
|
*/
|
120
134
|
export function bundleMultiPageHTML(pages: PageEntry[], logger?: Logger): string {
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
135
|
+
if (!Array.isArray(pages)) {
|
136
|
+
const errorMsg = 'Input pages must be an array of PageEntry objects';
|
137
|
+
logger?.error(errorMsg);
|
138
|
+
throw new Error(errorMsg);
|
139
|
+
}
|
126
140
|
|
127
|
-
|
141
|
+
logger?.info(`Bundling ${pages.length} pages into a multi-page HTML document.`);
|
128
142
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
143
|
+
let pageIndex = 0; // Keep track of original index for logging
|
144
|
+
const validPages = pages.filter(page => {
|
145
|
+
const isValid =
|
146
|
+
page &&
|
147
|
+
typeof page === 'object' &&
|
148
|
+
typeof page.url === 'string' &&
|
149
|
+
typeof page.html === 'string';
|
150
|
+
// Log with original index if invalid
|
151
|
+
if (!isValid) logger?.warn(`Skipping invalid page entry at index ${pageIndex}`);
|
152
|
+
pageIndex++; // Increment index regardless
|
153
|
+
return isValid;
|
154
|
+
});
|
137
155
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
156
|
+
if (validPages.length === 0) {
|
157
|
+
const errorMsg = 'No valid page entries found in input array';
|
158
|
+
logger?.error(errorMsg);
|
159
|
+
throw new Error(errorMsg);
|
160
|
+
}
|
143
161
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
162
|
+
const slugMap = new Map<string, string>();
|
163
|
+
const usedSlugs = new Set<string>();
|
164
|
+
let firstValidSlug: string | undefined = undefined;
|
165
|
+
let pageCounterForFallback = 1; // Counter for unique fallback slugs
|
148
166
|
|
149
|
-
|
150
|
-
|
151
|
-
|
167
|
+
for (const page of validPages) {
|
168
|
+
// --- REVISED SLUG LOGIC ---
|
169
|
+
let baseSlug = sanitizeSlug(page.url);
|
152
170
|
|
153
|
-
|
154
|
-
|
171
|
+
// Determine if the URL represents a root index page
|
172
|
+
const isRootIndex =
|
173
|
+
page.url === '/' || page.url === 'index.html' || page.url.endsWith('/index.html');
|
155
174
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
}
|
175
|
-
} else if (!baseSlug) {
|
176
|
-
// Handle cases where sanitizeSlug returns an empty string initially (e.g. sanitizeSlug('/'))
|
177
|
-
if (isRootIndex) {
|
178
|
-
baseSlug = 'index'; // Ensure root index gets 'index' slug even if sanitizeSlug returns empty
|
179
|
-
logger?.debug(`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`);
|
180
|
-
} else {
|
181
|
-
baseSlug = 'page'; // Fallback for other empty slugs
|
182
|
-
logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
|
183
|
-
}
|
184
|
-
}
|
185
|
-
// Ensure baseSlug is never empty after this point before collision check
|
186
|
-
if (!baseSlug) {
|
187
|
-
// Use a counter to ensure uniqueness if multiple pages sanitize to empty/page
|
188
|
-
baseSlug = `page-${pageCounterForFallback++}`;
|
189
|
-
logger?.warn(`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`);
|
190
|
-
}
|
191
|
-
// --- END REVISED SLUG LOGIC ---
|
192
|
-
|
193
|
-
|
194
|
-
// --- Collision Handling ---
|
195
|
-
let slug = baseSlug;
|
196
|
-
let collisionCounter = 1;
|
197
|
-
// Keep track of the original baseSlug for logging purposes in case of collision
|
198
|
-
const originalBaseSlugForLog = baseSlug;
|
199
|
-
while (usedSlugs.has(slug)) {
|
200
|
-
const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
|
201
|
-
// Log with original intended base slug for clarity
|
202
|
-
logger?.warn(`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`);
|
203
|
-
slug = newSlug;
|
175
|
+
if (baseSlug === 'index' && !isRootIndex) {
|
176
|
+
// If sanitizeSlug produced 'index' but it wasn't from a root index URL, avoid using 'index'.
|
177
|
+
logger?.debug(`URL "${page.url}" sanitized to "index", attempting to find alternative slug.`);
|
178
|
+
// Try using the last path segment instead.
|
179
|
+
// Get parts, remove trailing slash/index/index.html, filter empty
|
180
|
+
const pathParts = page.url
|
181
|
+
.replace(/\/$/, '')
|
182
|
+
.split('/')
|
183
|
+
.filter(p => p && p.toLowerCase() !== 'index.html' && p.toLowerCase() !== 'index');
|
184
|
+
if (pathParts.length > 0) {
|
185
|
+
const lastPartSlug = sanitizeSlug(pathParts[pathParts.length - 1]);
|
186
|
+
if (lastPartSlug && lastPartSlug !== 'index') {
|
187
|
+
// Avoid re-introducing 'index' or using empty
|
188
|
+
baseSlug = lastPartSlug;
|
189
|
+
logger?.debug(`Using last path part slug "${baseSlug}" instead.`);
|
190
|
+
} else {
|
191
|
+
baseSlug = 'page'; // Fallback if last part is empty, 'index', or missing
|
192
|
+
logger?.debug(`Last path part invalid ("${lastPartSlug}"), using fallback slug "page".`);
|
204
193
|
}
|
205
|
-
|
206
|
-
|
194
|
+
} else {
|
195
|
+
baseSlug = 'page'; // Fallback if no other parts
|
196
|
+
logger?.debug(`No valid path parts found, using fallback slug "page".`);
|
197
|
+
}
|
198
|
+
} else if (!baseSlug) {
|
199
|
+
// Handle cases where sanitizeSlug returns an empty string initially (e.g. sanitizeSlug('/'))
|
200
|
+
if (isRootIndex) {
|
201
|
+
baseSlug = 'index'; // Ensure root index gets 'index' slug even if sanitizeSlug returns empty
|
202
|
+
logger?.debug(
|
203
|
+
`URL "${page.url}" sanitized to empty string, using "index" as it is a root index.`
|
204
|
+
);
|
205
|
+
} else {
|
206
|
+
baseSlug = 'page'; // Fallback for other empty slugs
|
207
|
+
logger?.debug(`URL "${page.url}" sanitized to empty string, using fallback slug "page".`);
|
208
|
+
}
|
209
|
+
}
|
210
|
+
// Ensure baseSlug is never empty after this point before collision check
|
211
|
+
if (!baseSlug) {
|
212
|
+
// Use a counter to ensure uniqueness if multiple pages sanitize to empty/page
|
213
|
+
baseSlug = `page-${pageCounterForFallback++}`;
|
214
|
+
logger?.warn(
|
215
|
+
`Could not determine a valid base slug for "${page.url}", using generated fallback "${baseSlug}".`
|
216
|
+
);
|
217
|
+
}
|
218
|
+
// --- Collision Handling ---
|
219
|
+
let slug = baseSlug;
|
220
|
+
let collisionCounter = 1;
|
221
|
+
// Keep track of the original baseSlug for logging purposes in case of collision
|
222
|
+
const originalBaseSlugForLog = baseSlug;
|
223
|
+
while (usedSlugs.has(slug)) {
|
224
|
+
const newSlug = `${originalBaseSlugForLog}-${collisionCounter++}`;
|
225
|
+
// Log with original intended base slug for clarity
|
226
|
+
logger?.warn(
|
227
|
+
`Slug collision detected for "${page.url}" (intended slug: '${originalBaseSlugForLog}'). Using "${newSlug}" instead.`
|
228
|
+
);
|
229
|
+
slug = newSlug;
|
230
|
+
}
|
231
|
+
usedSlugs.add(slug);
|
232
|
+
slugMap.set(page.url, slug);
|
207
233
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
234
|
+
// Track the first valid slug for default navigation
|
235
|
+
if (firstValidSlug === undefined) {
|
236
|
+
// Use triple equals for check
|
237
|
+
firstValidSlug = slug;
|
212
238
|
}
|
239
|
+
}
|
213
240
|
|
214
|
-
|
215
|
-
|
216
|
-
|
241
|
+
// Determine the default page slug - prefer 'index' if present, otherwise use the first page's slug
|
242
|
+
// Use 'page' as ultimate fallback if firstValidSlug is somehow still undefined (e.g., only one page failed slug generation)
|
243
|
+
const defaultPageSlug = usedSlugs.has('index') ? 'index' : firstValidSlug || 'page';
|
217
244
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
245
|
+
// Generate HTML structure
|
246
|
+
// (Ensure template IDs use `page-${slug}`)
|
247
|
+
// (Ensure nav links use `href="#${slug}"` and `data-page="${slug}"`)
|
248
|
+
// (Ensure router script uses `${defaultPageSlug}` correctly)
|
249
|
+
const output = `
|
250
|
+
<!DOCTYPE html>
|
223
251
|
<html lang="en">
|
224
252
|
<head>
|
225
253
|
<meta charset="UTF-8">
|
@@ -235,78 +263,81 @@ export function bundleMultiPageHTML(pages: PageEntry[], logger?: Logger): string
|
|
235
263
|
</style>
|
236
264
|
</head>
|
237
265
|
<body>
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
}
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
266
|
+
<nav id="main-nav">
|
267
|
+
${validPages
|
268
|
+
.map(p => {
|
269
|
+
const slug = slugMap.get(p.url)!; // Slug is guaranteed to exist here
|
270
|
+
const label = slug; // Use slug as label for simplicity
|
271
|
+
return `<a href="#${slug}" data-page="${slug}">${label}</a>`;
|
272
|
+
})
|
273
|
+
.join('\n ')}
|
274
|
+
</nav>
|
275
|
+
<div id="page-container"></div>
|
276
|
+
${validPages
|
277
|
+
.map(p => {
|
278
|
+
const slug = slugMap.get(p.url)!;
|
279
|
+
// Basic sanitization/escaping might be needed for p.html if needed
|
280
|
+
return `<template id="page-${slug}">${p.html}</template>`;
|
281
|
+
})
|
282
|
+
.join('\n ')}
|
283
|
+
<script id="router-script">
|
284
|
+
document.addEventListener('DOMContentLoaded', function() {
|
285
|
+
const pageContainer = document.getElementById('page-container');
|
286
|
+
const navLinks = document.querySelectorAll('#main-nav a');
|
255
287
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
288
|
+
function navigateTo(slug) {
|
289
|
+
const template = document.getElementById('page-' + slug);
|
290
|
+
if (!template || !pageContainer) {
|
291
|
+
console.warn('Navigation failed: Template or container not found for slug:', slug);
|
292
|
+
// Maybe try navigating to default page? Or just clear container?
|
293
|
+
if (pageContainer) pageContainer.innerHTML = '<p>Page not found.</p>';
|
294
|
+
return;
|
295
|
+
}
|
296
|
+
// Clear previous content and append new content
|
297
|
+
pageContainer.innerHTML = ''; // Clear reliably
|
298
|
+
pageContainer.appendChild(template.content.cloneNode(true));
|
267
299
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
300
|
+
// Update active link styling
|
301
|
+
navLinks.forEach(link => {
|
302
|
+
link.classList.toggle('active', link.getAttribute('data-page') === slug);
|
303
|
+
});
|
272
304
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
}
|
305
|
+
// Update URL hash without triggering hashchange if already correct
|
306
|
+
if (window.location.hash.substring(1) !== slug) {
|
307
|
+
// Use pushState for cleaner history
|
308
|
+
history.pushState({ slug: slug }, '', '#' + slug);
|
278
309
|
}
|
310
|
+
}
|
279
311
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
312
|
+
// Handle back/forward navigation
|
313
|
+
window.addEventListener('popstate', (event) => {
|
314
|
+
let slug = window.location.hash.substring(1);
|
315
|
+
// If popstate event has state use it, otherwise fallback to hash or default
|
316
|
+
if (event && event.state && event.state.slug) { // Check event exists
|
317
|
+
slug = event.state.slug;
|
318
|
+
}
|
319
|
+
// Ensure the target page exists before navigating, fallback to default slug
|
320
|
+
const targetSlug = document.getElementById('page-' + slug) ? slug : '${defaultPageSlug}';
|
321
|
+
navigateTo(targetSlug);
|
322
|
+
});
|
291
323
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
});
|
324
|
+
// Handle direct link clicks
|
325
|
+
navLinks.forEach(link => {
|
326
|
+
link.addEventListener('click', function(e) {
|
327
|
+
e.preventDefault();
|
328
|
+
const slug = this.getAttribute('data-page');
|
329
|
+
if (slug) navigateTo(slug);
|
299
330
|
});
|
300
|
-
|
301
|
-
// Initial page load
|
302
|
-
const initialHash = window.location.hash.substring(1);
|
303
|
-
const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
|
304
|
-
navigateTo(initialSlug);
|
305
331
|
});
|
306
|
-
|
332
|
+
|
333
|
+
// Initial page load
|
334
|
+
const initialHash = window.location.hash.substring(1);
|
335
|
+
const initialSlug = document.getElementById('page-' + initialHash) ? initialHash : '${defaultPageSlug}';
|
336
|
+
navigateTo(initialSlug);
|
337
|
+
});
|
338
|
+
</script>
|
307
339
|
</body>
|
308
340
|
</html>`;
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
}
|
341
|
+
logger?.info(`Multi-page bundle generated. Size: ${Buffer.byteLength(output, 'utf-8')} bytes.`);
|
342
|
+
return output;
|
343
|
+
}
|