@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.
- package/dist/cli.js +46 -4
- package/dist/cli.js.map +1 -1
- package/dist/ffmpeg.d.ts.map +1 -1
- package/dist/ffmpeg.js +9 -0
- package/dist/ffmpeg.js.map +1 -1
- package/dist/ffprobe.d.ts.map +1 -1
- package/dist/ffprobe.js +11 -2
- package/dist/ffprobe.js.map +1 -1
- package/dist/html-project-parser.d.ts +5 -0
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +94 -107
- package/dist/html-project-parser.js.map +1 -1
- package/package.json +5 -1
- package/src/cli.ts +51 -4
- package/src/ffmpeg.ts +12 -0
- package/src/ffprobe.ts +11 -3
- package/src/html-project-parser.ts +129 -125
|
@@ -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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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(
|
|
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
|
-
):
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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') ||
|
|
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(
|
|
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(
|
|
765
|
+
const idAttr = containerElement.attrs.find(
|
|
766
|
+
(attr) => attr.name === 'id',
|
|
767
|
+
);
|
|
760
768
|
const id =
|
|
761
|
-
idAttr?.value ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
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]
|
|
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)
|
|
1104
|
+
objectFitContainAmbientBlurStrength: isNaN(blur)
|
|
1105
|
+
? defaults.objectFitContainAmbientBlurStrength
|
|
1106
|
+
: blur,
|
|
1103
1107
|
objectFitContainAmbientBrightness: isNaN(brightness)
|
|
1104
1108
|
? defaults.objectFitContainAmbientBrightness
|
|
1105
1109
|
: brightness,
|