diff --git a/packages/@atjson/offset-annotations/src/annotations/ceros-embed.ts b/packages/@atjson/offset-annotations/src/annotations/ceros-embed.ts index 22ce4939e..2b901685b 100644 --- a/packages/@atjson/offset-annotations/src/annotations/ceros-embed.ts +++ b/packages/@atjson/offset-annotations/src/annotations/ceros-embed.ts @@ -1,35 +1,89 @@ import { BlockAnnotation } from "@atjson/document"; -export class CerosEmbed extends BlockAnnotation<{ - /** - * The URL to the Ceros experience. - */ - url: string; - - /** - * The aspect ratio, as a fraction of the embed, which - * is used so the embed can be scaled automatically - * by the Ceros script tag. - */ - aspectRatio: number; - - /** - * The mobile aspect ratio of the embed, which is chosen - * by Ceros when on smaller screen sizes. - */ - mobileAspectRatio?: number; - - /** - * Layout information, used to indicate mutually - * exclusive layouts, for example sizes, floats, etc. - */ - layout?: string; - - /** - * A named identifier used to quickly jump to this item - */ - anchorName?: string; -}> { +export class CerosEmbed extends BlockAnnotation< + | { + /** + * The URL to the Ceros experience. + */ + url: string; + + /** + * Layout information, used to indicate mutually + * exclusive layouts, for example sizes, floats, etc. + */ + layout?: string; + + /** + * Accessible title for the generated iframe. For Flex embeds this + * is sourced from the container's `data-title` attribute. + */ + title?: string; + + /** + * A named identifier used to quickly jump to this item + */ + anchorName?: string; + + /** + * The type of ceros embed. + */ + cerosType?: "studio"; + + /** + * The aspect ratio, as a fraction of the embed, which + * is used so the embed can be scaled automatically + * by the Ceros script tag. + */ + aspectRatio: number; + + /** + * The mobile aspect ratio of the embed, which is chosen + * by Ceros when on smaller screen sizes. + */ + mobileAspectRatio?: number; + } + | { + /** + * The URL to the Ceros experience. + */ + url: string; + + /** + * Layout information, used to indicate mutually + * exclusive layouts, for example sizes, floats, etc. + */ + layout?: string; + + /** + * Accessible title for the generated iframe. For Flex embeds this + * is sourced from the container's `data-title` attribute. + */ + title?: string; + + /** + * A named identifier used to quickly jump to this item + */ + anchorName?: string; + + /** + * The type of ceros embed. + */ + cerosType: "flex"; + + /** + * The configured width for a Flex embed. Per Ceros docs this is + * typically a percentage value such as `100%`. + */ + embedWidth?: string; + + /** + * The configured height for a Flex embed. Per Ceros docs this is + * either `auto` for a full-height embed or a fixed pixel value + * such as `800px` for a scrolling embed. + */ + embedHeight?: string; + } +> { static vendorPrefix = "offset"; static type = "ceros-embed"; } diff --git a/packages/@atjson/renderer-html/src/index.ts b/packages/@atjson/renderer-html/src/index.ts index 2ed1fe8de..15f16c64b 100644 --- a/packages/@atjson/renderer-html/src/index.ts +++ b/packages/@atjson/renderer-html/src/index.ts @@ -132,11 +132,26 @@ export default class HTMLRenderer extends Renderer { } *CerosEmbed(embed: Block) { + if (embed.attributes.cerosType === "flex") { + let { embedHeight, embedWidth, title, url } = embed.attributes; + return `
`; + } + + let { anchorName, aspectRatio, mobileAspectRatio, title, url } = + embed.attributes; + return `
*
* */ + /** + * Ceros flex embeds are wrapped in divs + *
+ * + */ let containers = doc.where(isCerosContainer).as("container"); let iframeTags = doc.where(isCerosExperienceFrame).as("iframes"); + let flexContainers = doc.where(isFlexCerosContainer).as("container"); doc.where(isCerosOriginDomainsScript).remove(); @@ -99,9 +141,48 @@ export default function convertThirdPartyEmbeds(doc: Document) { anchorName: iframes[0].attributes.id, aspectRatio, mobileAspectRatio, + title: iframes[0].attributes.title, url: iframes[0].attributes.src, }, - }) + }), + ); + }); + + flexContainers + .join( + doc.where(isFlexCerosScript).as("scripts"), + function scriptAfterFlexContainer(container, script: Script) { + return adjacentSiblingWithOptionalWhitespace(doc, container, script); + }, + ) + .update(({ container, scripts }) => { + let script = scripts.find( + (annotation) => + typeof annotation.attributes.src === "string" && + annotation.attributes.src.length > 0, + ); + + if (!script) return; + + if (container.end !== script.start) { + doc.deleteText(container.end, script.start); + } + + doc.removeAnnotations(scripts); + + doc.replaceAnnotation( + container, + new CerosEmbed({ + start: container.start, + end: container.end, + attributes: { + cerosType: "flex", + url: container.attributes.dataset["ceros-experience"], + embedWidth: container.attributes.dataset["embed-width"], + embedHeight: container.attributes.dataset["embed-height"], + title: container.attributes.dataset["title"], + }, + }), ); }); @@ -120,7 +201,7 @@ export default function convertThirdPartyEmbeds(doc: Document) { src.match(/fwcdn\d\.com\//) != null || src.match(/fwpub\d\.com\//) != null) ); - } + }, ) .update(({ embed, scripts }) => { let playlist = embed.attributes.playlist; @@ -142,7 +223,7 @@ export default function convertThirdPartyEmbeds(doc: Document) { channel: channel, open: embed.attributes.open_in, }, - }) + }), ); // Remove newlines from embed code if (scripts.length) { @@ -184,7 +265,7 @@ export default function convertThirdPartyEmbeds(doc: Document) { audioId, anchorName, }, - }) + }), ); }); /** @@ -212,7 +293,7 @@ export default function convertThirdPartyEmbeds(doc: Document) { audioType, anchorName: iframe.attributes.id, }, - }) + }), ); }); /** @@ -238,7 +319,7 @@ export default function convertThirdPartyEmbeds(doc: Document) { attributes: { url: embed.attributes.url, }, - }) + }), ); }); diff --git a/packages/@atjson/source-html/test/ceros-embed.test.ts b/packages/@atjson/source-html/test/ceros-embed.test.ts index b44f803af..01d11f671 100644 --- a/packages/@atjson/source-html/test/ceros-embed.test.ts +++ b/packages/@atjson/source-html/test/ceros-embed.test.ts @@ -96,4 +96,111 @@ describe("CerosEmbed", () => { } `); }); + + test("parses Flex embeds into a structured Ceros block", () => { + let doc = HTMLSource.fromRaw( + `
+`, + ).convertTo(OffsetSource); + + expect(serialize(doc, { withStableIds: true })).toMatchInlineSnapshot(` + { + "blocks": [ + { + "attributes": { + "cerosType": "flex", + "embedHeight": "auto", + "embedWidth": "100%", + "title": "Example Flex Experience", + "url": "https://flexamples.ceros.site/example-1", + }, + "id": "B00000000", + "parents": [], + "selfClosing": false, + "type": "ceros-embed", + }, + ], + "marks": [], + "text": "", + } + `); + }); + + test("mixed studio and flex scenarios keep both embeds and remove only matched scripts", () => { + let doc = HTMLSource.fromRaw( + `
`, + ).convertTo(OffsetSource); + + expect(serialize(doc, { withStableIds: true })).toMatchInlineSnapshot(` + { + "blocks": [ + { + "attributes": { + "aspectRatio": 2, + "title": "Studio carousel", + "url": "//view.ceros.com/ceros-inspire/carousel-3", + }, + "id": "B00000000", + "parents": [], + "selfClosing": false, + "type": "ceros-embed", + }, + { + "attributes": { + "cerosType": "flex", + "embedHeight": "auto", + "embedWidth": "100%", + "title": "Example Flex Experience", + "url": "https://flexamples.ceros.site/example-1", + }, + "id": "B00000001", + "parents": [], + "selfClosing": false, + "type": "ceros-embed", + }, + ], + "marks": [ + { + "attributes": { + "-html-src": "https://www.example.com/keep-me.js", + }, + "id": "M00000000", + "range": "(2..2]", + "type": "-html-script", + }, + ], + "text": "", + } + `); + }); + + test("does not treat non-ceros embed scripts as flex embeds", () => { + let doc = HTMLSource.fromRaw( + `
+`, + ).convertTo(OffsetSource); + + expect(serialize(doc, { withStableIds: true })).toMatchObject({ + blocks: [{ type: "text", attributes: {} }], + marks: [ + { + type: "-html-div", + attributes: { + "-html-dataset": { + "-html-ceros-experience": + "https://flexamples.ceros.site/example-1", + "-html-embed-height": "auto", + "-html-embed-width": "100%", + }, + }, + }, + { + type: "-html-script", + attributes: { + "-html-src": "https://www.example.com/js/embed.v1.js", + }, + }, + ], + }); + }); }); diff --git a/tests/fixtures/html/ceros-flex.html b/tests/fixtures/html/ceros-flex.html new file mode 100644 index 000000000..e5efbfc16 --- /dev/null +++ b/tests/fixtures/html/ceros-flex.html @@ -0,0 +1,7 @@ +

This is the Flex Ceros embed:

+
+ +

+ This is some text after, to make sure that the converter doesn't strip the + HTML after the embed +