@gannochenko/staticstripes 0.0.18 → 0.0.20

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.
@@ -7,6 +7,7 @@ import {
7
7
  SequenceDefinition,
8
8
  Fragment,
9
9
  Container,
10
+ App,
10
11
  FFmpegOption,
11
12
  Upload,
12
13
  AIProvider,
@@ -120,6 +121,7 @@ export class HTMLProjectParser {
120
121
  aiProviders,
121
122
  title,
122
123
  date,
124
+ globalTags,
123
125
  cssText,
124
126
  this.projectPath,
125
127
  );
@@ -1609,18 +1611,23 @@ export class HTMLProjectParser {
1609
1611
  // 3. Check enabled flag from display property
1610
1612
  const enabled = this.parseEnabled(styles['display']);
1611
1613
 
1612
- // 4. Extract container if present (first one only)
1614
+ // 4. Extract container or app if present (first one only, mutually exclusive)
1613
1615
  const container = this.extractFragmentContainer(element);
1616
+ const app = container ? undefined : this.extractFragmentApp(element);
1614
1617
 
1615
1618
  // 5. Parse trimLeft from -trim-start property
1616
1619
  const trimLeft = this.parseTrimStart(styles['-trim-start']);
1617
1620
 
1621
+ // 5b. Parse trimRight from -trim-end property
1622
+ const trimRight = this.parseTrimEnd(styles['-trim-end']);
1623
+
1618
1624
  // 6. Parse duration from -duration property
1619
1625
  const duration = this.parseDurationProperty(
1620
1626
  styles['-duration'],
1621
1627
  assetName,
1622
1628
  assets,
1623
1629
  trimLeft,
1630
+ trimRight,
1624
1631
  );
1625
1632
 
1626
1633
  // 7. Parse -offset-start for overlayLeft (can be number or expression)
@@ -1687,6 +1694,7 @@ export class HTMLProjectParser {
1687
1694
  chromakeyColor: chromakeyData.chromakeyColor,
1688
1695
  ...(visualFilter && { visualFilter }), // Add visualFilter if present
1689
1696
  ...(container && { container }), // Add container if present
1697
+ ...(app && { app }), // Add app if present
1690
1698
  ...(timecodeLabel && { timecodeLabel }), // Add timecode label if present
1691
1699
  };
1692
1700
  }
@@ -1741,6 +1749,49 @@ export class HTMLProjectParser {
1741
1749
  return undefined;
1742
1750
  }
1743
1751
 
1752
+ /**
1753
+ * Extracts the first <app> child from a fragment element.
1754
+ * The src attribute points to the app's dst directory (relative to project).
1755
+ * The data-parameters attribute is parsed as JSON and merged into query params.
1756
+ */
1757
+ private extractFragmentApp(element: Element): App | undefined {
1758
+ if (!('children' in element) || !element.children) {
1759
+ return undefined;
1760
+ }
1761
+
1762
+ for (const child of element.children) {
1763
+ if (child.type === 'tag' && child.name === 'app') {
1764
+ const appElement = child as Element;
1765
+
1766
+ const id =
1767
+ appElement.attribs?.id ||
1768
+ `app_${Math.random().toString(36).substring(2, 11)}`;
1769
+
1770
+ const src = appElement.attribs?.src ?? '';
1771
+
1772
+ let parameters: Record<string, string> = {};
1773
+ const dataParameters = appElement.attribs?.['data-parameters'];
1774
+ if (dataParameters) {
1775
+ try {
1776
+ const parsed = JSON.parse(dataParameters);
1777
+ // Convert all values to strings for query parameters
1778
+ for (const [key, value] of Object.entries(parsed)) {
1779
+ parameters[key] = String(value);
1780
+ }
1781
+ } catch {
1782
+ console.warn(
1783
+ `Warning: invalid JSON in data-parameters for app "${id}": ${dataParameters}`,
1784
+ );
1785
+ }
1786
+ }
1787
+
1788
+ return { id, src, parameters };
1789
+ }
1790
+ }
1791
+
1792
+ return undefined;
1793
+ }
1794
+
1744
1795
  /**
1745
1796
  * Serializes an element's children to HTML string
1746
1797
  */
@@ -1844,6 +1895,20 @@ export class HTMLProjectParser {
1844
1895
  return Math.max(0, value);
1845
1896
  }
1846
1897
 
1898
+ /**
1899
+ * Parses -trim-end property into trimRight
1900
+ * Cannot be negative
1901
+ */
1902
+ private parseTrimEnd(trimEnd: string | undefined): number {
1903
+ if (!trimEnd) {
1904
+ return 0;
1905
+ }
1906
+
1907
+ const value = this.parseMilliseconds(trimEnd);
1908
+ // Ensure non-negative as per spec
1909
+ return Math.max(0, value);
1910
+ }
1911
+
1847
1912
  /**
1848
1913
  * Parses the -duration CSS property
1849
1914
  * Can be: "auto", percentage (e.g. "100%", "50%"), or time value (e.g. "5000ms", "5s")
@@ -1853,14 +1918,15 @@ export class HTMLProjectParser {
1853
1918
  assetName: string,
1854
1919
  assets: Map<string, Asset>,
1855
1920
  trimLeft: number,
1921
+ trimRight: number,
1856
1922
  ): number {
1857
1923
  if (!duration || duration.trim() === 'auto') {
1858
- // Auto: use asset duration minus trim-start
1924
+ // Auto: use asset duration minus trim-start and trim-end
1859
1925
  const asset = assets.get(assetName);
1860
1926
  if (!asset) {
1861
1927
  return 0;
1862
1928
  }
1863
- return Math.max(0, asset.duration - trimLeft);
1929
+ return Math.max(0, asset.duration - trimLeft - trimRight);
1864
1930
  }
1865
1931
 
1866
1932
  // Handle percentage (e.g., "100%", "50%")
package/src/project.ts CHANGED
@@ -12,6 +12,7 @@ import { Sequence } from './sequence';
12
12
  import { FilterBuffer } from './stream';
13
13
  import { ExpressionContext, FragmentData } from './expression-parser';
14
14
  import { renderContainers } from './container-renderer';
15
+ import { renderApps } from './app-renderer';
15
16
  import { dirname } from 'path';
16
17
 
17
18
  export class Project {
@@ -27,6 +28,7 @@ export class Project {
27
28
  private aiProviders: Map<string, AIProvider>,
28
29
  private title: string,
29
30
  private date: string | undefined,
31
+ private tags: string[],
30
32
  private cssText: string,
31
33
  private projectPath: string,
32
34
  ) {
@@ -146,6 +148,10 @@ export class Project {
146
148
  return this.date;
147
149
  }
148
150
 
151
+ public getTags(): string[] {
152
+ return this.tags;
153
+ }
154
+
149
155
  public getCssText(): string {
150
156
  return this.cssText;
151
157
  }
@@ -212,6 +218,72 @@ export class Project {
212
218
  return this.assetManager.getAudioInputLabelByAssetName(name);
213
219
  }
214
220
 
221
+ /**
222
+ * Renders all apps and creates virtual assets for them.
223
+ * Apps must dispatch "sts-render-complete" on document when ready,
224
+ * or rendering will fail after a 5-second timeout.
225
+ */
226
+ public async renderApps(
227
+ outputName: string,
228
+ activeCacheKeys?: Set<string>,
229
+ ): Promise<void> {
230
+ const output = this.getOutput(outputName);
231
+ if (!output) {
232
+ throw new Error(`Output "${outputName}" not found`);
233
+ }
234
+
235
+ const fragmentsWithApps = this.sequencesDefinitions.flatMap((seq) =>
236
+ seq.fragments.filter((frag) => frag.app),
237
+ );
238
+
239
+ if (fragmentsWithApps.length === 0) {
240
+ return;
241
+ }
242
+
243
+ console.log('\n=== Rendering Apps ===\n');
244
+
245
+ const apps = fragmentsWithApps.map((frag) => frag.app!);
246
+ const projectDir = dirname(this.projectPath);
247
+
248
+ const results = await renderApps(
249
+ apps,
250
+ output.resolution.width,
251
+ output.resolution.height,
252
+ projectDir,
253
+ outputName,
254
+ this.title,
255
+ this.date,
256
+ this.tags,
257
+ activeCacheKeys,
258
+ );
259
+
260
+ // Create virtual assets and update fragment assetNames
261
+ for (const result of results) {
262
+ const virtualAssetName = result.app.id;
263
+
264
+ const virtualAsset = {
265
+ name: virtualAssetName,
266
+ path: result.screenshotPath,
267
+ type: 'image' as const,
268
+ duration: 0,
269
+ width: output.resolution.width,
270
+ height: output.resolution.height,
271
+ rotation: 0,
272
+ hasVideo: true,
273
+ hasAudio: false,
274
+ };
275
+
276
+ this.assetManager.addVirtualAsset(virtualAsset);
277
+
278
+ const fragment = fragmentsWithApps.find(
279
+ (frag) => frag.app?.id === result.app.id,
280
+ );
281
+ if (fragment) {
282
+ fragment.assetName = virtualAssetName;
283
+ }
284
+ }
285
+ }
286
+
215
287
  /**
216
288
  * Renders all containers and creates virtual assets for them
217
289
  */
package/src/type.ts CHANGED
@@ -13,6 +13,12 @@ export type Container = {
13
13
  htmlContent: string;
14
14
  };
15
15
 
16
+ export type App = {
17
+ id: string;
18
+ src: string; // path to the app's dst directory (relative to project)
19
+ parameters: Record<string, string>; // extra params from data-parameters
20
+ };
21
+
16
22
  export type ParsedHtml = {
17
23
  ast: Document;
18
24
  css: Map<Element, CSSProperties>;
@@ -42,7 +48,7 @@ export type Fragment = {
42
48
  enabled: boolean;
43
49
  assetName: string;
44
50
  duration: number; // calculated, in seconds (can come from CSS or from the asset's duration)
45
- trimLeft: number; // in seconds
51
+ trimLeft: number; // in seconds (skip first N seconds of asset via -trim-start)
46
52
  overlayLeft: number | CompiledExpression; // amount of seconds to overlay with the previous fragment (normalized from margin-left + prev margin-right)
47
53
  overlayZIndex: number;
48
54
  transitionIn: string; // how to transition into the fragment
@@ -61,6 +67,7 @@ export type Fragment = {
61
67
  chromakeyColor: string;
62
68
  visualFilter?: string; // Optional visual filter (e.g., 'instagram-nashville')
63
69
  container?: Container; // Optional container attached to this fragment
70
+ app?: App; // Optional app attached to this fragment
64
71
  timecodeLabel?: string; // Optional label for timecode (from data-timecode attribute)
65
72
  };
66
73