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.
Files changed (74) hide show
  1. package/.eslintrc.json +67 -8
  2. package/.releaserc.js +25 -27
  3. package/CHANGELOG.md +14 -22
  4. package/LICENSE.md +21 -0
  5. package/README.md +22 -53
  6. package/commitlint.config.js +30 -34
  7. package/dist/cli/cli-entry.cjs +183 -98
  8. package/dist/cli/cli-entry.cjs.map +1 -1
  9. package/dist/index.d.ts +0 -3
  10. package/dist/index.js +178 -97
  11. package/dist/index.js.map +1 -1
  12. package/docs/.vitepress/config.ts +38 -33
  13. package/docs/.vitepress/sidebar-generator.ts +89 -38
  14. package/docs/architecture.md +186 -0
  15. package/docs/cli.md +23 -23
  16. package/docs/code-of-conduct.md +7 -1
  17. package/docs/configuration.md +12 -11
  18. package/docs/contributing.md +6 -2
  19. package/docs/deployment.md +10 -5
  20. package/docs/development.md +8 -5
  21. package/docs/getting-started.md +13 -13
  22. package/docs/index.md +1 -1
  23. package/docs/public/android-chrome-192x192.png +0 -0
  24. package/docs/public/android-chrome-512x512.png +0 -0
  25. package/docs/public/apple-touch-icon.png +0 -0
  26. package/docs/public/favicon-16x16.png +0 -0
  27. package/docs/public/favicon-32x32.png +0 -0
  28. package/docs/public/favicon.ico +0 -0
  29. package/docs/roadmap.md +233 -0
  30. package/docs/site.webmanifest +1 -0
  31. package/docs/troubleshooting.md +12 -1
  32. package/examples/main.ts +5 -30
  33. package/examples/sample-project/script.js +1 -1
  34. package/jest.config.ts +8 -13
  35. package/nodemon.json +5 -10
  36. package/package.json +2 -5
  37. package/src/cli/cli-entry.ts +2 -2
  38. package/src/cli/cli.ts +21 -16
  39. package/src/cli/options.ts +127 -113
  40. package/src/core/bundler.ts +253 -222
  41. package/src/core/extractor.ts +632 -565
  42. package/src/core/minifier.ts +173 -162
  43. package/src/core/packer.ts +141 -137
  44. package/src/core/parser.ts +74 -73
  45. package/src/core/web-fetcher.ts +270 -258
  46. package/src/index.ts +18 -17
  47. package/src/types.ts +9 -11
  48. package/src/utils/font.ts +12 -6
  49. package/src/utils/logger.ts +110 -105
  50. package/src/utils/meta.ts +75 -76
  51. package/src/utils/mime.ts +50 -50
  52. package/src/utils/slugify.ts +33 -34
  53. package/tests/unit/cli/cli-entry.test.ts +72 -70
  54. package/tests/unit/cli/cli.test.ts +314 -278
  55. package/tests/unit/cli/options.test.ts +294 -301
  56. package/tests/unit/core/bundler.test.ts +426 -329
  57. package/tests/unit/core/extractor.test.ts +793 -549
  58. package/tests/unit/core/minifier.test.ts +374 -274
  59. package/tests/unit/core/packer.test.ts +298 -264
  60. package/tests/unit/core/parser.test.ts +538 -150
  61. package/tests/unit/core/web-fetcher.test.ts +389 -359
  62. package/tests/unit/index.test.ts +238 -197
  63. package/tests/unit/utils/font.test.ts +26 -21
  64. package/tests/unit/utils/logger.test.ts +267 -260
  65. package/tests/unit/utils/meta.test.ts +29 -28
  66. package/tests/unit/utils/mime.test.ts +73 -74
  67. package/tests/unit/utils/slugify.test.ts +14 -12
  68. package/tsconfig.build.json +9 -10
  69. package/tsconfig.jest.json +1 -1
  70. package/tsconfig.json +2 -2
  71. package/tsup.config.ts +8 -9
  72. package/typedoc.json +5 -9
  73. /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
  74. /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
@@ -15,7 +15,7 @@ import { guessMimeType } from '../utils/mime'; // Assuming correct path
15
15
  * Escapes characters potentially problematic within inline `<script>` tags.
16
16
  */
17
17
  function escapeScriptContent(code: string): string {
18
- return code.replace(/<\/(script)/gi, '<\\/$1');
18
+ return code.replace(/<\/(script)/gi, '<\\/$1');
19
19
  }
20
20
 
21
21
  /**
@@ -26,137 +26,141 @@ function escapeScriptContent(code: string): string {
26
26
  * @param {Logger} [logger] - Optional logger instance.
27
27
  */
28
28
  function ensureBaseTag($: CheerioAPI, logger?: Logger): void {
29
- let head = $('head');
30
-
31
- // If <head> doesn't exist, create it, ensuring <html> exists first.
32
- if (head.length === 0) {
33
- logger?.debug('No <head> tag found. Creating <head> and ensuring <html> exists.');
34
- let htmlElement = $('html');
35
-
36
- // If <html> doesn't exist, create it and wrap the existing content.
37
- if (htmlElement.length === 0) {
38
- logger?.debug('No <html> tag found. Wrapping content in <html><body>...');
39
- const bodyContent = $.root().html() || '';
40
- $.root().empty();
41
- // FIX: Use 'as any' for type assertion
42
- htmlElement = $('<html>').appendTo($.root()) as any;
43
- // FIX: Use 'as any' for type assertion
44
- head = $('<head>').appendTo(htmlElement) as any;
45
- $('<body>').html(bodyContent).appendTo(htmlElement);
46
- } else {
47
- // If <html> exists but <head> doesn't, prepend <head> to <html>
48
- // FIX: Use 'as any' for type assertion
49
- head = $('<head>').prependTo(htmlElement) as any;
50
- }
51
- }
52
-
53
- // Now head should represent the head element selection.
54
- // Check if <base> exists within the guaranteed <head>.
55
- // Use type guard just in case head couldn't be created properly
56
- if (head && head.length > 0 && head.find('base[href]').length === 0) {
57
- logger?.debug('Prepending <base href="./"> to <head>.');
58
- head.prepend('<base href="./">');
29
+ let head = $('head');
30
+
31
+ // If <head> doesn't exist, create it, ensuring <html> exists first.
32
+ if (head.length === 0) {
33
+ logger?.debug('No <head> tag found. Creating <head> and ensuring <html> exists.');
34
+ let htmlElement = $('html');
35
+
36
+ // If <html> doesn't exist, create it and wrap the existing content.
37
+ if (htmlElement.length === 0) {
38
+ logger?.debug('No <html> tag found. Wrapping content in <html><body>...');
39
+ const bodyContent = $.root().html() || '';
40
+ $.root().empty();
41
+ // FIX: Use 'as any' for type assertion
42
+ htmlElement = $('<html>').appendTo($.root()) as any;
43
+ // FIX: Use 'as any' for type assertion
44
+ head = $('<head>').appendTo(htmlElement) as any;
45
+ $('<body>').html(bodyContent).appendTo(htmlElement);
46
+ } else {
47
+ // If <html> exists but <head> doesn't, prepend <head> to <html>
48
+ // FIX: Use 'as any' for type assertion
49
+ head = $('<head>').prependTo(htmlElement) as any;
59
50
  }
51
+ }
52
+
53
+ // Now head should represent the head element selection.
54
+ // Check if <base> exists within the guaranteed <head>.
55
+ // Use type guard just in case head couldn't be created properly
56
+ if (head && head.length > 0 && head.find('base[href]').length === 0) {
57
+ logger?.debug('Prepending <base href="./"> to <head>.');
58
+ head.prepend('<base href="./">');
59
+ }
60
60
  }
61
61
 
62
-
63
62
  /**
64
63
  * Inlines assets into the HTML document using Cheerio for safe DOM manipulation.
65
64
  */
66
65
  function inlineAssets($: CheerioAPI, assets: Asset[], logger?: Logger): void {
67
- logger?.debug(`Inlining ${assets.filter(a => a.content).length} assets with content...`);
68
- const assetMap = new Map<string, Asset>(assets.map(asset => [asset.url, asset]));
69
-
70
- // 1. Inline CSS (<link rel="stylesheet" href="...">)
71
- $('link[rel="stylesheet"][href]').each((_, el) => {
72
- const link = $(el);
73
- const href = link.attr('href');
74
- const asset = href ? assetMap.get(href) : undefined;
75
- if (asset?.content && typeof asset.content === 'string') {
76
- if (asset.content.startsWith('data:')) {
77
- logger?.debug(`Replacing link with style tag using existing data URI: ${asset.url}`);
78
- const styleTag = $('<style>').text(`@import url("${asset.content}");`);
79
- link.replaceWith(styleTag);
80
- } else {
81
- logger?.debug(`Inlining CSS: ${asset.url}`);
82
- const styleTag = $('<style>').text(asset.content);
83
- link.replaceWith(styleTag);
84
- }
85
- } else if (href) {
86
- logger?.warn(`Could not inline CSS: ${href}. Content missing or invalid.`);
87
- }
88
- });
89
-
90
- // 2. Inline JS (<script src="...">)
91
- $('script[src]').each((_, el) => {
92
- const script = $(el);
93
- const src = script.attr('src');
94
- const asset = src ? assetMap.get(src) : undefined;
95
- if (asset?.content && typeof asset.content === 'string') {
96
- logger?.debug(`Inlining JS: ${asset.url}`);
97
- const inlineScript = $('<script>');
98
- inlineScript.text(escapeScriptContent(asset.content));
99
- Object.entries(script.attr() || {}).forEach(([key, value]) => {
100
- if (key.toLowerCase() !== 'src') inlineScript.attr(key, value);
101
- });
102
- script.replaceWith(inlineScript);
103
- } else if (src) {
104
- logger?.warn(`Could not inline JS: ${src}. Content missing or not string.`);
105
- }
106
- });
107
-
108
- // 3. Inline Images (<img src="...">, <video poster="...">, etc.)
109
- $('img[src], video[poster], input[type="image"][src]').each((_, el) => {
110
- const element = $(el);
111
- const srcAttr = element.is('video') ? 'poster' : 'src';
112
- const src = element.attr(srcAttr);
113
- const asset = src ? assetMap.get(src) : undefined;
114
- if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
115
- logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
116
- element.attr(srcAttr, asset.content);
117
- } else if (src) {
118
- logger?.warn(`Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`);
119
- }
66
+ logger?.debug(`Inlining ${assets.filter(a => a.content).length} assets with content...`);
67
+ const assetMap = new Map<string, Asset>(assets.map(asset => [asset.url, asset]));
68
+
69
+ // 1. Inline CSS (<link rel="stylesheet" href="...">)
70
+ $('link[rel="stylesheet"][href]').each((_, el) => {
71
+ const link = $(el);
72
+ const href = link.attr('href');
73
+ const asset = href ? assetMap.get(href) : undefined;
74
+ if (asset?.content && typeof asset.content === 'string') {
75
+ if (asset.content.startsWith('data:')) {
76
+ logger?.debug(`Replacing link with style tag using existing data URI: ${asset.url}`);
77
+ const styleTag = $('<style>').text(`@import url("${asset.content}");`);
78
+ link.replaceWith(styleTag);
79
+ } else {
80
+ logger?.debug(`Inlining CSS: ${asset.url}`);
81
+ const styleTag = $('<style>').text(asset.content);
82
+ link.replaceWith(styleTag);
83
+ }
84
+ } else if (href) {
85
+ logger?.warn(`Could not inline CSS: ${href}. Content missing or invalid.`);
86
+ }
87
+ });
88
+
89
+ // 2. Inline JS (<script src="...">)
90
+ $('script[src]').each((_, el) => {
91
+ const script = $(el);
92
+ const src = script.attr('src');
93
+ const asset = src ? assetMap.get(src) : undefined;
94
+ if (asset?.content && typeof asset.content === 'string') {
95
+ logger?.debug(`Inlining JS: ${asset.url}`);
96
+ const inlineScript = $('<script>');
97
+ inlineScript.text(escapeScriptContent(asset.content));
98
+ Object.entries(script.attr() || {}).forEach(([key, value]) => {
99
+ if (key.toLowerCase() !== 'src') inlineScript.attr(key, value);
100
+ });
101
+ script.replaceWith(inlineScript);
102
+ } else if (src) {
103
+ logger?.warn(`Could not inline JS: ${src}. Content missing or not string.`);
104
+ }
105
+ });
106
+
107
+ // 3. Inline Images (<img src="...">, <video poster="...">, etc.)
108
+ $('img[src], video[poster], input[type="image"][src]').each((_, el) => {
109
+ const element = $(el);
110
+ const srcAttr = element.is('video') ? 'poster' : 'src';
111
+ const src = element.attr(srcAttr);
112
+ const asset = src ? assetMap.get(src) : undefined;
113
+ if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
114
+ logger?.debug(`Inlining image via ${srcAttr}: ${asset.url}`);
115
+ element.attr(srcAttr, asset.content);
116
+ } else if (src) {
117
+ logger?.warn(
118
+ `Could not inline image via ${srcAttr}: ${src}. Content missing or not a data URI.`
119
+ );
120
+ }
121
+ });
122
+
123
+ // 4. Inline srcset attributes (<img srcset="...">, <source srcset="...">)
124
+ $('img[srcset], source[srcset]').each((_, el) => {
125
+ const element = $(el);
126
+ const srcset = element.attr('srcset');
127
+ if (!srcset) return;
128
+ const newSrcsetParts: string[] = [];
129
+ let changed = false;
130
+ srcset.split(',').forEach(part => {
131
+ const trimmedPart = part.trim();
132
+ const [url, descriptor] = trimmedPart.split(/\s+/, 2);
133
+ const asset = url ? assetMap.get(url) : undefined;
134
+ if (
135
+ asset?.content &&
136
+ typeof asset.content === 'string' &&
137
+ asset.content.startsWith('data:')
138
+ ) {
139
+ newSrcsetParts.push(`${asset.content}${descriptor ? ' ' + descriptor : ''}`);
140
+ changed = true;
141
+ } else {
142
+ newSrcsetParts.push(trimmedPart);
143
+ }
120
144
  });
145
+ if (changed) {
146
+ element.attr('srcset', newSrcsetParts.join(', '));
147
+ }
148
+ });
149
+
150
+ // 5. Inline other asset types (video, audio sources)
151
+ $('video[src], audio[src], video > source[src], audio > source[src]').each((_, el) => {
152
+ const element = $(el);
153
+ const src = element.attr('src');
154
+ const asset = src ? assetMap.get(src) : undefined;
155
+ if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
156
+ logger?.debug(`Inlining media source: ${asset.url}`);
157
+ element.attr('src', asset.content);
158
+ }
159
+ });
121
160
 
122
- // 4. Inline srcset attributes (<img srcset="...">, <source srcset="...">)
123
- $('img[srcset], source[srcset]').each((_, el) => {
124
- const element = $(el);
125
- const srcset = element.attr('srcset');
126
- if (!srcset) return;
127
- const newSrcsetParts: string[] = [];
128
- let changed = false;
129
- srcset.split(',').forEach(part => {
130
- const trimmedPart = part.trim();
131
- const [url, descriptor] = trimmedPart.split(/\s+/, 2);
132
- const asset = url ? assetMap.get(url) : undefined;
133
- if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
134
- newSrcsetParts.push(`${asset.content}${descriptor ? ' ' + descriptor : ''}`);
135
- changed = true;
136
- } else {
137
- newSrcsetParts.push(trimmedPart);
138
- }
139
- });
140
- if (changed) {
141
- element.attr('srcset', newSrcsetParts.join(', '));
142
- }
143
- });
144
-
145
- // 5. Inline other asset types (video, audio sources)
146
- $('video[src], audio[src], video > source[src], audio > source[src]').each((_, el) => {
147
- const element = $(el);
148
- const src = element.attr('src');
149
- const asset = src ? assetMap.get(src) : undefined;
150
- if (asset?.content && typeof asset.content === 'string' && asset.content.startsWith('data:')) {
151
- logger?.debug(`Inlining media source: ${asset.url}`);
152
- element.attr('src', asset.content);
153
- }
154
- });
155
-
156
- logger?.debug('Asset inlining process complete.');
161
+ logger?.debug('Asset inlining process complete.');
157
162
  }
158
163
 
159
-
160
164
  /**
161
165
  * Packs a ParsedHTML object into a single, self-contained HTML string.
162
166
  * This involves ensuring a base tag exists and inlining all assets
@@ -168,24 +172,24 @@ function inlineAssets($: CheerioAPI, assets: Asset[], logger?: Logger): void {
168
172
  * @returns {string} The packed HTML string with assets inlined. Returns a minimal HTML structure if input is invalid.
169
173
  */
170
174
  export function packHTML(parsed: ParsedHTML, logger?: Logger): string {
171
- const { htmlContent, assets } = parsed;
172
- if (!htmlContent || typeof htmlContent !== 'string') {
173
- logger?.warn('Packer received empty or invalid htmlContent. Returning minimal HTML shell.');
174
- return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
175
- }
175
+ const { htmlContent, assets } = parsed;
176
+ if (!htmlContent || typeof htmlContent !== 'string') {
177
+ logger?.warn('Packer received empty or invalid htmlContent. Returning minimal HTML shell.');
178
+ return '<!DOCTYPE html><html><head><base href="./"></head><body></body></html>';
179
+ }
176
180
 
177
- logger?.debug('Loading HTML content into Cheerio for packing...');
178
- const $ = cheerio.load(htmlContent);
181
+ logger?.debug('Loading HTML content into Cheerio for packing...');
182
+ const $ = cheerio.load(htmlContent);
179
183
 
180
- logger?.debug('Ensuring <base> tag exists...');
181
- ensureBaseTag($, logger); // Ensure base tag safely
184
+ logger?.debug('Ensuring <base> tag exists...');
185
+ ensureBaseTag($, logger); // Ensure base tag safely
182
186
 
183
- logger?.debug('Starting asset inlining...');
184
- inlineAssets($, assets, logger); // Inline assets safely
187
+ logger?.debug('Starting asset inlining...');
188
+ inlineAssets($, assets, logger); // Inline assets safely
185
189
 
186
- logger?.debug('Generating final packed HTML string...');
187
- const finalHtml = $.html();
190
+ logger?.debug('Generating final packed HTML string...');
191
+ const finalHtml = $.html();
188
192
 
189
- logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
190
- return finalHtml;
191
- }
193
+ logger?.debug(`Packing complete. Final size: ${Buffer.byteLength(finalHtml)} bytes.`);
194
+ return finalHtml;
195
+ }
@@ -8,7 +8,6 @@
8
8
  * and data URIs are ignored. Duplicate asset URLs are ignored.
9
9
  */
10
10
 
11
- // FIX: Use only the named import for readFile
12
11
  import { readFile } from 'fs/promises';
13
12
  // NOTE: 'path' module was imported but not used, so removed. Add back if needed later.
14
13
  // import path from 'path';
@@ -34,82 +33,84 @@ import { guessMimeType } from '../utils/mime.js';
34
33
  * @throws {Error} Throws an error with cause if the file cannot be read.
35
34
  */
36
35
  export async function parseHTML(entryFilePath: string, logger?: Logger): Promise<ParsedHTML> {
37
- logger?.debug(`Parsing HTML file: ${entryFilePath}`);
38
- let htmlContent: string;
39
- try {
40
- // FIX: Use the correctly imported 'readFile' function directly
41
- htmlContent = await readFile(entryFilePath, 'utf-8');
42
- logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
43
- } catch (err: any) {
44
- logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
45
- throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
46
- }
36
+ logger?.debug(`Parsing HTML file: ${entryFilePath}`);
37
+ let htmlContent: string;
38
+ try {
39
+ // FIX: Use the correctly imported 'readFile' function directly
40
+ htmlContent = await readFile(entryFilePath, 'utf-8');
41
+ logger?.debug(`Successfully read HTML file (${Buffer.byteLength(htmlContent)} bytes).`);
42
+ } catch (err: any) {
43
+ logger?.error(`Failed to read HTML file "${entryFilePath}": ${err.message}`);
44
+ throw new Error(`Could not read input HTML file: ${entryFilePath}`, { cause: err });
45
+ }
47
46
 
48
- const $: CheerioAPI = cheerio.load(htmlContent);
49
- const assets: Asset[] = [];
50
- const addedUrls = new Set<string>();
47
+ const $: CheerioAPI = cheerio.load(htmlContent);
48
+ const assets: Asset[] = [];
49
+ const addedUrls = new Set<string>();
51
50
 
52
- /** Helper to add unique assets */
53
- const addAsset = (url?: string, forcedType?: Asset['type']): void => {
54
- if (!url || url.trim() === '' || url.startsWith('data:')) {
55
- return;
56
- }
57
- if (!addedUrls.has(url)) {
58
- addedUrls.add(url);
59
- const mimeInfo = guessMimeType(url);
60
- const type = forcedType ?? mimeInfo.assetType;
61
- assets.push({ type, url });
62
- logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
63
- } else {
64
- logger?.debug(`Skipping duplicate asset URL: ${url}`);
65
- }
66
- };
51
+ /** Helper to add unique assets */
52
+ const addAsset = (url?: string, forcedType?: Asset['type']): void => {
53
+ if (!url || url.trim() === '' || url.startsWith('data:')) {
54
+ return;
55
+ }
56
+ if (!addedUrls.has(url)) {
57
+ addedUrls.add(url);
58
+ const mimeInfo = guessMimeType(url);
59
+ const type = forcedType ?? mimeInfo.assetType;
60
+ assets.push({ type, url });
61
+ logger?.debug(`Discovered asset: Type='${type}', URL='${url}'`);
62
+ } else {
63
+ logger?.debug(`Skipping duplicate asset URL: ${url}`);
64
+ }
65
+ };
67
66
 
68
- logger?.debug('Extracting assets from HTML tags...');
67
+ logger?.debug('Extracting assets from HTML tags...');
69
68
 
70
- // --- Extract Assets from Various Tags ---
71
- // Stylesheets: <link rel="stylesheet" href="...">
72
- $('link[rel="stylesheet"][href]').each((_, el) => {
73
- addAsset($(el).attr('href'), 'css');
74
- });
75
- // JavaScript: <script src="...">
76
- $('script[src]').each((_, el) => {
77
- addAsset($(el).attr('src'), 'js');
78
- });
79
- // Images: <img src="...">, <input type="image" src="...">
80
- $('img[src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
81
- $('input[type="image"][src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
82
- // Image srcset: <img srcset="...">, <source srcset="..."> (within picture)
83
- $('img[srcset], picture source[srcset]').each((_, el) => {
84
- const srcset = $(el).attr('srcset');
85
- srcset?.split(',').forEach(entry => {
86
- const [url] = entry.trim().split(/\s+/);
87
- addAsset(url, 'image');
88
- });
69
+ // --- Extract Assets from Various Tags ---
70
+ // Stylesheets: <link rel="stylesheet" href="...">
71
+ $('link[rel="stylesheet"][href]').each((_, el) => {
72
+ addAsset($(el).attr('href'), 'css');
73
+ });
74
+ // JavaScript: <script src="...">
75
+ $('script[src]').each((_, el) => {
76
+ addAsset($(el).attr('src'), 'js');
77
+ });
78
+ // Images: <img src="...">, <input type="image" src="...">
79
+ $('img[src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
80
+ $('input[type="image"][src]').each((_, el) => addAsset($(el).attr('src'), 'image'));
81
+ // Image srcset: <img srcset="...">, <source srcset="..."> (within picture)
82
+ $('img[srcset], picture source[srcset]').each((_, el) => {
83
+ const srcset = $(el).attr('srcset');
84
+ srcset?.split(',').forEach(entry => {
85
+ const [url] = entry.trim().split(/\s+/);
86
+ addAsset(url, 'image');
89
87
  });
90
- // Video: <video src="...">, <video poster="...">
91
- $('video[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
92
- $('video[poster]').each((_, el) => addAsset($(el).attr('poster'), 'image'));
93
- // Audio: <audio src="...">
94
- $('audio[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
95
- // Media Sources: <source src="..."> within <video> or <audio>
96
- $('video > source[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
97
- $('audio > source[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
98
- // Icons and Manifest: <link rel="icon/shortcut icon/apple-touch-icon/manifest" href="...">
99
- $('link[href]').filter((_, el) => {
100
- const rel = $(el).attr('rel')?.toLowerCase() ?? '';
101
- return ['icon', 'shortcut icon', 'apple-touch-icon', 'manifest'].includes(rel);
102
- }).each((_, el) => {
103
- const rel = $(el).attr('rel')?.toLowerCase() ?? '';
104
- const isIcon = ['icon', 'shortcut icon', 'apple-touch-icon'].includes(rel);
105
- addAsset($(el).attr('href'), isIcon ? 'image' : undefined);
106
- });
107
- // Preloaded Fonts: <link rel="preload" as="font" href="...">
108
- $('link[rel="preload"][as="font"][href]').each((_, el) => {
109
- addAsset($(el).attr('href'), 'font');
88
+ });
89
+ // Video: <video src="...">, <video poster="...">
90
+ $('video[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
91
+ $('video[poster]').each((_, el) => addAsset($(el).attr('poster'), 'image'));
92
+ // Audio: <audio src="...">
93
+ $('audio[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
94
+ // Media Sources: <source src="..."> within <video> or <audio>
95
+ $('video > source[src]').each((_, el) => addAsset($(el).attr('src'), 'video'));
96
+ $('audio > source[src]').each((_, el) => addAsset($(el).attr('src'), 'audio'));
97
+ // Icons and Manifest: <link rel="icon/shortcut icon/apple-touch-icon/manifest" href="...">
98
+ $('link[href]')
99
+ .filter((_, el) => {
100
+ const rel = $(el).attr('rel')?.toLowerCase() ?? '';
101
+ return ['icon', 'shortcut icon', 'apple-touch-icon', 'manifest'].includes(rel);
102
+ })
103
+ .each((_, el) => {
104
+ const rel = $(el).attr('rel')?.toLowerCase() ?? '';
105
+ const isIcon = ['icon', 'shortcut icon', 'apple-touch-icon'].includes(rel);
106
+ addAsset($(el).attr('href'), isIcon ? 'image' : undefined);
110
107
  });
108
+ // Preloaded Fonts: <link rel="preload" as="font" href="...">
109
+ $('link[rel="preload"][as="font"][href]').each((_, el) => {
110
+ addAsset($(el).attr('href'), 'font');
111
+ });
111
112
 
112
- // --- Parsing Complete ---
113
- logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
114
- return { htmlContent, assets };
115
- }
113
+ // --- Parsing Complete ---
114
+ logger?.info(`HTML parsing complete. Discovered ${assets.length} unique asset links.`);
115
+ return { htmlContent, assets };
116
+ }