Skip to content

Commit 0b9ebbf

Browse files
committed
feat: add ClipSet to schema
There are a few changes to the current ClipSet workaround in cp-content-pipeline: - dataLayout -> layoutWidth (for consistency with theother nodes) - changes to how the Body for transcripts is represented (pending further discussion with CP) This commit also updates the from-bodyxml transformerfor clipset
1 parent b71779d commit 0b9ebbf

File tree

6 files changed

+545
-6
lines changed

6 files changed

+545
-6
lines changed

README.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type BodyBlock =
105105
| Tweet
106106
| Video
107107
| YoutubeVideo
108+
| ClipSet
108109
| Text
109110
```
110111
@@ -550,8 +551,6 @@ interface Video extends Node {
550551

551552
The `title` can be obtained by fetching the Video from the content API.
552553

553-
TODO: Figure out how Clips work, how they are different?
554-
555554
### `YoutubeVideo`
556555

557556
```ts
@@ -561,8 +560,76 @@ interface YoutubeVideo extends Node {
561560
}
562561
```
563562

563+
564564
**YoutubeVideo** represents a video referenced by a Youtube URL.
565565

566+
### `ClipSet`
567+
568+
```ts
569+
interface ClipSet extends Node {
570+
type: "clip-set"
571+
id: string
572+
autoplay: boolean
573+
loop: boolean
574+
muted: boolean
575+
layoutWidth: ClipSetLayoutWidth
576+
external noAudio: boolean
577+
external caption: string
578+
external credits: string
579+
external description: string
580+
external displayTitle: string
581+
external systemTitle: string
582+
external source: string
583+
external contentWarning: string[]
584+
external publishedDate: string
585+
external subtitle: string
586+
external clips: Clip[]
587+
external accessibility: ClipAccessibility
588+
}
589+
```
590+
591+
```ts
592+
type Clip = {
593+
id: string
594+
format: 'standard-inline' | 'mobile'
595+
dataSource: ClipSource[]
596+
poster: string
597+
}
598+
```
599+
600+
```ts
601+
type ClipSource = {
602+
audioCodec: string
603+
binaryUrl: string
604+
duration: number
605+
mediaType: string
606+
pixelHeight: number
607+
pixelWidth: number
608+
videoCodec: string
609+
}
610+
```
611+
612+
```ts
613+
type ClipCaption = {
614+
mediaType?: string
615+
url?: string
616+
}
617+
```
618+
```ts
619+
type ClipAccessibility = {
620+
captions?: ClipCaption[]
621+
transcript?: Body
622+
}
623+
```
624+
625+
```ts
626+
type ClipSetLayoutWidth = Extract<LayoutWidth, "in-line" | "mid-grid" | "full-grid">
627+
```
628+
629+
**ClipSet** represents a short piece of possibly-looping video content for an article.
630+
631+
The external fields are derived from the separately published [ClipSet](https://api.ft.com/schemas/clip-set.json) and [Clip](https://api.ft.com/schemas/clip.json) objects in the Content API.
632+
566633
### `ScrollyBlock`
567634
568635
```ts

content-tree.d.ts

Lines changed: 168 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export declare namespace ContentTree {
2-
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | Text;
2+
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | ClipSet | Text;
33
type LayoutWidth = "auto" | "in-line" | "inset-left" | "inset-right" | "full-bleed" | "full-grid" | "mid-grid" | "full-width";
44
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link;
55
interface Node {
@@ -175,6 +175,50 @@ export declare namespace ContentTree {
175175
type: "youtube-video";
176176
url: string;
177177
}
178+
interface ClipSet extends Node {
179+
type: "clip-set";
180+
id: string;
181+
autoplay: boolean;
182+
loop: boolean;
183+
muted: boolean;
184+
layoutWidth: ClipSetLayoutWidth;
185+
noAudio: boolean;
186+
caption: string;
187+
credits: string;
188+
description: string;
189+
displayTitle: string;
190+
systemTitle: string;
191+
source: string;
192+
contentWarning: string[];
193+
publishedDate: string;
194+
subtitle: string;
195+
clips: Clip[];
196+
accessibility: ClipAccessibility;
197+
}
198+
type Clip = {
199+
id: string;
200+
format: 'standard-inline' | 'mobile';
201+
dataSource: ClipSource[];
202+
poster: string;
203+
};
204+
type ClipSource = {
205+
audioCodec: string;
206+
binaryUrl: string;
207+
duration: number;
208+
mediaType: string;
209+
pixelHeight: number;
210+
pixelWidth: number;
211+
videoCodec: string;
212+
};
213+
type ClipCaption = {
214+
mediaType?: string;
215+
url?: string;
216+
};
217+
type ClipAccessibility = {
218+
captions?: ClipCaption[];
219+
transcript?: Body;
220+
};
221+
type ClipSetLayoutWidth = Extract<LayoutWidth, "in-line" | "mid-grid" | "full-grid">;
178222
interface ScrollyBlock extends Parent {
179223
type: "scrolly-block";
180224
theme: "sans" | "serif";
@@ -279,7 +323,7 @@ export declare namespace ContentTree {
279323
attributes: CustomCodeComponentAttributes;
280324
}
281325
namespace full {
282-
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | Text;
326+
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | ClipSet | Text;
283327
type LayoutWidth = "auto" | "in-line" | "inset-left" | "inset-right" | "full-bleed" | "full-grid" | "mid-grid" | "full-width";
284328
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link;
285329
interface Node {
@@ -455,6 +499,50 @@ export declare namespace ContentTree {
455499
type: "youtube-video";
456500
url: string;
457501
}
502+
interface ClipSet extends Node {
503+
type: "clip-set";
504+
id: string;
505+
autoplay: boolean;
506+
loop: boolean;
507+
muted: boolean;
508+
layoutWidth: ClipSetLayoutWidth;
509+
noAudio: boolean;
510+
caption: string;
511+
credits: string;
512+
description: string;
513+
displayTitle: string;
514+
systemTitle: string;
515+
source: string;
516+
contentWarning: string[];
517+
publishedDate: string;
518+
subtitle: string;
519+
clips: Clip[];
520+
accessibility: ClipAccessibility;
521+
}
522+
type Clip = {
523+
id: string;
524+
format: 'standard-inline' | 'mobile';
525+
dataSource: ClipSource[];
526+
poster: string;
527+
};
528+
type ClipSource = {
529+
audioCodec: string;
530+
binaryUrl: string;
531+
duration: number;
532+
mediaType: string;
533+
pixelHeight: number;
534+
pixelWidth: number;
535+
videoCodec: string;
536+
};
537+
type ClipCaption = {
538+
mediaType?: string;
539+
url?: string;
540+
};
541+
type ClipAccessibility = {
542+
captions?: ClipCaption[];
543+
transcript?: Body;
544+
};
545+
type ClipSetLayoutWidth = Extract<LayoutWidth, "in-line" | "mid-grid" | "full-grid">;
458546
interface ScrollyBlock extends Parent {
459547
type: "scrolly-block";
460548
theme: "sans" | "serif";
@@ -560,7 +648,7 @@ export declare namespace ContentTree {
560648
}
561649
}
562650
namespace transit {
563-
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | Text;
651+
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | ClipSet | Text;
564652
type LayoutWidth = "auto" | "in-line" | "inset-left" | "inset-right" | "full-bleed" | "full-grid" | "mid-grid" | "full-width";
565653
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link;
566654
interface Node {
@@ -731,6 +819,38 @@ export declare namespace ContentTree {
731819
type: "youtube-video";
732820
url: string;
733821
}
822+
interface ClipSet extends Node {
823+
type: "clip-set";
824+
id: string;
825+
autoplay: boolean;
826+
loop: boolean;
827+
muted: boolean;
828+
layoutWidth: ClipSetLayoutWidth;
829+
}
830+
type Clip = {
831+
id: string;
832+
format: 'standard-inline' | 'mobile';
833+
dataSource: ClipSource[];
834+
poster: string;
835+
};
836+
type ClipSource = {
837+
audioCodec: string;
838+
binaryUrl: string;
839+
duration: number;
840+
mediaType: string;
841+
pixelHeight: number;
842+
pixelWidth: number;
843+
videoCodec: string;
844+
};
845+
type ClipCaption = {
846+
mediaType?: string;
847+
url?: string;
848+
};
849+
type ClipAccessibility = {
850+
captions?: ClipCaption[];
851+
transcript?: Body;
852+
};
853+
type ClipSetLayoutWidth = Extract<LayoutWidth, "in-line" | "mid-grid" | "full-grid">;
734854
interface ScrollyBlock extends Parent {
735855
type: "scrolly-block";
736856
theme: "sans" | "serif";
@@ -826,7 +946,7 @@ export declare namespace ContentTree {
826946
}
827947
}
828948
namespace loose {
829-
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | Text;
949+
type BodyBlock = Paragraph | Heading | ImageSet | Flourish | BigNumber | CustomCodeComponent | Layout | List | Blockquote | Pullquote | ScrollyBlock | ThematicBreak | Table | Recommended | Tweet | Video | YoutubeVideo | ClipSet | Text;
830950
type LayoutWidth = "auto" | "in-line" | "inset-left" | "inset-right" | "full-bleed" | "full-grid" | "mid-grid" | "full-width";
831951
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link;
832952
interface Node {
@@ -1002,6 +1122,50 @@ export declare namespace ContentTree {
10021122
type: "youtube-video";
10031123
url: string;
10041124
}
1125+
interface ClipSet extends Node {
1126+
type: "clip-set";
1127+
id: string;
1128+
autoplay: boolean;
1129+
loop: boolean;
1130+
muted: boolean;
1131+
layoutWidth: ClipSetLayoutWidth;
1132+
noAudio?: boolean;
1133+
caption?: string;
1134+
credits?: string;
1135+
description?: string;
1136+
displayTitle?: string;
1137+
systemTitle?: string;
1138+
source?: string;
1139+
contentWarning?: string[];
1140+
publishedDate?: string;
1141+
subtitle?: string;
1142+
clips?: Clip[];
1143+
accessibility?: ClipAccessibility;
1144+
}
1145+
type Clip = {
1146+
id: string;
1147+
format: 'standard-inline' | 'mobile';
1148+
dataSource: ClipSource[];
1149+
poster: string;
1150+
};
1151+
type ClipSource = {
1152+
audioCodec: string;
1153+
binaryUrl: string;
1154+
duration: number;
1155+
mediaType: string;
1156+
pixelHeight: number;
1157+
pixelWidth: number;
1158+
videoCodec: string;
1159+
};
1160+
type ClipCaption = {
1161+
mediaType?: string;
1162+
url?: string;
1163+
};
1164+
type ClipAccessibility = {
1165+
captions?: ClipCaption[];
1166+
transcript?: Body;
1167+
};
1168+
type ClipSetLayoutWidth = Extract<LayoutWidth, "in-line" | "mid-grid" | "full-grid">;
10051169
interface ScrollyBlock extends Parent {
10061170
type: "scrolly-block";
10071171
theme: "sans" | "serif";

libraries/from-bodyxml/index.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let ContentType = {
88
content: "http://www.ft.com/ontology/content/Content",
99
article: "http://www.ft.com/ontology/content/Article",
1010
customCodeComponent: "http://www.ft.com/ontology/content/CustomCodeComponent",
11+
clipSet: "http://www.ft.com/ontology/content/ClipSet",
1112
};
1213

1314
/**
@@ -32,6 +33,24 @@ function toValidLayoutWidth(layoutWidth) {
3233
return "full-width";
3334
}
3435
}
36+
37+
/**
38+
* @param {string} layoutWidth
39+
* @returns {ContentTree.ClipSet["layoutWidth"]}
40+
*/
41+
function toValidClipLayoutWidth(layoutWidth) {
42+
if (
43+
[
44+
"in-line",
45+
"full-grid",
46+
"mid-grid",
47+
].includes(layoutWidth)
48+
) {
49+
return /** @type {ContentTree.ClipSet["layoutWidth"]} */ (layoutWidth);
50+
} else {
51+
return "in-line";
52+
}
53+
}
3554
/**
3655
* @typedef {import("unist").Parent} UParent
3756
* @typedef {import("unist").Node} UNode
@@ -320,6 +339,24 @@ export let defaultTransformers = {
320339
children: null,
321340
};
322341
},
342+
/**
343+
* @type {Transformer<ContentTree.transit.ClipSet>}
344+
*/
345+
[ContentType.clipSet](clip) {
346+
const id = clip.attributes.url ?? "";
347+
const uuid = id.split("/").pop();
348+
return {
349+
type: "clip-set",
350+
id: uuid ?? "",
351+
layoutWidth: toValidClipLayoutWidth(
352+
clip.attributes["data-layout-width"] || ""
353+
),
354+
autoplay: clip.attributes?.autoplay === "true",
355+
loop: clip.attributes?.loop === "true",
356+
muted: clip.attributes?.muted === "true",
357+
children: null,
358+
};
359+
},
323360
/**
324361
* @type {Transformer<ContentTree.transit.Recommended>}
325362
*/

0 commit comments

Comments
 (0)