@gannochenko/staticstripes 0.0.2 → 0.0.6

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.
@@ -11,11 +11,9 @@ import {
11
11
  import { execFile } from 'child_process';
12
12
  import { promisify } from 'util';
13
13
  import { resolve, dirname } from 'path';
14
+ import { existsSync } from 'fs';
14
15
  import { Project } from './project';
15
- import {
16
- parseValueLazy,
17
- CompiledExpression,
18
- } from './expression-parser';
16
+ import { parseValueLazy, CompiledExpression } from './expression-parser';
19
17
 
20
18
  const execFileAsync = promisify(execFile);
21
19
 
@@ -31,6 +29,10 @@ export class HTMLProjectParser {
31
29
 
32
30
  public async parse(): Promise<Project> {
33
31
  const assets = await this.processAssets();
32
+
33
+ // Preflight check: verify all assets exist
34
+ this.validateAssetFiles(assets);
35
+
34
36
  const outputs = this.processOutputs();
35
37
  const sequences = this.processSequences(assets);
36
38
  const cssText = this.html.cssText;
@@ -38,6 +40,27 @@ export class HTMLProjectParser {
38
40
  return new Project(sequences, assets, outputs, cssText, this.projectPath);
39
41
  }
40
42
 
43
+ /**
44
+ * Validates that all asset files exist on the filesystem
45
+ * Throws an error with a list of missing files if any are not found
46
+ */
47
+ private validateAssetFiles(assets: Asset[]): void {
48
+ const missingFiles: string[] = [];
49
+
50
+ for (const asset of assets) {
51
+ if (!existsSync(asset.path)) {
52
+ missingFiles.push(asset.path);
53
+ }
54
+ }
55
+
56
+ if (missingFiles.length > 0) {
57
+ const fileList = missingFiles.map(f => ` - ${f}`).join('\n');
58
+ throw new Error(
59
+ `Asset file(s) not found:\n${fileList}\n\nPlease check that all asset paths in project.html are correct.`
60
+ );
61
+ }
62
+ }
63
+
41
64
  /**
42
65
  * Processes asset elements from the parsed HTML and builds an assets map
43
66
  */
@@ -193,28 +216,22 @@ export class HTMLProjectParser {
193
216
  return 0;
194
217
  }
195
218
 
196
- try {
197
- const { stdout } = await execFileAsync('ffprobe', [
198
- '-v',
199
- 'error',
200
- '-show_entries',
201
- 'format=duration',
202
- '-of',
203
- 'default=noprint_wrappers=1:nokey=1',
204
- path,
205
- ]);
206
-
207
- const durationSeconds = parseFloat(stdout.trim());
208
- if (isNaN(durationSeconds)) {
209
- console.warn(`Could not parse duration for asset: ${path}`);
210
- return 0;
211
- }
212
-
213
- return Math.round(durationSeconds * 1000);
214
- } catch (error) {
215
- console.error(`Failed to get duration for asset: ${path}`, error);
216
- return 0;
219
+ const { stdout } = await execFileAsync('ffprobe', [
220
+ '-v',
221
+ 'error',
222
+ '-show_entries',
223
+ 'format=duration',
224
+ '-of',
225
+ 'default=noprint_wrappers=1:nokey=1',
226
+ path,
227
+ ]);
228
+
229
+ const durationSeconds = parseFloat(stdout.trim());
230
+ if (isNaN(durationSeconds)) {
231
+ throw new Error(`Could not parse duration for asset: ${path}`);
217
232
  }
233
+
234
+ return Math.round(durationSeconds * 1000);
218
235
  }
219
236
 
220
237
  /**
@@ -232,33 +249,28 @@ export class HTMLProjectParser {
232
249
  return 0;
233
250
  }
234
251
 
235
- try {
236
- const { stdout } = await execFileAsync('ffprobe', [
237
- '-v',
238
- 'error',
239
- '-select_streams',
240
- 'v:0',
241
- '-show_entries',
242
- 'stream_side_data=rotation',
243
- '-of',
244
- 'default=noprint_wrappers=1:nokey=1',
245
- path,
246
- ]);
247
-
248
- const rotation = parseInt(stdout.trim(), 10);
249
-
250
- if (isNaN(rotation)) {
251
- // No rotation metadata found
252
- return 0;
253
- }
254
-
255
- // Normalize to 0, 90, 180, 270
256
- const normalized = Math.abs(rotation) % 360;
257
- return normalized;
258
- } catch (error) {
259
- // No rotation metadata or error - default to 0
252
+ const { stdout } = await execFileAsync('ffprobe', [
253
+ '-v',
254
+ 'error',
255
+ '-select_streams',
256
+ 'v:0',
257
+ '-show_entries',
258
+ 'stream_side_data=rotation',
259
+ '-of',
260
+ 'default=noprint_wrappers=1:nokey=1',
261
+ path,
262
+ ]);
263
+
264
+ const rotation = parseInt(stdout.trim(), 10);
265
+
266
+ if (isNaN(rotation)) {
267
+ // No rotation metadata found
260
268
  return 0;
261
269
  }
270
+
271
+ // Normalize to 0, 90, 180, 270
272
+ const normalized = Math.abs(rotation) % 360;
273
+ return normalized;
262
274
  }
263
275
 
264
276
  /**
@@ -276,34 +288,28 @@ export class HTMLProjectParser {
276
288
  return { width: 0, height: 0 };
277
289
  }
278
290
 
279
- try {
280
- const { stdout } = await execFileAsync('ffprobe', [
281
- '-v',
282
- 'error',
283
- '-select_streams',
284
- 'v:0',
285
- '-show_entries',
286
- 'stream=width,height',
287
- '-of',
288
- 'csv=s=x:p=0',
289
- path,
290
- ]);
291
-
292
- const dimensions = stdout.trim();
293
- const [widthStr, heightStr] = dimensions.split('x');
294
- const width = parseInt(widthStr, 10);
295
- const height = parseInt(heightStr, 10);
296
-
297
- if (isNaN(width) || isNaN(height)) {
298
- console.warn(`Could not parse dimensions for asset: ${path}`);
299
- return { width: 0, height: 0 };
300
- }
301
-
302
- return { width, height };
303
- } catch (error) {
304
- console.error(`Failed to get dimensions for asset: ${path}`, error);
305
- return { width: 0, height: 0 };
291
+ const { stdout } = await execFileAsync('ffprobe', [
292
+ '-v',
293
+ 'error',
294
+ '-select_streams',
295
+ 'v:0',
296
+ '-show_entries',
297
+ 'stream=width,height',
298
+ '-of',
299
+ 'csv=s=x:p=0',
300
+ path,
301
+ ]);
302
+
303
+ const dimensions = stdout.trim();
304
+ const [widthStr, heightStr] = dimensions.split('x');
305
+ const width = parseInt(widthStr, 10);
306
+ const height = parseInt(heightStr, 10);
307
+
308
+ if (isNaN(width) || isNaN(height)) {
309
+ throw new Error(`Could not parse dimensions for: ${path}`);
306
310
  }
311
+
312
+ return { width, height };
307
313
  }
308
314
 
309
315
  /**
@@ -350,25 +356,20 @@ export class HTMLProjectParser {
350
356
  }
351
357
 
352
358
  // For video, probe for audio stream
353
- try {
354
- const { stdout } = await execFileAsync('ffprobe', [
355
- '-v',
356
- 'error',
357
- '-select_streams',
358
- 'a:0',
359
- '-show_entries',
360
- 'stream=codec_type',
361
- '-of',
362
- 'default=noprint_wrappers=1:nokey=1',
363
- path,
364
- ]);
365
-
366
- // If we get output, an audio stream exists
367
- return stdout.trim() === 'audio';
368
- } catch (error) {
369
- // No audio stream or error
370
- return false;
371
- }
359
+ const { stdout } = await execFileAsync('ffprobe', [
360
+ '-v',
361
+ 'error',
362
+ '-select_streams',
363
+ 'a:0',
364
+ '-show_entries',
365
+ 'stream=codec_type',
366
+ '-of',
367
+ 'default=noprint_wrappers=1:nokey=1',
368
+ path,
369
+ ]);
370
+
371
+ // If we get output, an audio stream exists
372
+ return stdout.trim() === 'audio';
372
373
  }
373
374
 
374
375
  /**
@@ -394,7 +395,9 @@ export class HTMLProjectParser {
394
395
 
395
396
  // Process each output element
396
397
  for (const element of outputElements) {
397
- const attrs = new Map(element.attrs.map((attr) => [attr.name, attr.value]));
398
+ const attrs = new Map(
399
+ element.attrs.map((attr) => [attr.name, attr.value]),
400
+ );
398
401
 
399
402
  // Extract name
400
403
  const name = attrs.get('name') || 'output';
@@ -628,16 +631,19 @@ export class HTMLProjectParser {
628
631
  private processFragment(
629
632
  element: Element,
630
633
  assets: Map<string, Asset>,
631
- ): (Fragment & {
632
- overlayRight: number | CompiledExpression;
633
- overlayZIndexRight: number;
634
- }) | null {
634
+ ):
635
+ | (Fragment & {
636
+ overlayRight: number | CompiledExpression;
637
+ overlayZIndexRight: number;
638
+ })
639
+ | null {
635
640
  const attrs = new Map(element.attrs.map((attr) => [attr.name, attr.value]));
636
641
  const styles = this.html.css.get(element) || {};
637
642
 
638
643
  // 1. Extract fragment ID from id attribute or generate one
639
644
  const id =
640
- attrs.get('id') || `fragment_${Math.random().toString(36).substring(2, 11)}`;
645
+ attrs.get('id') ||
646
+ `fragment_${Math.random().toString(36).substring(2, 11)}`;
641
647
 
642
648
  // 2. Extract assetName from attribute or CSS -asset property
643
649
  const assetName = attrs.get('data-asset') || styles['-asset'] || '';
@@ -669,9 +675,7 @@ export class HTMLProjectParser {
669
675
  const overlayZIndex = this.parseZIndex(styles['-overlay-start-z-index']);
670
676
 
671
677
  // 10. Parse -overlay-end-z-index for overlayZIndexRight (temporary)
672
- const overlayZIndexRight = this.parseZIndex(
673
- styles['-overlay-end-z-index'],
674
- );
678
+ const overlayZIndexRight = this.parseZIndex(styles['-overlay-end-z-index']);
675
679
 
676
680
  // 11. Parse -transition-start
677
681
  const transitionIn = this.parseTransitionProperty(
@@ -679,7 +683,9 @@ export class HTMLProjectParser {
679
683
  );
680
684
 
681
685
  // 12. Parse -transition-end
682
- const transitionOut = this.parseTransitionProperty(styles['-transition-end']);
686
+ const transitionOut = this.parseTransitionProperty(
687
+ styles['-transition-end'],
688
+ );
683
689
 
684
690
  // 13. Parse -object-fit
685
691
  const objectFitData = this.parseObjectFitProperty(styles['-object-fit']);
@@ -756,9 +762,12 @@ export class HTMLProjectParser {
756
762
  const containerElement = child as Element;
757
763
 
758
764
  // Get id attribute
759
- const idAttr = containerElement.attrs.find((attr) => attr.name === 'id');
765
+ const idAttr = containerElement.attrs.find(
766
+ (attr) => attr.name === 'id',
767
+ );
760
768
  const id =
761
- idAttr?.value || `container_${Math.random().toString(36).substring(2, 11)}`;
769
+ idAttr?.value ||
770
+ `container_${Math.random().toString(36).substring(2, 11)}`;
762
771
 
763
772
  // Get innerHTML (serialize all children)
764
773
  const htmlContent = this.serializeElement(containerElement);
@@ -958,12 +967,7 @@ export class HTMLProjectParser {
958
967
 
959
968
  // Check if it's a calc() expression
960
969
  if (trimmed.startsWith('calc(')) {
961
- try {
962
- return parseValueLazy(trimmed) as CompiledExpression;
963
- } catch (error) {
964
- console.error(`Failed to parse -offset-start expression: ${trimmed}`, error);
965
- return 0;
966
- }
970
+ return parseValueLazy(trimmed) as CompiledExpression;
967
971
  }
968
972
 
969
973
  // Otherwise parse as time value
@@ -985,12 +989,7 @@ export class HTMLProjectParser {
985
989
 
986
990
  // Check if it's a calc() expression
987
991
  if (trimmed.startsWith('calc(')) {
988
- try {
989
- return parseValueLazy(trimmed) as CompiledExpression;
990
- } catch (error) {
991
- console.error(`Failed to parse -offset-end expression: ${trimmed}`, error);
992
- return 0;
993
- }
992
+ return parseValueLazy(trimmed) as CompiledExpression;
994
993
  }
995
994
 
996
995
  // Otherwise parse as time value
@@ -1014,9 +1013,10 @@ export class HTMLProjectParser {
1014
1013
  * Format: "<transition-name> <duration>"
1015
1014
  * Example: "fade-in 5s", "fade-out 500ms"
1016
1015
  */
1017
- private parseTransitionProperty(
1018
- transition: string | undefined,
1019
- ): { name: string; duration: number } {
1016
+ private parseTransitionProperty(transition: string | undefined): {
1017
+ name: string;
1018
+ duration: number;
1019
+ } {
1020
1020
  if (!transition) {
1021
1021
  return { name: '', duration: 0 };
1022
1022
  }
@@ -1087,7 +1087,9 @@ export class HTMLProjectParser {
1087
1087
 
1088
1088
  // "contain ambient <blur> <brightness> <saturation>"
1089
1089
  if (subType === 'ambient') {
1090
- const blur = parts[2] ? parseFloat(parts[2]) : defaults.objectFitContainAmbientBlurStrength;
1090
+ const blur = parts[2]
1091
+ ? parseFloat(parts[2])
1092
+ : defaults.objectFitContainAmbientBlurStrength;
1091
1093
  const brightness = parts[3]
1092
1094
  ? parseFloat(parts[3])
1093
1095
  : defaults.objectFitContainAmbientBrightness;
@@ -1099,7 +1101,9 @@ export class HTMLProjectParser {
1099
1101
  ...defaults,
1100
1102
  objectFit: 'contain',
1101
1103
  objectFitContain: 'ambient',
1102
- objectFitContainAmbientBlurStrength: isNaN(blur) ? defaults.objectFitContainAmbientBlurStrength : blur,
1104
+ objectFitContainAmbientBlurStrength: isNaN(blur)
1105
+ ? defaults.objectFitContainAmbientBlurStrength
1106
+ : blur,
1103
1107
  objectFitContainAmbientBrightness: isNaN(brightness)
1104
1108
  ? defaults.objectFitContainAmbientBrightness
1105
1109
  : brightness,