@financial-times/content-tree 0.1.0
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/LICENSE +8 -0
- package/README.md +219 -0
- package/SPEC.md +867 -0
- package/content-tree.d.ts +1461 -0
- package/package.json +53 -0
- package/schemas/body-tree.schema.json +1523 -0
- package/schemas/content-tree.schema.json +2305 -0
- package/schemas/transit-tree.schema.json +1541 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
# Content Tree Spec
|
|
2
|
+
|
|
3
|
+
## Abstract Types
|
|
4
|
+
|
|
5
|
+
These abstract helper types define special types a [Parent](#parent) can use as
|
|
6
|
+
[children][term-child].
|
|
7
|
+
|
|
8
|
+
### `LayoutWidth`
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
type LayoutWidth =
|
|
12
|
+
| "auto"
|
|
13
|
+
| "in-line"
|
|
14
|
+
| "inset-left"
|
|
15
|
+
| "inset-right"
|
|
16
|
+
| "full-bleed"
|
|
17
|
+
| "full-grid"
|
|
18
|
+
| "mid-grid"
|
|
19
|
+
| "full-width"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`LayoutWidth` defines how the component should be presented in the article page according to the column layout system.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Core Nodes
|
|
26
|
+
|
|
27
|
+
### `Node`
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
interface Node {
|
|
31
|
+
type: string
|
|
32
|
+
data?: any
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The abstract node. The data field is for internal implementation information and
|
|
37
|
+
will never be defined in the content-tree spec.
|
|
38
|
+
|
|
39
|
+
### `Parent`
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
interface Parent extends Node {
|
|
43
|
+
children: Node[]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Parent** (**[UnistParent][term-parent]**) represents a node in content-tree
|
|
48
|
+
containing other nodes (said to be _[children][term-child]_).
|
|
49
|
+
|
|
50
|
+
Its content is limited to only other content-tree content.
|
|
51
|
+
|
|
52
|
+
### `Root`
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
interface Root extends Node {
|
|
56
|
+
type: "root"
|
|
57
|
+
body: Body
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Root** (**[Parent][term-parent]**) represents the root of a content-tree.
|
|
62
|
+
|
|
63
|
+
**Root** can be used as the _[root][term-root]_ of a _[tree][term-tree]_.
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Containers
|
|
67
|
+
|
|
68
|
+
A Container is a node which houses a distinct block of editorial content. These will typically have children containing any number of component nodes. They can be published and validated as independent fields in the Content API.
|
|
69
|
+
|
|
70
|
+
Examples: `body`, `topper`
|
|
71
|
+
|
|
72
|
+
### `Body`
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
interface Body extends Parent {
|
|
76
|
+
type: "body"
|
|
77
|
+
version: number
|
|
78
|
+
children: BodyBlock[]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
#### BodyBlock
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
type BodyBlock =
|
|
85
|
+
| FormattingBlock
|
|
86
|
+
| StoryBlock
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`BodyBlock` nodes are the only things that are valid as the top level of a `Body`.
|
|
90
|
+
|
|
91
|
+
**Body** (**[Parent][term-parent]**) represents the body of an article.
|
|
92
|
+
|
|
93
|
+
(note: `bodyTree` is just this part)
|
|
94
|
+
|
|
95
|
+
## Formatting Blocks
|
|
96
|
+
|
|
97
|
+
### FormattingBlock
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
type FormattingBlock =
|
|
101
|
+
| Paragraph
|
|
102
|
+
| Heading
|
|
103
|
+
| List
|
|
104
|
+
| Blockquote
|
|
105
|
+
| ThematicBreak
|
|
106
|
+
| Text
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`FormattingBlock` nodes contains only text-structured blocks used for formatting textual content.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### `Text`
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
interface Text extends Node {
|
|
116
|
+
type: "text"
|
|
117
|
+
value: string
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
### `Phrasing`
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link | FindOutMoreLink
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A phrasing node cannot have ancestor of the same type.
|
|
129
|
+
|
|
130
|
+
i.e. a Strong will never be inside another Strong, or inside any other node that
|
|
131
|
+
is inside a Strong.
|
|
132
|
+
|
|
133
|
+
**Text** (**[Literal][term-literal]**) represents text.
|
|
134
|
+
|
|
135
|
+
### `Break`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
interface Break extends Node {
|
|
139
|
+
type: "break"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Break** Node represents a break in the text, such as in a poem.
|
|
144
|
+
|
|
145
|
+
_Non-normative note: this would normally be represented by a `<br>` in the
|
|
146
|
+
html._
|
|
147
|
+
|
|
148
|
+
### `ThematicBreak`
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
interface ThematicBreak extends Node {
|
|
152
|
+
type: "thematic-break"
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**ThematicBreak** Node represents a break in the text, such as in a shift of
|
|
157
|
+
topic within a section.
|
|
158
|
+
|
|
159
|
+
_Non-normative note: this would be represented by an `<hr>` in the html._
|
|
160
|
+
|
|
161
|
+
### `Paragraph`
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
interface Paragraph extends Parent {
|
|
165
|
+
type: "paragraph"
|
|
166
|
+
children: Phrasing[]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Paragraph represents a unit of text.
|
|
171
|
+
|
|
172
|
+
### `Heading`
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface Heading extends Parent {
|
|
176
|
+
type: "heading"
|
|
177
|
+
children: Text[]
|
|
178
|
+
level: "chapter" | "subheading" | "label"
|
|
179
|
+
fragmentIdentifier?: string
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Heading** represents a unit of text that marks the beginning of an article
|
|
184
|
+
section.
|
|
185
|
+
|
|
186
|
+
### `Strong`
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
interface Strong extends Parent {
|
|
190
|
+
type: "strong"
|
|
191
|
+
children: Phrasing[]
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Strong** represents contents with strong importance, seriousness or urgency.
|
|
196
|
+
|
|
197
|
+
### `Emphasis`
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
interface Emphasis extends Parent {
|
|
201
|
+
type: "emphasis"
|
|
202
|
+
children: Phrasing[]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Emphasis** represents stressed emphasis of its contents.
|
|
207
|
+
|
|
208
|
+
### `Strikethrough`
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
interface Strikethrough extends Parent {
|
|
212
|
+
type: "strikethrough"
|
|
213
|
+
children: Phrasing[]
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Strikethrough** represents a piece of text that has been stricken.
|
|
218
|
+
|
|
219
|
+
### `Link`
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
interface Link extends Parent {
|
|
223
|
+
type: "link"
|
|
224
|
+
url: string
|
|
225
|
+
title: string
|
|
226
|
+
children: Phrasing[]
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Link** represents a hyperlink.
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
### `FindOutMoreLink`
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
interface FindOutMoreLink extends Parent {
|
|
237
|
+
type: "find-out-more-link"
|
|
238
|
+
url: string
|
|
239
|
+
title: string
|
|
240
|
+
children: Phrasing[]
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**FindOutMoreLink** represents a type of link for onward journey.
|
|
245
|
+
|
|
246
|
+
### `List`
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
interface List extends Parent {
|
|
250
|
+
type: "list"
|
|
251
|
+
ordered: boolean
|
|
252
|
+
children: ListItem[]
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**List** represents a list of items.
|
|
257
|
+
|
|
258
|
+
### `ListItem`
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
interface ListItem extends Parent {
|
|
262
|
+
type: "list-item"
|
|
263
|
+
children: (Paragraph | Phrasing)[]
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `Blockquote`
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
interface Blockquote extends Parent {
|
|
271
|
+
type: "blockquote"
|
|
272
|
+
children: (Paragraph | Phrasing)[]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Blockquote** represents a quotation.
|
|
277
|
+
|
|
278
|
+
## StoryBlocks
|
|
279
|
+
|
|
280
|
+
### `StoryBlock`
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
type StoryBlock =
|
|
284
|
+
| ImageSet
|
|
285
|
+
| Flourish
|
|
286
|
+
| BigNumber
|
|
287
|
+
| CustomCodeComponent
|
|
288
|
+
| Layout
|
|
289
|
+
| Pullquote
|
|
290
|
+
| ScrollyBlock
|
|
291
|
+
| Table
|
|
292
|
+
| Recommended
|
|
293
|
+
| RecommendedList
|
|
294
|
+
| Tweet
|
|
295
|
+
| Video
|
|
296
|
+
| YoutubeVideo
|
|
297
|
+
| Timeline
|
|
298
|
+
| ImagePair
|
|
299
|
+
| InNumbers
|
|
300
|
+
| Definition
|
|
301
|
+
| InfoBox
|
|
302
|
+
| InfoPair
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`StoryBlock` nodes are things that can be inserted into an article body.
|
|
306
|
+
|
|
307
|
+
### `Pullquote`
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
interface Pullquote extends Node {
|
|
311
|
+
type: "pullquote"
|
|
312
|
+
text: string
|
|
313
|
+
source?: string
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Pullquote** represents a brief quotation taken from the main text of an
|
|
318
|
+
article.
|
|
319
|
+
|
|
320
|
+
_non normative note:_ the reason this is string properties and not children is
|
|
321
|
+
that it is more confusing if a pullquote falls back to text than if it
|
|
322
|
+
doesn't. The text is taken from elsewhere in the article.
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
### `ImageSet`
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
interface ImageSet extends Node {
|
|
329
|
+
type: "image-set"
|
|
330
|
+
id: string
|
|
331
|
+
external picture: ImageSetPicture
|
|
332
|
+
fragmentIdentifier?: string
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Image types
|
|
337
|
+
|
|
338
|
+
##### `ImageSetPicture`
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
type ImageSetPicture = {
|
|
342
|
+
layoutWidth: string
|
|
343
|
+
imageType: "image" | "graphic"
|
|
344
|
+
alt: string
|
|
345
|
+
caption: string
|
|
346
|
+
credit: string
|
|
347
|
+
images: Image[]
|
|
348
|
+
fallbackImage: Image
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`ImageSetPicture` defines the data associated with an [ImageSet](#ImageSet)
|
|
353
|
+
|
|
354
|
+
##### `Image`
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
type Image = {
|
|
358
|
+
id: string
|
|
359
|
+
width: number
|
|
360
|
+
height: number
|
|
361
|
+
format:
|
|
362
|
+
| "desktop"
|
|
363
|
+
| "mobile"
|
|
364
|
+
| "square"
|
|
365
|
+
| "square-ftedit"
|
|
366
|
+
| "standard"
|
|
367
|
+
| "wide"
|
|
368
|
+
| "standard-inline"
|
|
369
|
+
url: string
|
|
370
|
+
sourceSet?: ImageSource[]
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`Image` defines a single use-case of a Picture[#ImageSetPicture].
|
|
375
|
+
|
|
376
|
+
### `ImageSource`
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
type ImageSource = {
|
|
380
|
+
url: string
|
|
381
|
+
width: number
|
|
382
|
+
dpr: number
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**ImageSource** defines a single resource for an [image](#image).
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
### `Recommended`
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
interface Recommended extends Node {
|
|
394
|
+
type: "recommended"
|
|
395
|
+
id: string
|
|
396
|
+
heading?: string
|
|
397
|
+
teaserTitleOverride?: string
|
|
398
|
+
external teaser: Teaser
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- Recommended represents a reference to an FT content that has been recommended
|
|
403
|
+
by editorial.
|
|
404
|
+
- The `heading`, when present, is used where the purpose of the link is more
|
|
405
|
+
specific than being "Recommended" (an example might be "In depth")
|
|
406
|
+
- The `teaserTitleOverride`, when present, is used in place of the content title
|
|
407
|
+
of the link.
|
|
408
|
+
|
|
409
|
+
_non normative note:_ historically, recommended links used to be a list of up to
|
|
410
|
+
three content items. Testing later showed that having one more prominent link
|
|
411
|
+
was more engaging. Only use `RecommendedList` if you explicitly need to display multiple links.
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
### `RecommendedList`
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
interface RecommendedList extends Node {
|
|
419
|
+
type: "recommended-list";
|
|
420
|
+
heading?: string;
|
|
421
|
+
children: Recommended[];
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
- RecommendedList represents a collection of Recommended items selected by editorial.
|
|
426
|
+
- The `heading`, when present, is used where the purpose of the link is more
|
|
427
|
+
specific than being "Related Content"
|
|
428
|
+
|
|
429
|
+
#### Teaser types
|
|
430
|
+
|
|
431
|
+
These types were extracted from x-dash's
|
|
432
|
+
[x-teaser](https://github.com/Financial-Times/x-dash/blob/3408c268/components/x-teaser/Props.d.ts).
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
type TeaserConcept = {
|
|
436
|
+
apiUrl: string
|
|
437
|
+
directType: string
|
|
438
|
+
id: string
|
|
439
|
+
predicate: string
|
|
440
|
+
prefLabel: string
|
|
441
|
+
type: string
|
|
442
|
+
types: string[]
|
|
443
|
+
url: string
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
type Teaser = {
|
|
447
|
+
id: string
|
|
448
|
+
url: string
|
|
449
|
+
type:
|
|
450
|
+
| "article"
|
|
451
|
+
| "video"
|
|
452
|
+
| "podcast"
|
|
453
|
+
| "audio"
|
|
454
|
+
| "package"
|
|
455
|
+
| "liveblog"
|
|
456
|
+
| "promoted-content"
|
|
457
|
+
| "paid-post"
|
|
458
|
+
title: string
|
|
459
|
+
publishedDate: string
|
|
460
|
+
firstPublishedDate: string
|
|
461
|
+
metaLink?: TeaserConcept
|
|
462
|
+
metaAltLink?: TeaserConcept
|
|
463
|
+
metaPrefixText?: string
|
|
464
|
+
metaSuffixText?: string
|
|
465
|
+
indicators: {
|
|
466
|
+
accessLevel: "premium" | "subscribed" | "registered" | "free"
|
|
467
|
+
isOpinion?: boolean
|
|
468
|
+
isColumn?: boolean
|
|
469
|
+
isPodcast?: boolean
|
|
470
|
+
isEditorsChoice?: boolean
|
|
471
|
+
isExclusive?: boolean
|
|
472
|
+
isScoop?: boolean
|
|
473
|
+
}
|
|
474
|
+
image: {
|
|
475
|
+
url: string
|
|
476
|
+
width: number
|
|
477
|
+
height: number
|
|
478
|
+
}
|
|
479
|
+
clientName?: string
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
### `Tweet`
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
interface Tweet extends Node {
|
|
488
|
+
id: string
|
|
489
|
+
type: "tweet"
|
|
490
|
+
external html: string
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Tweet** represents a tweet.
|
|
495
|
+
|
|
496
|
+
### `Flourish`
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
|
|
500
|
+
type FlourishLayoutWidth = Extract<LayoutWidth, "full-grid" | "in-line">
|
|
501
|
+
|
|
502
|
+
interface Flourish extends Node {
|
|
503
|
+
type: "flourish"
|
|
504
|
+
id: string
|
|
505
|
+
layoutWidth: FlourishLayoutWidth
|
|
506
|
+
flourishType: string
|
|
507
|
+
description?: string
|
|
508
|
+
timestamp?: string
|
|
509
|
+
external fallbackImage?: Image
|
|
510
|
+
fragmentIdentifier?: string
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Flourish** represents a flourish chart.
|
|
515
|
+
|
|
516
|
+
### `BigNumber`
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
interface BigNumber extends Node {
|
|
520
|
+
type: "big-number"
|
|
521
|
+
number: string
|
|
522
|
+
description: string
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**BigNumber** represents a big number.
|
|
527
|
+
|
|
528
|
+
### `Video`
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
interface Video extends Node {
|
|
532
|
+
type: "video"
|
|
533
|
+
id: string
|
|
534
|
+
external title: string
|
|
535
|
+
}
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**Video** represents an FT video referenced by a URL.
|
|
539
|
+
|
|
540
|
+
The `title` can be obtained by fetching the Video from the content API.
|
|
541
|
+
|
|
542
|
+
TODO: Figure out how Clips work, how they are different?
|
|
543
|
+
|
|
544
|
+
### `YoutubeVideo`
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
interface YoutubeVideo extends Node {
|
|
548
|
+
type: "youtube-video"
|
|
549
|
+
url: string
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**YoutubeVideo** represents a video referenced by a Youtube URL.
|
|
554
|
+
|
|
555
|
+
### `ScrollyBlock`
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
interface ScrollyBlock extends Parent {
|
|
559
|
+
type: "scrolly-block"
|
|
560
|
+
theme: "sans" | "serif"
|
|
561
|
+
children: ScrollySection[]
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**ScrollyBlock** represents a block for telling stories through scroll position.
|
|
566
|
+
|
|
567
|
+
### `ScrollySection`
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
interface ScrollySection extends Parent {
|
|
571
|
+
type: "scrolly-section"
|
|
572
|
+
display: "dark-background" | "light-background"
|
|
573
|
+
noBox?: true,
|
|
574
|
+
position: "left" | "center" | "right"
|
|
575
|
+
transition?: "delay-before" | "delay-after"
|
|
576
|
+
children: [ScrollyImage, ...ScrollyCopy[]]
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**ScrollySection** represents a section of a [ScrollyBlock](#scrollyblock)
|
|
581
|
+
|
|
582
|
+
### `ScrollyImage`
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
interface ScrollyImage extends Node {
|
|
586
|
+
type: "scrolly-image"
|
|
587
|
+
id: string
|
|
588
|
+
external picture: ImageSetPicture
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
**ScrollyImage** represents an image contained in a [ScrollySection](#scrollysection)
|
|
593
|
+
|
|
594
|
+
### `ScrollyCopy`
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
interface ScrollyCopy extends Parent {
|
|
598
|
+
type: "scrolly-copy"
|
|
599
|
+
children: (ScrollyHeading | Paragraph)[]
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
**ScrollyCopy** represents a collection of **ScrollyHeading** or **Paragraph** nodes.
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
interface ScrollyHeading extends Parent {
|
|
607
|
+
type: "scrolly-heading"
|
|
608
|
+
level: "chapter" | "heading" | "subheading"
|
|
609
|
+
children: Text[]
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**ScrollyHeading** represents a heading within a **ScrollyCopy** block.
|
|
614
|
+
|
|
615
|
+
### `Layout`
|
|
616
|
+
|
|
617
|
+
```ts
|
|
618
|
+
interface Layout extends Parent {
|
|
619
|
+
type: "layout"
|
|
620
|
+
layoutName: "auto" | "card" | "timeline"
|
|
621
|
+
layoutWidth: string
|
|
622
|
+
children: [Heading, LayoutImage, ...LayoutSlot[]] | [Heading, ...LayoutSlot[]] | LayoutSlot[]
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Layout** nodes are a generic component used to display a combination of other
|
|
627
|
+
nodes (headings, images and paragraphs) in a visually distinctive way.
|
|
628
|
+
|
|
629
|
+
The `layoutName` acts as a sort of theme for the component.
|
|
630
|
+
|
|
631
|
+
### `LayoutSlot`
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
interface LayoutSlot extends Parent {
|
|
636
|
+
type: "layout-slot"
|
|
637
|
+
children: (Heading | Paragraph | LayoutImage)[]
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
A **Layout** can contain a number of **LayoutSlots**, which can be arranged
|
|
642
|
+
visually
|
|
643
|
+
|
|
644
|
+
_Non-normative note_: typically these would be displayed as flex items, so they
|
|
645
|
+
would appear next to each other taking up equal width.
|
|
646
|
+
|
|
647
|
+
### `LayoutImage`
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
interface LayoutImage extends Node {
|
|
651
|
+
type: "layout-image"
|
|
652
|
+
id: string
|
|
653
|
+
alt: string
|
|
654
|
+
caption: string
|
|
655
|
+
credit: string
|
|
656
|
+
external picture: ImageSetPicture
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
- **LayoutImage** is a workaround to handle pre-existing articles that were
|
|
661
|
+
published using `<img>` tags rather than `<ft-content>` images. The reason for
|
|
662
|
+
this was that in the bodyXML, layout nodes were inside an `<experimental>`
|
|
663
|
+
tag, and that didn't support publishing `<ft-content>`.
|
|
664
|
+
|
|
665
|
+
### `Table`
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
type TableColumnSettings = {
|
|
669
|
+
hideOnMobile: boolean
|
|
670
|
+
sortable: boolean
|
|
671
|
+
sortType: 'text' | 'number' | 'date' | 'currency' | 'percent'
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
type TableLayoutWidth = Extract<LayoutWidth,
|
|
675
|
+
| 'auto'
|
|
676
|
+
| 'full-grid'
|
|
677
|
+
| 'inset-left'
|
|
678
|
+
| 'inset-right'
|
|
679
|
+
| 'full-bleed'>
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
interface TableCaption extends Parent {
|
|
683
|
+
type: 'table-caption'
|
|
684
|
+
children: Phrasing[]
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
interface TableCell extends Parent {
|
|
688
|
+
type: 'table-cell'
|
|
689
|
+
heading?: boolean
|
|
690
|
+
columnSpan?: number
|
|
691
|
+
rowSpan?: number
|
|
692
|
+
children: Phrasing[]
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
interface TableRow extends Parent {
|
|
696
|
+
type: 'table-row'
|
|
697
|
+
children: TableCell[]
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
interface TableBody extends Parent {
|
|
701
|
+
type: 'table-body'
|
|
702
|
+
children: TableRow[]
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
interface TableFooter extends Parent {
|
|
706
|
+
type: 'table-footer'
|
|
707
|
+
children: Phrasing[]
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
interface Table extends Parent {
|
|
711
|
+
type: 'table'
|
|
712
|
+
stripes: boolean
|
|
713
|
+
compact: boolean
|
|
714
|
+
layoutWidth: TableLayoutWidth
|
|
715
|
+
collapseAfterHowManyRows?: number
|
|
716
|
+
responsiveStyle: 'overflow' | 'flat' | 'scroll'
|
|
717
|
+
children: [TableCaption, TableBody, TableFooter] | [TableCaption, TableBody] | [TableBody, TableFooter] | [TableBody]
|
|
718
|
+
columnSettings: TableColumnSettings[]
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**Table** represents 2d data.
|
|
723
|
+
|
|
724
|
+
### CustomCodeComponent
|
|
725
|
+
|
|
726
|
+
```ts
|
|
727
|
+
type CustomCodeComponentAttributes = {
|
|
728
|
+
[key: string]: string | boolean | undefined
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
interface CustomCodeComponent extends Node {
|
|
732
|
+
/** Component type */
|
|
733
|
+
type: "custom-code-component"
|
|
734
|
+
/** Id taken from the CAPI url */
|
|
735
|
+
id: string
|
|
736
|
+
/** How the component should be presented in the article page according to the column layout system */
|
|
737
|
+
layoutWidth: LayoutWidth
|
|
738
|
+
/** Repository for the code of the component in the format "[github org]/[github repo]/[component name]". */
|
|
739
|
+
external path: string
|
|
740
|
+
/** Semantic version of the code of the component, e.g. "^0.3.5". */
|
|
741
|
+
external versionRange: string
|
|
742
|
+
/** Last date-time when the attributes for this block were modified, in ISO-8601 format. */
|
|
743
|
+
external attributesLastModified: string
|
|
744
|
+
/** Configuration data to be passed to the component. */
|
|
745
|
+
external attributes: CustomCodeComponentAttributes
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
- The **CustomCodeComponent*** allows for more experimental forms of journalism, allowing editors to provide properties via Spark.
|
|
750
|
+
- The component itself lives off-platform, and an example might be a git repository with a standard structure. This structure would include the rendering instructions, and the data structure that is expected to be provided to the component for it to render if necessary.
|
|
751
|
+
- The basic interface in Spark to make reference to this system above (eg. the git repo URL or a public S3 bucket), and provide some data for it if necessary. This will be the Custom Component storyblock.
|
|
752
|
+
- The data Spark receives from entering a specific ID will be used to render dynamic fields (the `attributes`).
|
|
753
|
+
|
|
754
|
+
### ImagePair
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
interface ImagePair extends Parent {
|
|
758
|
+
type: 'image-pair'
|
|
759
|
+
children: [ImageSet, ImageSet]
|
|
760
|
+
}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
**ImagePair** is a set of two images
|
|
764
|
+
|
|
765
|
+
### Timeline
|
|
766
|
+
|
|
767
|
+
```ts
|
|
768
|
+
/**
|
|
769
|
+
* Timeline nodes display a timeline of events in arbitrary order.
|
|
770
|
+
*/
|
|
771
|
+
interface Timeline extends Parent {
|
|
772
|
+
type: "timeline"
|
|
773
|
+
/** The title for the timeline */
|
|
774
|
+
title: string
|
|
775
|
+
children: TimelineEvent[]
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* TimelineEvent is the representation of a single event in a Timeline.
|
|
780
|
+
*/
|
|
781
|
+
interface TimelineEvent extends Parent {
|
|
782
|
+
type: "timeline-event"
|
|
783
|
+
/** The title of the event */
|
|
784
|
+
title: string
|
|
785
|
+
/** Any combination of paragraphs and image sets */
|
|
786
|
+
children: (Paragraph | ImageSet)[];
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### InNumbers
|
|
791
|
+
|
|
792
|
+
```ts
|
|
793
|
+
/**
|
|
794
|
+
* A definition has a term and a related description. It is used to describe a term.
|
|
795
|
+
*/
|
|
796
|
+
interface Definition extends Node {
|
|
797
|
+
type: "definition"
|
|
798
|
+
term: string
|
|
799
|
+
description: string
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* InNumbers represents a set of numbers with related descriptions.
|
|
804
|
+
*/
|
|
805
|
+
interface InNumbers extends Parent {
|
|
806
|
+
type: "in-numbers"
|
|
807
|
+
/** The title for the InNumbers */
|
|
808
|
+
title?: string
|
|
809
|
+
children: [Definition, Definition, Definition]
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### Card
|
|
814
|
+
|
|
815
|
+
```ts
|
|
816
|
+
/** Allowed children for a card
|
|
817
|
+
*/
|
|
818
|
+
type CardChildren = ImageSet | Exclude<FormattingBlock, Heading>
|
|
819
|
+
/**
|
|
820
|
+
* A card describes a subject with images and text
|
|
821
|
+
*/
|
|
822
|
+
interface Card extends Parent {
|
|
823
|
+
type: "card"
|
|
824
|
+
/** The title of this card */
|
|
825
|
+
title?: string
|
|
826
|
+
children: CardChildren[]
|
|
827
|
+
}
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
### InfoBox
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
/**
|
|
834
|
+
* Allowed layout widths for an InfoBox.
|
|
835
|
+
*/
|
|
836
|
+
type InfoBoxLayoutWidth = Extract<LayoutWidth, "in-line" | "inset-left">
|
|
837
|
+
/**
|
|
838
|
+
* An info box describes a subject via a single card
|
|
839
|
+
*/
|
|
840
|
+
interface InfoBox extends Parent {
|
|
841
|
+
type: "info-box"
|
|
842
|
+
/** The layout width supported by this node */
|
|
843
|
+
layoutWidth: InfoBoxLayoutWidth
|
|
844
|
+
children: [Card]
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### InfoPair
|
|
849
|
+
|
|
850
|
+
```ts
|
|
851
|
+
/**
|
|
852
|
+
* InfoPair provides exactly two cards.
|
|
853
|
+
*/
|
|
854
|
+
interface InfoPair extends Parent {
|
|
855
|
+
type: "info-pair"
|
|
856
|
+
/** The title of the info pair */
|
|
857
|
+
title?: string
|
|
858
|
+
children: [Card, Card]
|
|
859
|
+
}
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
|