@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.
Files changed (83) hide show
  1. package/Makefile +17 -4
  2. package/dist/cli/commands/add-assets.d.ts +3 -0
  3. package/dist/cli/commands/add-assets.d.ts.map +1 -0
  4. package/dist/cli/commands/add-assets.js +113 -0
  5. package/dist/cli/commands/add-assets.js.map +1 -0
  6. package/dist/cli/commands/bootstrap.d.ts +3 -0
  7. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  8. package/dist/cli/commands/bootstrap.js +49 -0
  9. package/dist/cli/commands/bootstrap.js.map +1 -0
  10. package/dist/cli/commands/generate.d.ts +3 -0
  11. package/dist/cli/commands/generate.d.ts.map +1 -0
  12. package/dist/cli/commands/generate.js +132 -0
  13. package/dist/cli/commands/generate.js.map +1 -0
  14. package/dist/cli/commands/upload.d.ts +6 -0
  15. package/dist/cli/commands/upload.d.ts.map +1 -0
  16. package/dist/cli/commands/upload.js +67 -0
  17. package/dist/cli/commands/upload.js.map +1 -0
  18. package/dist/cli/s3/s3-upload-strategy.d.ts +18 -0
  19. package/dist/cli/s3/s3-upload-strategy.d.ts.map +1 -0
  20. package/dist/cli/s3/s3-upload-strategy.js +149 -0
  21. package/dist/cli/s3/s3-upload-strategy.js.map +1 -0
  22. package/dist/cli/upload-strategy-factory.d.ts +23 -0
  23. package/dist/cli/upload-strategy-factory.d.ts.map +1 -0
  24. package/dist/cli/upload-strategy-factory.js +49 -0
  25. package/dist/cli/upload-strategy-factory.js.map +1 -0
  26. package/dist/cli/upload-strategy.d.ts +25 -0
  27. package/dist/cli/upload-strategy.d.ts.map +1 -0
  28. package/dist/cli/upload-strategy.js +3 -0
  29. package/dist/cli/upload-strategy.js.map +1 -0
  30. package/dist/cli/youtube/auth-commands.d.ts +3 -0
  31. package/dist/cli/youtube/auth-commands.d.ts.map +1 -0
  32. package/dist/cli/youtube/auth-commands.js +273 -0
  33. package/dist/cli/youtube/auth-commands.js.map +1 -0
  34. package/dist/cli/youtube/cli.d.ts +7 -0
  35. package/dist/cli/youtube/cli.d.ts.map +1 -0
  36. package/dist/cli/youtube/cli.js +13 -0
  37. package/dist/cli/youtube/cli.js.map +1 -0
  38. package/dist/cli/youtube/upload-handler.d.ts +12 -0
  39. package/dist/cli/youtube/upload-handler.d.ts.map +1 -0
  40. package/dist/cli/youtube/upload-handler.js +66 -0
  41. package/dist/cli/youtube/upload-handler.js.map +1 -0
  42. package/dist/cli/youtube/youtube-upload-strategy.d.ts +15 -0
  43. package/dist/cli/youtube/youtube-upload-strategy.d.ts.map +1 -0
  44. package/dist/cli/youtube/youtube-upload-strategy.js +37 -0
  45. package/dist/cli/youtube/youtube-upload-strategy.js.map +1 -0
  46. package/dist/cli.js +12 -281
  47. package/dist/cli.js.map +1 -1
  48. package/dist/html-parser.d.ts +3 -4
  49. package/dist/html-parser.d.ts.map +1 -1
  50. package/dist/html-parser.js +20 -17
  51. package/dist/html-parser.js.map +1 -1
  52. package/dist/html-project-parser.d.ts +24 -0
  53. package/dist/html-project-parser.d.ts.map +1 -1
  54. package/dist/html-project-parser.js +361 -57
  55. package/dist/html-project-parser.js.map +1 -1
  56. package/dist/project.d.ts +15 -2
  57. package/dist/project.d.ts.map +1 -1
  58. package/dist/project.js +57 -1
  59. package/dist/project.js.map +1 -1
  60. package/dist/type.d.ts +26 -4
  61. package/dist/type.d.ts.map +1 -1
  62. package/dist/youtube-uploader.d.ts +40 -0
  63. package/dist/youtube-uploader.d.ts.map +1 -0
  64. package/dist/youtube-uploader.js +227 -0
  65. package/dist/youtube-uploader.js.map +1 -0
  66. package/package.json +6 -2
  67. package/src/cli/commands/add-assets.ts +159 -0
  68. package/src/cli/commands/bootstrap.ts +57 -0
  69. package/src/cli/commands/generate.ts +189 -0
  70. package/src/cli/commands/upload.ts +83 -0
  71. package/src/cli/s3/s3-upload-strategy.ts +194 -0
  72. package/src/cli/upload-strategy-factory.ts +58 -0
  73. package/src/cli/upload-strategy.ts +31 -0
  74. package/src/cli/youtube/auth-commands.ts +312 -0
  75. package/src/cli/youtube/cli.ts +11 -0
  76. package/src/cli/youtube/upload-handler.ts +101 -0
  77. package/src/cli/youtube/youtube-upload-strategy.ts +43 -0
  78. package/src/cli.ts +14 -390
  79. package/src/html-parser.ts +23 -21
  80. package/src/html-project-parser.ts +400 -62
  81. package/src/project.ts +71 -1
  82. package/src/type.ts +30 -4
  83. 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 ('tagName' in node) {
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.tagName === 'asset') {
121
+ if (element.name === 'asset') {
104
122
  results.push(element);
105
123
  }
106
124
  }
107
125
 
108
- if ('childNodes' in node && node.childNodes) {
109
- for (const child of node.childNodes) {
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 = new Map(element.attrs.map((attr) => [attr.name, attr.value]));
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.tagName, relativePath);
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 = new Map(
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 ('tagName' in node) {
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.tagName === 'output') {
470
+ if (element.name === 'output') {
455
471
  results.push(element);
456
472
  }
457
473
  }
458
474
 
459
- if ('childNodes' in node && node.childNodes) {
460
- for (const child of node.childNodes) {
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 ('childNodes' in ffmpegElement && ffmpegElement.childNodes) {
481
- for (const child of ffmpegElement.childNodes) {
482
- if ('tagName' in child) {
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.tagName === 'option') {
485
- const attrs = new Map(
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 ('childNodes' in childElement && childElement.childNodes) {
497
- for (const textNode of childElement.childNodes) {
498
- if ('value' in textNode) {
499
- args += textNode.value;
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 ('tagName' in node) {
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.tagName === 'ffmpeg') {
547
+ if (element.name === 'ffmpeg') {
534
548
  results.push(element);
535
549
  }
536
550
  }
537
551
 
538
- if ('childNodes' in node && node.childNodes) {
539
- for (const child of node.childNodes) {
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 ('childNodes' in projectElement && projectElement.childNodes) {
645
- for (const child of projectElement.childNodes) {
646
- if ('tagName' in child) {
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.tagName === 'sequence') {
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 ('tagName' in node) {
998
+ if (node.type === 'tag') {
664
999
  const element = node as Element;
665
- if (element.tagName === 'project') {
1000
+ if (element.name === 'project') {
666
1001
  return element;
667
1002
  }
668
1003
  }
669
1004
 
670
- if ('childNodes' in node && node.childNodes) {
671
- for (const child of node.childNodes) {
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 ('tagName' in node) {
1026
+ if (node.type === 'tag') {
692
1027
  const element = node as Element;
693
- if (element.tagName === 'fragment') {
1028
+ if (element.name === 'fragment') {
694
1029
  fragments.push(element);
695
1030
  }
696
1031
  }
697
1032
 
698
- if ('childNodes' in node && node.childNodes) {
699
- for (const child of node.childNodes) {
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 ('childNodes' in sequenceElement && sequenceElement.childNodes) {
707
- for (const child of sequenceElement.childNodes) {
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 = new Map(element.attrs.map((attr) => [attr.name, attr.value]));
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 (!('childNodes' in element) || !element.childNodes) {
1183
+ if (!('children' in element) || !element.children) {
845
1184
  return undefined;
846
1185
  }
847
1186
 
848
- for (const child of element.childNodes) {
849
- if ('tagName' in child && child.tagName === 'container') {
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
- idAttr?.value ||
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 ('nodeName' in node && node.nodeName === '#text') {
1216
+ if (node.type === 'text') {
881
1217
  // Text node
882
- if ('value' in node && typeof node.value === 'string') {
883
- html += node.value;
1218
+ if ('data' in node && typeof node.data === 'string') {
1219
+ html += node.data;
884
1220
  }
885
- } else if ('tagName' in node) {
1221
+ } else if (node.type === 'tag') {
886
1222
  // Element node
887
1223
  const el = node as Element;
888
- html += `<${el.tagName}`;
1224
+ html += `<${el.name}`;
889
1225
 
890
1226
  // Add attributes
891
- for (const attr of el.attrs) {
892
- html += ` ${attr.name}="${attr.value}"`;
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 ('childNodes' in el && el.childNodes) {
899
- for (const child of el.childNodes) {
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.tagName}>`;
1242
+ html += `</${el.name}>`;
905
1243
  }
906
1244
  };
907
1245
 
908
1246
  // Serialize all children
909
- if ('childNodes' in element && element.childNodes) {
910
- for (const child of element.childNodes) {
1247
+ if ('children' in element && element.children) {
1248
+ for (const child of element.children) {
911
1249
  traverse(child);
912
1250
  }
913
1251
  }