Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve fromBodyXML to convert Spark's kitchen sink. #58

Merged
merged 12 commits into from
Jan 28, 2025
135 changes: 115 additions & 20 deletions libraries/from-bodyxml/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ let ContentType = {
article: "http://www.ft.com/ontology/content/Article",
}

/**
* @param {string} layoutWidth
* @returns {ContentTree.LayoutWidth}
*/
function toValidLayoutWidth(layoutWidth) {
if(["auto", "in-line", "inset-left", "inset-right", "full-bleed", "full-grid", "mid-grid", "full-width"].includes(layoutWidth)) {
return /** @type {ContentTree.LayoutWidth} */(layoutWidth);
} else {
return 'full-width';
}
}
/**
* @typedef {import("unist").Parent} UParent
* @typedef {import("unist").Node} UNode
Expand Down Expand Up @@ -47,6 +58,15 @@ export let defaultTransformers = {
level: "subheading",
}
},
/**
* @type {Transformer<ContentTree.transit.Heading>}
*/
h3(h3) {
return {
type: "heading",
level: "subheading",
}
},
/**
* @type {Transformer<ContentTree.transit.Heading>}
*/
Expand Down Expand Up @@ -105,14 +125,25 @@ export let defaultTransformers = {
}
},
/**
* @type {Transformer<ContentTree.transit.Link>}
* @type {Transformer<ContentTree.transit.Link | ContentTree.transit.YoutubeVideo>}
*/
a(a) {
return {
if(a.attributes['data-asset-type'] === 'video') {
const url = a.attributes.href ?? '';
if(url.includes('youtube.com')) {
return /** @type {ContentTree.transit.YoutubeVideo} */({
type: "youtube-video",
url: url,
children: null
})
}
//TODO: specialist support Vimeo, but this isn't in the Content Tree spec yet
}
return /** @type {ContentTree.transit.Link} */({
type: "link",
title: a.attributes.title ?? "",
url: a.attributes.href ?? "",
}
})
},
/**
* @type {Transformer<ContentTree.transit.List>}
Expand Down Expand Up @@ -161,17 +192,31 @@ export let defaultTransformers = {
children: null,
}
},
/**
* @type {Transformer<ContentTree.transit.BigNumber>}
*/
["big-number"](bn) {
let number = find(bn, {name: "big-number-headline"})
let description = find(bn, {name: "big-number-intro"})
return {
type: "big-number",
number: number ? xastToString(number) : "",
description: description ? xastToString(description) : "",
children: null,
}
},
/**
* @type {Transformer<ContentTree.transit.LayoutImage>}
*/
img(img) {
return {
type: "layout-image",
id: img.attributes.src ?? "",
credit: img.attributes["data-copyright"]?.replace(/^© /, "") ?? "",
credit: img.attributes["data-copyright"] ?? "",
// todo this can't be right
alt: img.attributes.longdesc ?? "",
alt: img.attributes.alt ?? "",
caption: img.attributes.longdesc ?? "",
children: null,
}
},
/**
Expand All @@ -180,7 +225,7 @@ export let defaultTransformers = {
[ContentType.imageset](content) {
return {
type: "image-set",
id: content.attributes.id ?? "",
id: content.attributes.url ?? "",
children: null,
}
},
Expand All @@ -190,42 +235,89 @@ export let defaultTransformers = {
[ContentType.video](content) {
return {
type: "video",
id: content.attributes.id ?? "",
id: content.attributes.url ?? "",
embedded: content.attributes["data-embedded"] == "true" ? true : false,
children: null,
}
},
// TODO these two Link transforms may be wrong. what is a "content" or an "article"?
/**
* @type {Transformer<ContentTree.transit.Flourish>}
* @type {Transformer<ContentTree.transit.Flourish | ContentTree.transit.Link>}
*/
[ContentType.content](content) {
if (content.attributes["data-asset-type"] == "flourish") {
return {
return /** @type {ContentTree.transit.Flourish} */ ({
type: "flourish",
flourishType: content.attributes["data-flourish-type"] || "",
layoutWidth: content.attributes["data-layout-width"] || "",
layoutWidth: toValidLayoutWidth(content.attributes["data-layout-width"] || ""),
description: content.attributes["alt"] || "",
timestamp: content.attributes["data-time-stamp"] || "",
// fallbackImage -- TODO should this be external in content-tree?
}
})
}
return {
const id = content.attributes.url ?? "";
const uuid = id.split('/').pop();
return /** @type {ContentTree.transit.Link} */({
type: "link",
url: `https://www.ft.com/content/${content.attributes.id}`,
url: `https://www.ft.com/content/${uuid}`,
title: content.attributes.dataTitle ?? "",
}
})
},
/**
* @type {Transformer<ContentTree.transit.Link>}
*/
[ContentType.article](content) {
const id = content.attributes.url ?? "";
const uuid = id.split('/').pop();
return {
type: "link",
url: `https://www.ft.com/content/${content.attributes.id}`,
url: `https://www.ft.com/content/${uuid}`,
title: content.attributes.dataTitle ?? "",
}
},
/**
* @type {Transformer<ContentTree.transit.Recommended>}
*/
recommended(rl) {
const link = find(rl, { name: 'ft-content'});
const heading = find(rl, { name: 'recommended-title'});
return {
type: "recommended",
id: link?.attributes?.url ?? "",
heading: heading ? xastToString(heading) : "",
teaserTitleOverride: link ? xastToString(link) : "",
children: null
}
},
/**
* @type {Transformer<
* ContentTree.transit.Layout |
* ContentTree.transit.LayoutSlot |
* { type: "__LIFT_CHILDREN__"} |
* { type: "__UNKNOWN__"}
* >}
*/
div(div) {
if(div.attributes.class === "n-content-layout") {
return /** @type {ContentTree.transit.Layout} */({
type: "layout",
layoutName: div.attributes['data-layout-name'] ?? "auto",
layoutWidth: toValidLayoutWidth(div.attributes['data-layout-width'] ?? ""),
});
}
if(div.attributes.class === "n-content-layout__container") {
return { type: "__LIFT_CHILDREN__" };
}
if(div.attributes.class === "n-content-layout__slot") {
return /** @type { ContentTree.transit.LayoutSlot } */({
type: "layout-slot"
})
}
return { type: "__UNKNOWN__" };
},
experimental() {
return { type: "__LIFT_CHILDREN__" }
}
}

/**
Expand Down Expand Up @@ -263,22 +355,25 @@ export function fromXast(bodyxast, transformers = defaultTransformers) {
type: "root",
body: {
type: "body",
children: xmlnode.children[0].children.map(walk),
version: 1,
// this is a flatmap because of <experimental/>
children: xmlnode.children[0].children.flatMap(walk),
},
}
} else if (isXElement(xmlnode)) {
// i thought about this solution for no more than 5 seconds
if (xmlnode.name == "experimental") {
return xmlnode.children.map(walk)
}

let transformer =
(xmlnode.name == "content" || xmlnode.name == "ft-content")
? String(xmlnode.attributes.type)
: xmlnode.name

if (transformer in transformers) {
let ctnode = transformers[transformer](xmlnode)
if ("children" in ctnode && ctnode.children === null) {
if(ctnode.type === "__LIFT_CHILDREN__") {
// we don't want this node to stick around, but we want to keep its' children
return xmlnode.children.flatMap(walk);
} else if ("children" in ctnode && ctnode.children === null) {
// this is how we indicate we shouldn't iterate, but this thing
// shouldn't have any children
delete ctnode.children
Expand Down
2 changes: 1 addition & 1 deletion libraries/from-bodyxml/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ for (let inputName of inputNames) {
}
})
}
}
}
Loading
Loading