https://github.com/financial-times/content-tree
A structured definition for Financial Times articles, and its utilities
https://github.com/financial-times/content-tree
content-tree
Last synced: 7 months ago
JSON representation
A structured definition for Financial Times articles, and its utilities
- Host: GitHub
- URL: https://github.com/financial-times/content-tree
- Owner: Financial-Times
- Created: 2022-11-08T12:13:10.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-04-22T11:44:47.000Z (about 1 year ago)
- Last Synced: 2024-04-22T12:42:54.081Z (about 1 year ago)
- Topics: content-tree
- Language: JavaScript
- Homepage:
- Size: 511 KB
- Stars: 4
- Watchers: 11
- Forks: 0
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# ![content-tree][logo]
A tree for Financial Times article content.
---
**content-tree** is a specification for representing Financial Times article
content as an abstract tree. It implements the **[unist][unist]** spec.## Contents
- [Introduction](#introduction)
- [Types](#types)
- [Mixins](#mixins)
- [Nodes](#nodes)
- [License](#license)## Introduction
This document defines a format for representing Financial Times article content
as a tree. This specification is written in a
[typescript](https://www.typescriptlang.org/)-like grammar, augmented by the
addition of the `external` property modifier.The `external` property modifier indicates that the specified field is absent
when the `content-tree` is in
[**transit**](#what-does-it-mean-to-be-in-transit), and required when the
**content-tree** is at rest.### What is `content-tree`?
`content-tree` extends [unist][unist], a format for syntax trees, to benefit
from its [ecosystem of utilities][unist-utilities].`content-tree` relates to [JavaScript][js] in that it has an [ecosystem of
utilities][unist-utilities] for working with trees in JavaScript. However,
`content-tree` is not limited to JavaScript and can be used in other programming
languages.### How to use the types
We provide two namespaces in `content-tree.d.ts`, which is automatically
generated from this README. `ContentTree` and `ContentTree.transit`.Install this repository as a dependency:
```sh notangle
npm install https://github.com/Financial-Times/content-tree
```Use it in your code:
```ts notangle
import type {ContentTree} from "@financial-times/content-tree"function makeBigNumber(): ContentTree.BigNumber {
return {
type: "|"
+--------------+
| "big-number" |
+--------------+
}
}function makeImageSetNoFixins(): ContentTree.transit.ImageSet {
return {
type: "image-set",
id: string,
// if you try to add a `picture` here it will get mad
}
}
```### What does it mean to be in transit?
When a `content-tree` is being rendered visually, external resources have been
fetched and added to the tree. When the `content-tree` is being transmitted
across the network, these external resources are referenced only by their `id`.It is the state of the tree in the network that we call "in transit".
## Abstract Types
These abstract helper types define special types a [Parent](#parent) can use as
[children][term-child].### `BodyBlock`
```ts
type BodyBlock =
| Paragraph
| Heading
| ImageSet
| BigNumber
| CustomCodeComponent
| Layout
| List
| Blockquote
| Pullquote
| ScrollyBlock
| ThematicBreak
| Table
| Recommended
| Tweet
| Video
| YoutubeVideo
````BodyBlock` nodes are the only things that are valid as the top level of a `Body`.
### `LayoutWidth`
```ts
type LayoutWidth =
| "auto"
| "in-line"
| "inset-left"
| "inset-right"
| "full-bleed"
| "full-grid"
| "mid-grid"
| "full-width"
````LayoutWidth` defines how the component should be presented in the article page according to the column layout system.
### `Phrasing`
```ts
type Phrasing = Text | Break | Strong | Emphasis | Strikethrough | Link
```A phrasing node cannot have ancestor of the same type.
i.e. a Strong will never be inside another Strong, or inside any other node that
is inside a Strong.## Nodes
### `Node`
```ts
interface Node {
type: string
data?: any
}
```The abstract node. The data field is for internal implementation information and
will never be defined in the content-tree spec.### `Parent`
```ts
interface Parent extends Node {
children: Node[]
}
```**Parent** (**[UnistParent][term-parent]**) represents a node in content-tree
containing other nodes (said to be _[children][term-child]_).Its content is limited to only other content-tree content.
### `Root`
```ts
interface Root extends Node {
type: "root"
body: Body
}
```**Root** (**[Parent][term-parent]**) represents the root of a content-tree.
**Root** can be used as the _[root][term-root]_ of a _[tree][term-tree]_.
### `Body`
```ts
interface Body extends Parent {
type: "body"
version: number
children: BodyBlock[]
}
```**Body** (**[Parent][term-parent]**) represents the body of an article.
(note: `bodyTree` is just this part)
### `Text`
```ts
interface Text extends Node {
type: "text"
value: string
}
```**Text** (**[Literal][term-literal]**) represents text.
### `Break`
```ts
interface Break extends Node {
type: "break"
}
```**Break** Node represents a break in the text, such as in a poem.
_Non-normative note: this would normally be represented by a `
` in the
html._### `ThematicBreak`
```ts
interface ThematicBreak extends Node {
type: "thematic-break"
}
```**ThematicBreak** Node represents a break in the text, such as in a shift of
topic within a section._Non-normative note: this would be represented by an `
` in the html._### `Paragraph`
```ts
interface Paragraph extends Parent {
type: "paragraph"
children: Phrasing[]
}
```Paragraph represents a unit of text.
### `Heading`
```ts
interface Heading extends Parent {
type: "heading"
children: Text[]
level: "chapter" | "subheading" | "label"
}
```**Heading** represents a unit of text that marks the beginning of an article
section.### `Strong`
```ts
interface Strong extends Parent {
type: "strong"
children: Phrasing[]
}
```**Strong** represents contents with strong importance, seriousness or urgency.
### `Emphasis`
```ts
interface Emphasis extends Parent {
type: "emphasis"
children: Phrasing[]
}
```**Emphasis** represents stressed emphasis of its contents.
### `Strikethrough`
```ts
interface Strikethrough extends Parent {
type: "strikethrough"
children: Phrasing[]
}
```**Strikethrough** represents a piece of text that has been stricken.
### `Link`
```ts
interface Link extends Parent {
type: "link"
url: string
title: string
children: Phrasing[]
}
```**Link** represents a hyperlink.
### `List`
```ts
interface List extends Parent {
type: "list"
ordered: boolean
children: ListItem[]
}
```**List** represents a list of items.
### `ListItem`
```ts
interface ListItem extends Parent {
type: "list-item"
children: (Paragraph | Phrasing)[]
}
```### `Blockquote`
```ts
interface Blockquote extends Parent {
type: "blockquote"
children: (Paragraph | Phrasing)[]
}
```**Blockquote** represents a quotation.
### `Pullquote`
```ts
interface Pullquote extends Node {
type: "pullquote"
text: string
source?: string
}
```**Pullquote** represents a brief quotation taken from the main text of an
article._non normative note:_ the reason this is string properties and not children is
that it is more confusing if a pullquote falls back to text than if it
doesn't. The text is taken from elsewhere in the article.### `ImageSet`
```ts
interface ImageSet extends Node {
type: "image-set"
id: string
external picture: ImageSetPicture
}
```#### Image types
##### `ImageSetPicture`
```ts
type ImageSetPicture = {
layoutWidth: string
imageType: "image" | "graphic"
alt: string
caption: string
credit: string
images: Image[]
fallbackImage: Image
}
````ImageSetPicture` defines the data associated with an [ImageSet](#ImageSet)
##### `Image`
```ts
type Image = {
id: string
width: number
height: number
format:
| "desktop"
| "mobile"
| "square"
| "square-ftedit"
| "standard"
| "wide"
| "standard-inline"
url: string
sourceSet?: ImageSource[]
}
````Image` defines a single use-case of a Picture[#ImageSetPicture].
### `ImageSource`
```ts
type ImageSource = {
url: string
width: number
dpr: number
}
```**ImageSource** defines a single resource for an [image](#image).
### `Recommended`
```ts
interface Recommended extends Node {
type: "recommended"
id: string
heading?: string
teaserTitleOverride?: string
external teaser: Teaser
}
```- Recommended represents a reference to an FT content that has been recommended
by editorial.
- The `heading`, when present, is used where the purpose of the link is more
specific than being "Recommended" (an example might be "In depth")
- The `teaserTitleOverride`, when present, is used in place of the content title
of the link._non normative note:_ historically, recommended links used to be a list of up to
three content items. Testing later showed that having one more prominent link
was more engaging, and Spark (and therefore content-tree)now only supports that
use case.#### Teaser types
These types were extracted from x-dash's
[x-teaser](https://github.com/Financial-Times/x-dash/blob/3408c268/components/x-teaser/Props.d.ts).```ts
type TeaserConcept = {
apiUrl: string
directType: string
id: string
predicate: string
prefLabel: string
type: string
types: string[]
url: string
}type Teaser = {
id: string
url: string
type:
| "article"
| "video"
| "podcast"
| "audio"
| "package"
| "liveblog"
| "promoted-content"
| "paid-post"
title: string
publishedDate: string
firstPublishedDate: string
metaLink?: TeaserConcept
metaAltLink?: TeaserConcept
metaPrefixText?: string
metaSuffixText?: string
indicators: {
accessLevel: "premium" | "subscribed" | "registered" | "free"
isOpinion?: boolean
isColumn?: boolean
isPodcast?: boolean
isEditorsChoice?: boolean
isExclusive?: boolean
isScoop?: boolean
}
image: {
url: string
width: number
height: number
}
}
```### `Tweet`
```ts
interface Tweet extends Node {
id: string
type: "tweet"
external html: string
}
```**Tweet** represents a tweet.
### `Flourish`
```ts
interface Flourish extends Node {
type: "flourish"
id: string
layoutWidth: string
flourishType: string
description?: string
timestamp?: string
fallbackImage?: Image
}
```**Flourish** represents a flourish chart.
### `BigNumber`
```ts
interface BigNumber extends Node {
type: "big-number"
number: string
description: string
}
```**BigNumber** represents a big number.
### `Video`
```ts
interface Video extends Node {
type: "video"
id: string
embedded: boolean
}
```**Video** represents for an FT video referenced by a URL.
TODO: Figure out how Clips work, how they are different?
### `YoutubeVideo`
```ts
interface YoutubeVideo extends Node {
type: "youtube-video"
url: string
}
```**YoutubeVideo** represents a video referenced by a Youtube URL.
### `ScrollyBlock`
```ts
interface ScrollyBlock extends Parent {
type: "scrolly-block"
theme: "sans" | "serif"
children: ScrollySection[]
}
```**ScrollyBlock** represents a block for telling stories through scroll position.
### `ScrollySection`
```ts
interface ScrollySection extends Parent {
type: "scrolly-section"
display: "dark-background" | "light-background"
noBox?: true,
position: "left" | "center" | "right"
transition?: "delay-before" | "delay-after"
children: [ScrollyImage, ...ScrollyCopy[]]
}
```**ScrollySection** represents a section of a [ScrollyBlock](#scrollyblock)
### `ScrollyImage`
```ts
interface ScrollyImage extends Node {
type: "scrolly-image"
id: string
external picture: ImageSetPicture
}
```**ScrollyImage** represents an image contained in a [ScrollySection](#scrollysection)
### `ScrollyCopy`
```ts
interface ScrollyCopy extends Parent {
type: "scrolly-copy"
children: (ScrollyHeading | Paragraph)[]
}
```**ScrollyCopy** represents a collection of **ScrollyHeading** or **Paragraph** nodes.
```ts
interface ScrollyHeading extends Parent {
type: "scrolly-heading"
level: "chapter" | "heading" | "subheading"
children: Text[]
}
```**ScrollyHeading** represents a heading within a **ScrollyCopy** block.
### `Layout`
```ts
interface Layout extends Parent {
type: "layout"
layoutName: "auto" | "card" | "timeline"
layoutWidth: string
children: [Heading, LayoutImage, ...LayoutSlot[]] | [Heading, ...LayoutSlot[]] | LayoutSlot[]
}
```**Layout** nodes are a generic component used to display a combination of other
nodes (headings, images and paragraphs) in a visually distinctive way.The `layoutName` acts as a sort of theme for the component.
### `LayoutSlot`
```ts
interface LayoutSlot extends Parent {
type: "layout-slot"
children: (Heading | Paragraph | LayoutImage)[]
}
```A **Layout** can contain a number of **LayoutSlots**, which can be arranged
visually_Non-normative note_: typically these would be displayed as flex items, so they
would appear next to each other taking up equal width.### `LayoutImage`
```ts
interface LayoutImage extends Node {
type: "layout-image"
id: string
alt: string
caption: string
credit: string
external picture: ImageSetPicture
}
```- **LayoutImage** is a workaround to handle pre-existing articles that were
published using `` tags rather than `` images. The reason for
this was that in the bodyXML, layout nodes were inside an ``
tag, and that didn't support publishing ``.### `Table`
```ts
type TableColumnSettings = {
hideOnMobile: boolean
sortable: boolean
sortType: 'text' | 'number' | 'date' | 'currency' | 'percent'
}interface TableCaption extends Parent {
type: 'table-caption'
children: Phrasing[]
}interface TableCell extends Parent {
type: 'table-cell'
heading?: boolean
children: Phrasing[]
}interface TableRow extends Parent {
type: 'table-row'
children: TableCell[]
}interface TableBody extends Parent {
type: 'table-body'
children: TableRow[]
}interface TableFooter extends Parent {
type: 'table-footer'
children: Phrasing[]
}interface Table extends Parent {
type: 'table'
stripes: boolean
compact: boolean
layoutWidth:
| 'auto'
| 'full-grid'
| 'inset-left'
| 'inset-right'
| 'full-bleed'
collapseAfterHowManyRows?: number
responsiveStyle: 'overflow' | 'flat' | 'scroll'
children: [TableCaption, TableBody, TableFooter] | [TableCaption, TableBody] | [TableBody, TableFooter] | [TableBody]
columnSettings: TableColumnSettings[]
}
```**Table** represents 2d data.
### CustomCodeComponent
```ts
type CustomCodeComponentAttributes = {
[key: string]: string | boolean | undefined
}interface CustomCodeComponent extends Node {
/** Component type */
type: "custom-code-component"
/** Id taken from the CAPI url */
id: string
/** How the component should be presented in the article page according to the column layout system */
layoutWidth: LayoutWidth
/** Repository for the code of the component in the format "[github org]/[github repo]/[component name]". */
external path: string
/** Semantic version of the code of the component, e.g. "^0.3.5". */
external versionRange: string
/** Last date-time when the attributes for this block were modified, in ISO-8601 format. */
external attributesLastModified: string
/** Configuration data to be passed to the component. */
external attributes: CustomCodeComponentAttributes
}
```- The **CustomCodeComponent*** allows for more experimental forms of journalism, allowing editors to provide properties via Spark.
- 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.
- 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.
- The data Spark receives from entering a specific ID will be used to render dynamic fields (the `attributes`).## License
This software is published by the Financial Times under the [MIT licence](mit).
Derived from [unist][unist] © [Titus Wormer][titus]
[mit]: http://opensource.org/licenses/MIT
[titus]: https://wooorm.com
[logo]: ./logo.png
[unist]: https://github.com/syntax-tree/unist
[js]: https://www.ecma-international.org/ecma-262/9.0/index.html
[webidl]: https://heycam.github.io/webidl/
[term-tree]: https://github.com/syntax-tree/unist#tree
[term-literal]: https://github.com/syntax-tree/unist#tree
[term-parent]: https://github.com/syntax-tree/unist#parent
[term-child]: https://github.com/syntax-tree/unist#child
[term-root]: https://github.com/syntax-tree/unist#root
[term-leaf]: https://github.com/syntax-tree/unist#leaf
[unist-utilities]: https://github.com/syntax-tree/unist#utilities