Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
/**
* A generated surface before hydration.
*
* @typedef {import("../../models/aitab/AITab.sys.mjs").A2UISurface} A2UISurface
*/
/**
* A component with its catalog properties.
*
* @typedef {import("../../models/aitab/AITab.sys.mjs").A2UIComponent &
* import("../../models/aitab/AITab.sys.mjs").A2UIComponentProps} AITabComponent
*/
/**
* A Smart Page prepared for rendering.
*
* @typedef {object} HydratedAITabData
* @property {string} component - Root component type.
* @property {AITabComponent[]} children - Hydrated header and content components.
*/
/**
* Parses A2UI JSON objects to prepare for rendering
*/
export class A2UI {
#components;
#dataModel;
#hydrators;
#surface;
/**
* @param {A2UISurface} a2ui - Generated surface to hydrate.
*/
constructor(a2ui) {
this.#surface = structuredClone(a2ui);
this.#hydrators = new Map();
this.#hydrators.set("Highlights", this.#hydrateHighlights.bind(this));
this.#hydrators.set("SourceLinks", this.#hydrateSourceLinks.bind(this));
this.#hydrators.set("TextBlock", this.#hydrateTextBlock.bind(this));
this.#hydrators.set("RankedTable", this.#hydrateRankedTable.bind(this));
this.#hydrators.set("Cards", this.#hydrateCards.bind(this));
this.#hydrators.set("Timeline", this.#hydrateTimeline.bind(this));
this.#hydrators.set("List", this.#hydrateList.bind(this));
}
/**
* Iterates the A2UI JSON and prepares for rendering
*
* @returns {HydratedAITabData} Page with hydrated child components.
*/
toUI() {
const surface = structuredClone(this.#surface);
this.#dataModel = surface.dataModel;
this.#components = new Map();
surface.components.forEach(component => {
this.#components.set(component.id, component);
});
const root = this.#components.get("root");
if (!root) {
return {
component: "Page",
children: [],
};
}
const page = {
component: root.component,
children: [],
};
this.#getHeader(root, page);
this.#getChildren(root, page);
return page;
}
#hydrateList(component, dataModel = undefined) {
if (component.title) {
component.title = this.#resolveValue(component.title, "", dataModel);
}
if (component.description) {
component.description = this.#resolveValue(
component.description,
"",
dataModel
);
}
component.groups = this.#resolveArray(component.groups, [], dataModel);
return component;
}
#hydrateTimeline(component, dataModel = undefined) {
if (component.title) {
component.title = this.#resolveValue(component.title, "", dataModel);
}
if (component.description) {
component.description = this.#resolveValue(
component.description,
"",
dataModel
);
}
component.items = this.#resolveArray(component.items, [], dataModel);
return component;
}
#hydrateCards(component, dataModel = undefined) {
if (component.title) {
component.title = this.#resolveValue(component.title, "", dataModel);
}
component.items = this.#resolveArray(component.items, [], dataModel);
return component;
}
#hydrateRankedTable(component, dataModel = undefined) {
if (component.title) {
component.title = this.#resolveValue(component.title, "", dataModel);
}
if (component.description) {
component.description = this.#resolveValue(
component.description,
"",
dataModel
);
}
component.rows = this.#resolveArray(component.rows, [], dataModel);
return component;
}
#hydrateTextBlock(component, dataModel = undefined) {
if (component.lead) {
component.lead = this.#resolveValue(component.lead, "", dataModel);
}
if (component.paragraphs) {
component.paragraphs = this.#resolveArray(
component.paragraphs,
[],
dataModel
);
}
return component;
}
#hydrateHighlights(component, dataModel = undefined) {
component.title = component.title
? this.#resolveValue(component.title, "", dataModel)
: undefined;
component.items = this.#resolveArray(
component.items,
[],
dataModel,
highlightItem => this.#hydrateHighlightItem(highlightItem, dataModel)
);
return component;
}
#getHeader(root, page) {
if (!root.header || !this.#components.has(root.header)) {
return;
}
const header = this.#components.get(root.header);
if (header.eyebrow) {
header.eyebrow = this.#resolveValue(header.eyebrow, "");
}
if (header.title) {
header.title = this.#resolveValue(header.title, "");
}
if (header.subhead) {
header.subhead = this.#resolveValue(header.subhead, "");
}
if (header.references) {
header.references = this.#hydrateSourceLinks(header.references);
}
page.children.push(header);
}
#hydrateSourceLinks(sourceLinks, dataModel = undefined) {
if (sourceLinks.title) {
sourceLinks.title = this.#resolveValue(sourceLinks.title, "", dataModel);
}
if (sourceLinks.items) {
sourceLinks.items = this.#resolveArray(sourceLinks.items, [], dataModel);
}
return sourceLinks;
}
#getChildren(root, page) {
if (!root.children) {
return;
}
if (Array.isArray(root.children)) {
for (const child of root.children) {
if (!this.#components.has(child)) {
continue;
}
const component = this.#components.get(child);
page.children.push(this.#hydrateComponent(component));
}
return;
}
const { componentId, path } = root.children;
if (
typeof componentId !== "string" ||
typeof path !== "string" ||
!this.#components.has(componentId)
) {
return;
}
const dataModels = this.#resolvePath(path, []);
if (!Array.isArray(dataModels)) {
return;
}
const template = this.#components.get(componentId);
for (const dataModel of dataModels) {
const component = structuredClone(template);
page.children.push(this.#hydrateComponent(component, dataModel));
}
}
#hydrateComponent(component, dataModel = undefined) {
const hydrator = this.#hydrators.get(component.component);
if (typeof hydrator === "function") {
return hydrator(component, dataModel);
}
return component;
}
#resolveValue(target, defaultValue = undefined, dataModel = undefined) {
if (!target) {
return null;
}
if (
typeof target === "object" &&
!Array.isArray(target) &&
typeof target.path === "string"
) {
return this.#resolvePath(target.path, defaultValue, dataModel);
}
return target;
}
#resolveArray(
target,
defaultValue = [],
dataModel = undefined,
itemMapper = undefined
) {
const value = this.#resolveValue(target, defaultValue, dataModel);
if (!Array.isArray(value)) {
return defaultValue;
}
return itemMapper ? value.map(itemMapper) : value;
}
#hydrateHighlightItem(highlightItem, dataModel = undefined) {
if (highlightItem.sources) {
highlightItem.sources = this.#hydrateSourceLinks(
highlightItem.sources,
dataModel
);
}
return highlightItem;
}
#resolvePath(path, defaultValue = undefined, dataModel = undefined) {
const absolutePath = path.startsWith("/");
if (!absolutePath && !dataModel) {
return defaultValue;
}
let value = absolutePath ? this.#dataModel : dataModel;
const fieldPath = path.split("/");
if (absolutePath) {
fieldPath.shift();
}
while (fieldPath.length) {
if (value == null || typeof value !== "object") {
return undefined;
}
let key = fieldPath.shift();
key = key.replace(/~1/g, "/").replace(/~0/g, "~");
value = value[key];
}
return value;
}
}