@gannochenko/staticstripes 0.0.11 → 0.0.12
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/Makefile +17 -4
- package/dist/cli/commands/add-assets.d.ts +3 -0
- package/dist/cli/commands/add-assets.d.ts.map +1 -0
- package/dist/cli/commands/add-assets.js +113 -0
- package/dist/cli/commands/add-assets.js.map +1 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +49 -0
- package/dist/cli/commands/bootstrap.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +3 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +132 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/upload.d.ts +6 -0
- package/dist/cli/commands/upload.d.ts.map +1 -0
- package/dist/cli/commands/upload.js +67 -0
- package/dist/cli/commands/upload.js.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts +18 -0
- package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -0
- package/dist/cli/s3/s3-upload-strategy.js +149 -0
- package/dist/cli/s3/s3-upload-strategy.js.map +1 -0
- package/dist/cli/upload-strategy-factory.d.ts +23 -0
- package/dist/cli/upload-strategy-factory.d.ts.map +1 -0
- package/dist/cli/upload-strategy-factory.js +49 -0
- package/dist/cli/upload-strategy-factory.js.map +1 -0
- package/dist/cli/upload-strategy.d.ts +25 -0
- package/dist/cli/upload-strategy.d.ts.map +1 -0
- package/dist/cli/upload-strategy.js +3 -0
- package/dist/cli/upload-strategy.js.map +1 -0
- package/dist/cli/youtube/auth-commands.d.ts +3 -0
- package/dist/cli/youtube/auth-commands.d.ts.map +1 -0
- package/dist/cli/youtube/auth-commands.js +273 -0
- package/dist/cli/youtube/auth-commands.js.map +1 -0
- package/dist/cli/youtube/cli.d.ts +7 -0
- package/dist/cli/youtube/cli.d.ts.map +1 -0
- package/dist/cli/youtube/cli.js +13 -0
- package/dist/cli/youtube/cli.js.map +1 -0
- package/dist/cli/youtube/upload-handler.d.ts +12 -0
- package/dist/cli/youtube/upload-handler.d.ts.map +1 -0
- package/dist/cli/youtube/upload-handler.js +66 -0
- package/dist/cli/youtube/upload-handler.js.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts +15 -0
- package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -0
- package/dist/cli/youtube/youtube-upload-strategy.js +37 -0
- package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -0
- package/dist/cli.js +12 -281
- package/dist/cli.js.map +1 -1
- package/dist/html-parser.d.ts +3 -4
- package/dist/html-parser.d.ts.map +1 -1
- package/dist/html-parser.js +20 -17
- package/dist/html-parser.js.map +1 -1
- package/dist/html-project-parser.d.ts +24 -0
- package/dist/html-project-parser.d.ts.map +1 -1
- package/dist/html-project-parser.js +361 -57
- package/dist/html-project-parser.js.map +1 -1
- package/dist/project.d.ts +15 -2
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +57 -1
- package/dist/project.js.map +1 -1
- package/dist/type.d.ts +26 -4
- package/dist/type.d.ts.map +1 -1
- package/dist/youtube-uploader.d.ts +40 -0
- package/dist/youtube-uploader.d.ts.map +1 -0
- package/dist/youtube-uploader.js +227 -0
- package/dist/youtube-uploader.js.map +1 -0
- package/package.json +6 -2
- package/src/cli/commands/add-assets.ts +159 -0
- package/src/cli/commands/bootstrap.ts +57 -0
- package/src/cli/commands/generate.ts +189 -0
- package/src/cli/commands/upload.ts +83 -0
- package/src/cli/s3/s3-upload-strategy.ts +194 -0
- package/src/cli/upload-strategy-factory.ts +58 -0
- package/src/cli/upload-strategy.ts +31 -0
- package/src/cli/youtube/auth-commands.ts +312 -0
- package/src/cli/youtube/cli.ts +11 -0
- package/src/cli/youtube/upload-handler.ts +101 -0
- package/src/cli/youtube/youtube-upload-strategy.ts +43 -0
- package/src/cli.ts +14 -390
- package/src/html-parser.ts +23 -21
- package/src/html-project-parser.ts +400 -62
- package/src/project.ts +71 -1
- package/src/type.ts +30 -4
- package/src/youtube-uploader.ts +288 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Fragment,
|
|
9
9
|
Container,
|
|
10
10
|
FFmpegOption,
|
|
11
|
+
Upload,
|
|
11
12
|
} from './type';
|
|
12
13
|
import { execFile } from 'child_process';
|
|
13
14
|
import { promisify } from 'util';
|
|
@@ -18,6 +19,19 @@ import { parseValueLazy, CompiledExpression } from './expression-parser';
|
|
|
18
19
|
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Helper to get attributes as a Map from htmlparser2 element
|
|
24
|
+
*/
|
|
25
|
+
function getAttrs(element: Element): Map<string, string> {
|
|
26
|
+
const map = new Map<string, string>();
|
|
27
|
+
if (element.attribs) {
|
|
28
|
+
for (const [name, value] of Object.entries(element.attribs)) {
|
|
29
|
+
map.set(name, value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return map;
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
export class HTMLProjectParser {
|
|
22
36
|
private projectDir: string;
|
|
23
37
|
|
|
@@ -36,6 +50,8 @@ export class HTMLProjectParser {
|
|
|
36
50
|
|
|
37
51
|
const outputs = this.processOutputs();
|
|
38
52
|
const ffmpegOptions = this.processFfmpegOptions();
|
|
53
|
+
const uploads = this.processUploads();
|
|
54
|
+
const title = this.processTitle();
|
|
39
55
|
const sequences = this.processSequences(assets);
|
|
40
56
|
const cssText = this.html.cssText;
|
|
41
57
|
|
|
@@ -44,6 +60,8 @@ export class HTMLProjectParser {
|
|
|
44
60
|
assets,
|
|
45
61
|
outputs,
|
|
46
62
|
ffmpegOptions,
|
|
63
|
+
uploads,
|
|
64
|
+
title,
|
|
47
65
|
cssText,
|
|
48
66
|
this.projectPath,
|
|
49
67
|
);
|
|
@@ -96,17 +114,17 @@ export class HTMLProjectParser {
|
|
|
96
114
|
const results: Element[] = [];
|
|
97
115
|
|
|
98
116
|
const traverse = (node: ASTNode) => {
|
|
99
|
-
if ('
|
|
117
|
+
if (node.type === 'tag') {
|
|
100
118
|
const element = node as Element;
|
|
101
119
|
|
|
102
120
|
// Check if element is an <asset> tag
|
|
103
|
-
if (element.
|
|
121
|
+
if (element.name === 'asset') {
|
|
104
122
|
results.push(element);
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
if ('
|
|
109
|
-
for (const child of node.
|
|
126
|
+
if ('children' in node && node.children) {
|
|
127
|
+
for (const child of node.children) {
|
|
110
128
|
traverse(child);
|
|
111
129
|
}
|
|
112
130
|
}
|
|
@@ -122,7 +140,7 @@ export class HTMLProjectParser {
|
|
|
122
140
|
private async extractAssetFromElement(
|
|
123
141
|
element: Element,
|
|
124
142
|
): Promise<Asset | null> {
|
|
125
|
-
const attrs =
|
|
143
|
+
const attrs = getAttrs(element);
|
|
126
144
|
|
|
127
145
|
// Extract name (required)
|
|
128
146
|
const name = attrs.get('data-name') || attrs.get('id');
|
|
@@ -152,7 +170,7 @@ export class HTMLProjectParser {
|
|
|
152
170
|
type = explicitType;
|
|
153
171
|
} else {
|
|
154
172
|
// Infer from tag name or file extension
|
|
155
|
-
type = this.inferAssetType(element.
|
|
173
|
+
type = this.inferAssetType(element.name, relativePath);
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
// Get duration using ffprobe (in ms) - only for audio/video
|
|
@@ -404,9 +422,7 @@ export class HTMLProjectParser {
|
|
|
404
422
|
|
|
405
423
|
// Process each output element
|
|
406
424
|
for (const element of outputElements) {
|
|
407
|
-
const attrs =
|
|
408
|
-
element.attrs.map((attr) => [attr.name, attr.value]),
|
|
409
|
-
);
|
|
425
|
+
const attrs = getAttrs(element);
|
|
410
426
|
|
|
411
427
|
// Extract name
|
|
412
428
|
const name = attrs.get('name') || 'output';
|
|
@@ -447,17 +463,17 @@ export class HTMLProjectParser {
|
|
|
447
463
|
const results: Element[] = [];
|
|
448
464
|
|
|
449
465
|
const traverse = (node: ASTNode) => {
|
|
450
|
-
if ('
|
|
466
|
+
if (node.type === 'tag') {
|
|
451
467
|
const element = node as Element;
|
|
452
468
|
|
|
453
469
|
// Check if element is an <output> tag
|
|
454
|
-
if (element.
|
|
470
|
+
if (element.name === 'output') {
|
|
455
471
|
results.push(element);
|
|
456
472
|
}
|
|
457
473
|
}
|
|
458
474
|
|
|
459
|
-
if ('
|
|
460
|
-
for (const child of node.
|
|
475
|
+
if ('children' in node && node.children) {
|
|
476
|
+
for (const child of node.children) {
|
|
461
477
|
traverse(child);
|
|
462
478
|
}
|
|
463
479
|
}
|
|
@@ -477,14 +493,12 @@ export class HTMLProjectParser {
|
|
|
477
493
|
// Process each <ffmpeg> element (should typically be only one)
|
|
478
494
|
for (const ffmpegElement of ffmpegElements) {
|
|
479
495
|
// Find all <option> child elements
|
|
480
|
-
if ('
|
|
481
|
-
for (const child of ffmpegElement.
|
|
482
|
-
if ('
|
|
496
|
+
if ('children' in ffmpegElement && ffmpegElement.children) {
|
|
497
|
+
for (const child of ffmpegElement.children) {
|
|
498
|
+
if (child.type === 'tag') {
|
|
483
499
|
const childElement = child as Element;
|
|
484
|
-
if (childElement.
|
|
485
|
-
const attrs =
|
|
486
|
-
childElement.attrs.map((attr) => [attr.name, attr.value]),
|
|
487
|
-
);
|
|
500
|
+
if (childElement.name === 'option') {
|
|
501
|
+
const attrs = getAttrs(childElement);
|
|
488
502
|
|
|
489
503
|
const name = attrs.get('name');
|
|
490
504
|
if (!name) {
|
|
@@ -493,10 +507,10 @@ export class HTMLProjectParser {
|
|
|
493
507
|
|
|
494
508
|
// Get the text content (the FFmpeg arguments)
|
|
495
509
|
let args = '';
|
|
496
|
-
if ('
|
|
497
|
-
for (const textNode of childElement.
|
|
498
|
-
if ('
|
|
499
|
-
args += textNode.
|
|
510
|
+
if ('children' in childElement && childElement.children) {
|
|
511
|
+
for (const textNode of childElement.children) {
|
|
512
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
513
|
+
args += textNode.data;
|
|
500
514
|
}
|
|
501
515
|
}
|
|
502
516
|
}
|
|
@@ -526,17 +540,338 @@ export class HTMLProjectParser {
|
|
|
526
540
|
const results: Element[] = [];
|
|
527
541
|
|
|
528
542
|
const traverse = (node: ASTNode) => {
|
|
529
|
-
if ('
|
|
543
|
+
if (node.type === 'tag') {
|
|
530
544
|
const element = node as Element;
|
|
531
545
|
|
|
532
546
|
// Check if element is an <ffmpeg> tag
|
|
533
|
-
if (element.
|
|
547
|
+
if (element.name === 'ffmpeg') {
|
|
534
548
|
results.push(element);
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
|
|
538
|
-
if ('
|
|
539
|
-
for (const child of node.
|
|
552
|
+
if ('children' in node && node.children) {
|
|
553
|
+
for (const child of node.children) {
|
|
554
|
+
traverse(child);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
traverse(this.html.ast);
|
|
560
|
+
return results;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Processes all uploads (YouTube, S3, etc.) from the parsed HTML
|
|
565
|
+
*/
|
|
566
|
+
private processUploads(): Map<string, Upload> {
|
|
567
|
+
const uploadsElements = this.findUploadsElements();
|
|
568
|
+
const uploads = new Map<string, Upload>();
|
|
569
|
+
|
|
570
|
+
for (const uploadsElement of uploadsElements) {
|
|
571
|
+
if ('children' in uploadsElement && uploadsElement.children) {
|
|
572
|
+
for (const child of uploadsElement.children) {
|
|
573
|
+
if (child.type === 'tag') {
|
|
574
|
+
const childElement = child as Element;
|
|
575
|
+
let upload: Upload | null = null;
|
|
576
|
+
|
|
577
|
+
if (childElement.name === 'youtube') {
|
|
578
|
+
upload = this.parseYouTubeElement(childElement);
|
|
579
|
+
} else if (childElement.name === 's3') {
|
|
580
|
+
upload = this.parseS3Element(childElement);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (upload) {
|
|
584
|
+
uploads.set(upload.name, upload);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return uploads;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Parses a single <youtube> element
|
|
596
|
+
*/
|
|
597
|
+
private parseYouTubeElement(element: Element): Upload | null {
|
|
598
|
+
const attrs = getAttrs(element);
|
|
599
|
+
|
|
600
|
+
const name = attrs.get('name');
|
|
601
|
+
const outputName = attrs.get('data-output-name');
|
|
602
|
+
|
|
603
|
+
if (!name || !outputName) {
|
|
604
|
+
console.warn('YouTube upload missing name or data-output-name attribute');
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const videoId = attrs.get('id') || undefined;
|
|
609
|
+
|
|
610
|
+
// Parse child elements
|
|
611
|
+
let uploadTitle: string | undefined;
|
|
612
|
+
let privacy: 'public' | 'unlisted' | 'private' = 'private';
|
|
613
|
+
let madeForKids = false;
|
|
614
|
+
const tags: string[] = [];
|
|
615
|
+
let category = 'entertainment';
|
|
616
|
+
let language = 'en';
|
|
617
|
+
let description = '';
|
|
618
|
+
let thumbnailTimecode: number | undefined;
|
|
619
|
+
|
|
620
|
+
if ('children' in element && element.children) {
|
|
621
|
+
for (const child of element.children) {
|
|
622
|
+
if (child.type === 'tag') {
|
|
623
|
+
const childElement = child as Element;
|
|
624
|
+
|
|
625
|
+
switch (childElement.name) {
|
|
626
|
+
case 'title': {
|
|
627
|
+
// Get text content
|
|
628
|
+
if ('children' in childElement && childElement.children) {
|
|
629
|
+
for (const textNode of childElement.children) {
|
|
630
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
631
|
+
uploadTitle = (uploadTitle || '') + textNode.data;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
uploadTitle = uploadTitle?.trim();
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'public':
|
|
639
|
+
privacy = 'public';
|
|
640
|
+
break;
|
|
641
|
+
case 'unlisted':
|
|
642
|
+
privacy = 'unlisted';
|
|
643
|
+
break;
|
|
644
|
+
case 'private':
|
|
645
|
+
privacy = 'private';
|
|
646
|
+
break;
|
|
647
|
+
case 'made-for-kids':
|
|
648
|
+
madeForKids = true;
|
|
649
|
+
break;
|
|
650
|
+
case 'tag': {
|
|
651
|
+
const tagAttrs = getAttrs(childElement);
|
|
652
|
+
const tagName = tagAttrs.get('name');
|
|
653
|
+
if (tagName) {
|
|
654
|
+
tags.push(tagName);
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case 'category': {
|
|
659
|
+
const catAttrs = getAttrs(childElement);
|
|
660
|
+
const catName = catAttrs.get('name');
|
|
661
|
+
if (catName) {
|
|
662
|
+
category = catName;
|
|
663
|
+
}
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case 'language': {
|
|
667
|
+
const langAttrs = getAttrs(childElement);
|
|
668
|
+
const langName = langAttrs.get('name');
|
|
669
|
+
if (langName) {
|
|
670
|
+
language = langName;
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case 'pre': {
|
|
675
|
+
// Get text content
|
|
676
|
+
if ('children' in childElement && childElement.children) {
|
|
677
|
+
for (const textNode of childElement.children) {
|
|
678
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
679
|
+
description += textNode.data;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
case 'thumbnail': {
|
|
686
|
+
const thumbAttrs = getAttrs(childElement);
|
|
687
|
+
const timecode = thumbAttrs.get('data-timecode');
|
|
688
|
+
if (timecode) {
|
|
689
|
+
// Parse timecode (e.g., "1000ms" or "1s")
|
|
690
|
+
const match = timecode.match(/^(\d+(?:\.\d+)?)(ms|s)$/);
|
|
691
|
+
if (match) {
|
|
692
|
+
const value = parseFloat(match[1]);
|
|
693
|
+
const unit = match[2];
|
|
694
|
+
thumbnailTimecode = unit === 's' ? value * 1000 : value;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
name,
|
|
706
|
+
tag: element.name, // e.g., "youtube", "s3", etc.
|
|
707
|
+
outputName,
|
|
708
|
+
title: uploadTitle,
|
|
709
|
+
videoId,
|
|
710
|
+
privacy,
|
|
711
|
+
madeForKids,
|
|
712
|
+
tags,
|
|
713
|
+
category,
|
|
714
|
+
language,
|
|
715
|
+
description: description.trim(),
|
|
716
|
+
thumbnailTimecode,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Parses a single <s3> element
|
|
722
|
+
*/
|
|
723
|
+
private parseS3Element(element: Element): Upload | null {
|
|
724
|
+
const attrs = getAttrs(element);
|
|
725
|
+
|
|
726
|
+
const name = attrs.get('name');
|
|
727
|
+
const outputName = attrs.get('data-output-name');
|
|
728
|
+
|
|
729
|
+
if (!name || !outputName) {
|
|
730
|
+
console.warn('S3 upload missing name or data-output-name attribute');
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Parse S3-specific child elements
|
|
735
|
+
let endpoint: string | undefined;
|
|
736
|
+
let region = '';
|
|
737
|
+
let bucket = '';
|
|
738
|
+
let path = '';
|
|
739
|
+
let acl: string | undefined;
|
|
740
|
+
|
|
741
|
+
if ('children' in element && element.children) {
|
|
742
|
+
for (const child of element.children) {
|
|
743
|
+
if (child.type === 'tag') {
|
|
744
|
+
const childElement = child as Element;
|
|
745
|
+
const childAttrs = getAttrs(childElement);
|
|
746
|
+
|
|
747
|
+
switch (childElement.name) {
|
|
748
|
+
case 'endpoint': {
|
|
749
|
+
endpoint = childAttrs.get('name');
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case 'region': {
|
|
753
|
+
region = childAttrs.get('name') || '';
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case 'bucket': {
|
|
757
|
+
bucket = childAttrs.get('name') || '';
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
case 'path': {
|
|
761
|
+
path = childAttrs.get('name') || '';
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case 'acl': {
|
|
765
|
+
acl = childAttrs.get('name');
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Validate required fields
|
|
774
|
+
if (!region || !bucket || !path) {
|
|
775
|
+
console.warn(`S3 upload "${name}" missing required fields (region, bucket, or path)`);
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
name,
|
|
781
|
+
tag: element.name, // "s3"
|
|
782
|
+
outputName,
|
|
783
|
+
privacy: 'private', // Default values for S3 (not used but required by Upload type)
|
|
784
|
+
madeForKids: false,
|
|
785
|
+
tags: [],
|
|
786
|
+
category: '',
|
|
787
|
+
language: '',
|
|
788
|
+
description: '',
|
|
789
|
+
s3: {
|
|
790
|
+
endpoint,
|
|
791
|
+
region,
|
|
792
|
+
bucket,
|
|
793
|
+
path,
|
|
794
|
+
acl,
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Processes the title from the parsed HTML
|
|
801
|
+
*/
|
|
802
|
+
private processTitle(): string {
|
|
803
|
+
const titleElements = this.findTitleElements();
|
|
804
|
+
|
|
805
|
+
if (titleElements.length === 0) {
|
|
806
|
+
return 'Untitled Project';
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Get text content from first title element
|
|
810
|
+
const titleElement = titleElements[0];
|
|
811
|
+
let title = '';
|
|
812
|
+
|
|
813
|
+
if ('children' in titleElement && titleElement.children) {
|
|
814
|
+
for (const textNode of titleElement.children) {
|
|
815
|
+
if (textNode.type === 'text' && 'data' in textNode) {
|
|
816
|
+
title += textNode.data;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return title.trim() || 'Untitled Project';
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Finds all title elements in the HTML (top-level only, not inside uploads)
|
|
826
|
+
*/
|
|
827
|
+
private findTitleElements(): Element[] {
|
|
828
|
+
const results: Element[] = [];
|
|
829
|
+
|
|
830
|
+
const traverse = (node: ASTNode, insideUploads: boolean = false) => {
|
|
831
|
+
if (node.type === 'tag') {
|
|
832
|
+
const element = node as Element;
|
|
833
|
+
|
|
834
|
+
// Find top-level <title> tags (not inside <youtube> elements in <uploads>)
|
|
835
|
+
if (element.name === 'title' && !insideUploads) {
|
|
836
|
+
results.push(element);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Mark that we're inside an uploads section
|
|
840
|
+
const isUploadsSection = element.name === 'uploads';
|
|
841
|
+
|
|
842
|
+
if ('children' in node && node.children) {
|
|
843
|
+
for (const child of node.children) {
|
|
844
|
+
traverse(child, insideUploads || isUploadsSection);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} else if ('children' in node && node.children) {
|
|
848
|
+
for (const child of node.children) {
|
|
849
|
+
traverse(child, insideUploads);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
traverse(this.html.ast);
|
|
855
|
+
return results;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Finds all uploads elements in the HTML
|
|
860
|
+
*/
|
|
861
|
+
private findUploadsElements(): Element[] {
|
|
862
|
+
const results: Element[] = [];
|
|
863
|
+
|
|
864
|
+
const traverse = (node: ASTNode) => {
|
|
865
|
+
if (node.type === 'tag') {
|
|
866
|
+
const element = node as Element;
|
|
867
|
+
|
|
868
|
+
if (element.name === 'uploads') {
|
|
869
|
+
results.push(element);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if ('children' in node && node.children) {
|
|
874
|
+
for (const child of node.children) {
|
|
540
875
|
traverse(child);
|
|
541
876
|
}
|
|
542
877
|
}
|
|
@@ -641,11 +976,11 @@ export class HTMLProjectParser {
|
|
|
641
976
|
|
|
642
977
|
// Get direct sequence children only
|
|
643
978
|
const sequences: Element[] = [];
|
|
644
|
-
if ('
|
|
645
|
-
for (const child of projectElement.
|
|
646
|
-
if ('
|
|
979
|
+
if ('children' in projectElement && projectElement.children) {
|
|
980
|
+
for (const child of projectElement.children) {
|
|
981
|
+
if (child.type === 'tag') {
|
|
647
982
|
const element = child as Element;
|
|
648
|
-
if (element.
|
|
983
|
+
if (element.name === 'sequence') {
|
|
649
984
|
sequences.push(element);
|
|
650
985
|
}
|
|
651
986
|
}
|
|
@@ -660,15 +995,15 @@ export class HTMLProjectParser {
|
|
|
660
995
|
*/
|
|
661
996
|
private findProjectElement(): Element | null {
|
|
662
997
|
const traverse = (node: ASTNode): Element | null => {
|
|
663
|
-
if ('
|
|
998
|
+
if (node.type === 'tag') {
|
|
664
999
|
const element = node as Element;
|
|
665
|
-
if (element.
|
|
1000
|
+
if (element.name === 'project') {
|
|
666
1001
|
return element;
|
|
667
1002
|
}
|
|
668
1003
|
}
|
|
669
1004
|
|
|
670
|
-
if ('
|
|
671
|
-
for (const child of node.
|
|
1005
|
+
if ('children' in node && node.children) {
|
|
1006
|
+
for (const child of node.children) {
|
|
672
1007
|
const result = traverse(child);
|
|
673
1008
|
if (result) return result;
|
|
674
1009
|
}
|
|
@@ -688,23 +1023,23 @@ export class HTMLProjectParser {
|
|
|
688
1023
|
const fragments: Element[] = [];
|
|
689
1024
|
|
|
690
1025
|
const traverse = (node: ASTNode) => {
|
|
691
|
-
if ('
|
|
1026
|
+
if (node.type === 'tag') {
|
|
692
1027
|
const element = node as Element;
|
|
693
|
-
if (element.
|
|
1028
|
+
if (element.name === 'fragment') {
|
|
694
1029
|
fragments.push(element);
|
|
695
1030
|
}
|
|
696
1031
|
}
|
|
697
1032
|
|
|
698
|
-
if ('
|
|
699
|
-
for (const child of node.
|
|
1033
|
+
if ('children' in node && node.children) {
|
|
1034
|
+
for (const child of node.children) {
|
|
700
1035
|
traverse(child);
|
|
701
1036
|
}
|
|
702
1037
|
}
|
|
703
1038
|
};
|
|
704
1039
|
|
|
705
1040
|
// Start traversing from the sequence element's children
|
|
706
|
-
if ('
|
|
707
|
-
for (const child of sequenceElement.
|
|
1041
|
+
if ('children' in sequenceElement && sequenceElement.children) {
|
|
1042
|
+
for (const child of sequenceElement.children) {
|
|
708
1043
|
traverse(child);
|
|
709
1044
|
}
|
|
710
1045
|
}
|
|
@@ -725,7 +1060,7 @@ export class HTMLProjectParser {
|
|
|
725
1060
|
overlayZIndexRight: number;
|
|
726
1061
|
})
|
|
727
1062
|
| null {
|
|
728
|
-
const attrs =
|
|
1063
|
+
const attrs = getAttrs(element);
|
|
729
1064
|
const styles = this.html.css.get(element) || {};
|
|
730
1065
|
|
|
731
1066
|
// 1. Extract fragment ID from id attribute or generate one
|
|
@@ -784,6 +1119,9 @@ export class HTMLProjectParser {
|
|
|
784
1119
|
// 15. Parse filter (for visual filters)
|
|
785
1120
|
const visualFilter = this.parseVisualFilterProperty(styles['filter']);
|
|
786
1121
|
|
|
1122
|
+
// 16. Extract timecode label from data-timecode attribute
|
|
1123
|
+
const timecodeLabel = attrs.get('data-timecode') || undefined;
|
|
1124
|
+
|
|
787
1125
|
return {
|
|
788
1126
|
id,
|
|
789
1127
|
enabled,
|
|
@@ -814,6 +1152,7 @@ export class HTMLProjectParser {
|
|
|
814
1152
|
chromakeyColor: chromakeyData.chromakeyColor,
|
|
815
1153
|
...(visualFilter && { visualFilter }), // Add visualFilter if present
|
|
816
1154
|
...(container && { container }), // Add container if present
|
|
1155
|
+
...(timecodeLabel && { timecodeLabel }), // Add timecode label if present
|
|
817
1156
|
};
|
|
818
1157
|
}
|
|
819
1158
|
|
|
@@ -841,20 +1180,17 @@ export class HTMLProjectParser {
|
|
|
841
1180
|
*/
|
|
842
1181
|
private extractFragmentContainer(element: Element): Container | undefined {
|
|
843
1182
|
// Find first container child
|
|
844
|
-
if (!('
|
|
1183
|
+
if (!('children' in element) || !element.children) {
|
|
845
1184
|
return undefined;
|
|
846
1185
|
}
|
|
847
1186
|
|
|
848
|
-
for (const child of element.
|
|
849
|
-
if ('
|
|
1187
|
+
for (const child of element.children) {
|
|
1188
|
+
if (child.type === 'tag' && child.name === 'container') {
|
|
850
1189
|
const containerElement = child as Element;
|
|
851
1190
|
|
|
852
1191
|
// Get id attribute
|
|
853
|
-
const idAttr = containerElement.attrs.find(
|
|
854
|
-
(attr) => attr.name === 'id',
|
|
855
|
-
);
|
|
856
1192
|
const id =
|
|
857
|
-
|
|
1193
|
+
containerElement.attribs?.id ||
|
|
858
1194
|
`container_${Math.random().toString(36).substring(2, 11)}`;
|
|
859
1195
|
|
|
860
1196
|
// Get innerHTML (serialize all children)
|
|
@@ -877,37 +1213,39 @@ export class HTMLProjectParser {
|
|
|
877
1213
|
let html = '';
|
|
878
1214
|
|
|
879
1215
|
const traverse = (node: ASTNode) => {
|
|
880
|
-
if (
|
|
1216
|
+
if (node.type === 'text') {
|
|
881
1217
|
// Text node
|
|
882
|
-
if ('
|
|
883
|
-
html += node.
|
|
1218
|
+
if ('data' in node && typeof node.data === 'string') {
|
|
1219
|
+
html += node.data;
|
|
884
1220
|
}
|
|
885
|
-
} else if ('
|
|
1221
|
+
} else if (node.type === 'tag') {
|
|
886
1222
|
// Element node
|
|
887
1223
|
const el = node as Element;
|
|
888
|
-
html += `<${el.
|
|
1224
|
+
html += `<${el.name}`;
|
|
889
1225
|
|
|
890
1226
|
// Add attributes
|
|
891
|
-
|
|
892
|
-
|
|
1227
|
+
if (el.attribs) {
|
|
1228
|
+
for (const [name, value] of Object.entries(el.attribs)) {
|
|
1229
|
+
html += ` ${name}="${value}"`;
|
|
1230
|
+
}
|
|
893
1231
|
}
|
|
894
1232
|
|
|
895
1233
|
html += '>';
|
|
896
1234
|
|
|
897
1235
|
// Process children
|
|
898
|
-
if ('
|
|
899
|
-
for (const child of el.
|
|
1236
|
+
if ('children' in el && el.children) {
|
|
1237
|
+
for (const child of el.children) {
|
|
900
1238
|
traverse(child);
|
|
901
1239
|
}
|
|
902
1240
|
}
|
|
903
1241
|
|
|
904
|
-
html += `</${el.
|
|
1242
|
+
html += `</${el.name}>`;
|
|
905
1243
|
}
|
|
906
1244
|
};
|
|
907
1245
|
|
|
908
1246
|
// Serialize all children
|
|
909
|
-
if ('
|
|
910
|
-
for (const child of element.
|
|
1247
|
+
if ('children' in element && element.children) {
|
|
1248
|
+
for (const child of element.children) {
|
|
911
1249
|
traverse(child);
|
|
912
1250
|
}
|
|
913
1251
|
}
|