import type { DocumentContent } from "../content/document" import { GroupContentType } from "../content/group" import type { GroupContent, GroupItemContent } from "../content/group" import { isNestableContent } from "../content/nestable" import { isRepeatableContentValue, RepeatableContentType } from "../content/repeatable" import type { RepeatableContent } from "../content/repeatable" import { RichTextContentType } from "../content/richText" import { CompositeSliceContentType, SharedSliceContentType } from "../content/slice" import type { CompositeSliceContent, SharedSliceContent } from "../content/slice" import { type CompositeSliceItemContent, type SharedSliceItemContent, type LegacySliceItemContent, type SliceItemContent, type SlicesContent, SlicesContentType, } from "../content/slices" import { TableContentType, type TableContent } from "../content/table" import type { WidgetContent } from "../content/widget" import type { StaticCustomTypeModel } from "../model/customType" import type { GroupModel, NestedGroupModel } from "../model/group" import type { LinkModel } from "../model/link" import type { NestableModel } from "../model/nestable" import type { CompositeSliceModel, LegacySliceModel, SharedSliceVariationContentModel, SliceContentModel, } from "../model/slice" import type { StaticSlicesModel } from "../model/slices" import { TableCellModel } from "../model/table" import type { TableModel } from "../model/table" import type { StaticWidgetModel } from "../model/widget" import * as contentPath from "./contentPath" import * as customTypeModel from "./customTypeModel" type TraverseDocumentContentWithModelConfig = { model: StaticCustomTypeModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } export function traverseDocumentContentWithModel( document: DocumentContent, config: TraverseDocumentContentWithModelConfig, ): DocumentContent { const { model, transformWidget, transformSlice } = config // Remove tabs from custom type model const flatModel = model ? customTypeModel.flatten(model) : {} const traversed: DocumentContent = {} for (const [key, model] of Object.entries(flatModel)) { const path = contentPath.make( { key: config.model.id, type: "CustomType" }, { key, type: "Widget" }, ) const traversedContent = traverseWidgetContentWithModel(path, key, key, document[key], { model, transformWidget, transformSlice, }) if (traversedContent) { traversed[key] = traversedContent } } return traversed } type TraverseSlicesContentWithModelConfig = { model: StaticSlicesModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } export function traverseSlicesContentWithModel( path: contentPath.Path, key: string, apiID: string, content: SlicesContent | undefined, config: TraverseSlicesContentWithModelConfig, ): SlicesContent | undefined { const { transformWidget = (args) => args.content, transformSlice = (args) => args.content } = config if (!content) { return transformWidget({ path, key, apiID, model: config.model, }) } const traversed: SlicesContent["value"] = [] for (const sliceItemContent of content.value) { const sliceModel = config.model.config?.choices?.[sliceItemContent.name] let traversedSliceItemContent: SliceItemContent | undefined if (sliceModel?.type === "SharedSlice") { if (sliceItemContent.widget.__TYPE__ === SharedSliceContentType) { // Happy path, content and model are both shared slices const variationID = sliceItemContent.widget.variation const variationModel = sliceModel.variations.find( (variation) => variation.id === variationID, ) if (variationModel) { const model: SharedSliceVariationContentModel = { type: "SharedSlice", sliceName: sliceModel.id, variationID: variationModel.id, fields: { primary: variationModel.primary || {}, items: variationModel.items || {}, }, } traversedSliceItemContent = traverseSharedSliceContentWithModel( path.concat({ key: sliceItemContent.key, type: "SharedSlice" }), sliceItemContent.key, sliceItemContent.name, sliceItemContent as SharedSliceItemContent, { model, transformWidget, transformSlice }, ) } } else if (sliceItemContent.widget.__TYPE__ === CompositeSliceContentType) { // Migrated path, content is composite, model is shared slice const variationModel = sliceModel.variations[0] if (variationModel) { const model: SharedSliceVariationContentModel = { type: "SharedSlice", sliceName: sliceModel.id, variationID: variationModel.id, fields: { primary: variationModel.primary || {}, items: variationModel.items || {}, }, } traversedSliceItemContent = traverseCompositeSliceContentWithModel( path.concat({ key: sliceItemContent.key, type: "Slice" }), sliceItemContent.key, sliceItemContent.name, sliceItemContent as CompositeSliceItemContent, { model, transformWidget, transformSlice }, ) } } } else if (sliceModel?.type === "Slice") { if (sliceItemContent.widget.__TYPE__ === CompositeSliceContentType) { traversedSliceItemContent = traverseCompositeSliceContentWithModel( path.concat({ key: sliceItemContent.key, type: "Slice" }), sliceItemContent.key, sliceItemContent.name, sliceItemContent as CompositeSliceItemContent, { model: sliceModel, transformWidget, transformSlice }, ) } } else if ( sliceModel && sliceItemContent.widget.__TYPE__ !== SharedSliceContentType && sliceItemContent.widget.__TYPE__ !== CompositeSliceContentType ) { traversedSliceItemContent = traverseLegacySliceContentWithModel( path.concat({ key: sliceItemContent.key, type: "LegacySlice" }), sliceItemContent.key, sliceItemContent.name, sliceItemContent as LegacySliceItemContent, { model: sliceModel, transformWidget, transformSlice }, ) } if (traversedSliceItemContent) { traversed.push(traversedSliceItemContent) } } return transformWidget({ path, key, apiID, content: { ...content, value: traversed }, model: config.model, }) } type TraverseSharedSliceContentWithModelConfig = { model: SharedSliceVariationContentModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } export function traverseSharedSliceContentWithModel( path: contentPath.Path, key: string, apiID: string, content: SharedSliceItemContent | undefined, config: TraverseSharedSliceContentWithModelConfig, ): SharedSliceItemContent | undefined { if (!content) { return } const { transformWidget = (args) => args.content, transformSlice = (args) => args.content } = config const traversedPrimary: SharedSliceContent["primary"] = {} for (const [key, model] of Object.entries(config.model.fields.primary ?? {})) { const traversedContent = traverseWidgetContentWithModel( path.concat({ key: "primary", type: "primary" }, { key, type: "Widget" }), key, key, content.widget.primary[key], { model, transformWidget }, ) if ( traversedContent && (isNestableContent(traversedContent) || traversedContent.__TYPE__ === GroupContentType) ) { traversedPrimary[key] = traversedContent } } const traversedItems = traverseGroupItemsContentWithModel( path.concat({ key: "items", type: "items" }), content.widget.items, { model: config.model.fields.items ?? {}, transformWidget }, ) return transformSlice({ path, key, apiID, content: { ...content, widget: { ...content.widget, primary: traversedPrimary, items: traversedItems }, }, model: config.model, }) } type TraverseCompositeSliceContentWithModelConfig = { model: CompositeSliceModel | SharedSliceVariationContentModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } export function traverseCompositeSliceContentWithModel( path: contentPath.Path, key: string, apiID: string, content: CompositeSliceItemContent | undefined, config: TraverseCompositeSliceContentWithModelConfig, ): CompositeSliceItemContent | SharedSliceItemContent | undefined { if (!content) { return } const { transformWidget = (args) => args.content, transformSlice = (args) => args.content } = config const primaryModel = config.model?.type === "SharedSlice" ? config.model?.fields.primary : config.model?.["non-repeat"] const traversedPrimary: CompositeSliceContent["nonRepeat"] = {} for (const [key, model] of Object.entries(primaryModel ?? {})) { const traversedContent = traverseWidgetContentWithModel( path.concat({ key: "non-repeat", type: "primary" }, { key, type: "Widget" }), key, apiID, content.widget.nonRepeat[key], { model, transformWidget }, ) if (traversedContent && isNestableContent(traversedContent)) { traversedPrimary[key] = traversedContent } } const itemsModel = config.model?.type === "SharedSlice" ? config.model?.fields.items : config.model?.repeat const traversedItems = traverseGroupItemsContentWithModel( path.concat({ key: "repeat", type: "items" }), content.widget.repeat, { model: itemsModel ?? {}, transformWidget }, ) return transformSlice({ path, key, apiID, content: { ...content, widget: { ...content.widget, nonRepeat: traversedPrimary, repeat: traversedItems }, }, model: config.model, }) } type TraverseLegacySliceContentWithModelConfig = { model: LegacySliceModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } export function traverseLegacySliceContentWithModel( path: contentPath.Path, key: string, apiID: string, content: LegacySliceItemContent | undefined, config: TraverseLegacySliceContentWithModelConfig, ): LegacySliceItemContent | SharedSliceItemContent | undefined { if (!content) { return } const { model, transformWidget = (args) => args.content, transformSlice = (args) => args.content, } = config const traversedContent = traverseWidgetContentWithModel(path, key, apiID, content.widget, { model, transformWidget, transformSlice, }) if ( traversedContent && (isNestableContent(traversedContent) || traversedContent.__TYPE__ === GroupContentType) ) { return transformSlice({ path, key, apiID, content: { ...content, widget: traversedContent }, model, }) } } type TraverseGroupContentWithModelConfig = { model: GroupModel transformWidget: TraverseWidgetContentWithModelFunction } export function traverseGroupContentWithModel( path: contentPath.Path, key: string, apiID: string, content: GroupContent | undefined, config: TraverseGroupContentWithModelConfig, ): GroupContent | undefined { const { model, transformWidget } = config if (!content) { return transformWidget({ path, key, apiID, model, }) } const traversed = traverseGroupItemsContentWithModel(path, content.value, { model: model.config?.fields ?? {}, transformWidget, }) return transformWidget({ path, key, apiID, content: { ...content, value: traversed }, model, }) } type TraverseGroupItemsContentWithModelConfig = { model: Record transformWidget: TraverseWidgetContentWithModelFunction } export function traverseGroupItemsContentWithModel( path: contentPath.Path, content: GroupItemContent[] | undefined, config: TraverseGroupItemsContentWithModelConfig, ): GroupItemContent[] { const { transformWidget } = config if (!content) { return [] } const traversed: GroupItemContent[] = [] for (const groupItemContent of content) { // Legacy v3 documents can have .value as a plain object or string // instead of the expected [key, content][] tuples. if (!Array.isArray(groupItemContent.value)) { continue } const groupItemPath = path.concat({ key: groupItemContent.key, type: "GroupItem" }) const groupItemContentValue = Object.fromEntries(groupItemContent.value) const traversedValue: GroupItemContent["value"] = [] for (const [key, model] of Object.entries(config.model)) { const traversedContent = traverseWidgetContentWithModel( groupItemPath.concat({ key, type: "Widget" }), key, key, groupItemContentValue[key], { model, transformWidget }, ) if ( traversedContent && (isNestableContent(traversedContent) || traversedContent.__TYPE__ === GroupContentType) ) { traversedValue.push([key, traversedContent]) } } traversed.push({ ...groupItemContent, value: traversedValue }) } return traversed } type TraverseRepeatableContentWithModelConfig = { model: LinkModel transformWidget: TraverseWidgetContentWithModelFunction } export function traverseRepeatableContentWithModel( path: contentPath.Path, key: string, apiID: string, content: RepeatableContent | undefined, config: TraverseRepeatableContentWithModelConfig, ): RepeatableContent | undefined { const { model, transformWidget } = config if (!content) { return transformWidget({ path, key, apiID, model, }) } const nonRepeatModel = model.type === "Link" && model.config ? { ...model, config: { ...model.config, repeat: false } } : model const traversed: RepeatableContent["value"] = [] for (let index = 0; index < content.value.length; index++) { const traversedContent = traverseWidgetContentWithModel( path.concat({ key: index.toString(), type: "Widget" }), key, apiID, content.value[index], { model: nonRepeatModel, transformWidget }, ) if (traversedContent && isRepeatableContentValue(traversedContent)) { traversed.push(traversedContent) } } return transformWidget({ path, key, apiID, content: { ...content, value: traversed }, model, }) } type TraverseTableContentWithModelConfig = { model: TableModel transformWidget: TraverseWidgetContentWithModelFunction } export function traverseTableContentWithModel( path: contentPath.Path, key: string, apiID: string, content: TableContent | undefined, config: TraverseTableContentWithModelConfig, ): TableContent | undefined { const { model, transformWidget } = config if (!content) { return transformWidget({ path, key, apiID, model, }) } const traversed: TableContent["content"] = [] for (let rowIndex = 0; rowIndex < content.content.length; rowIndex++) { const traversedContent: TableContent["content"][number]["content"] = [] for (let cellIndex = 0; cellIndex < content.content[rowIndex].content.length; cellIndex++) { const cellPath = path.concat({ key: [rowIndex, cellIndex].join(","), type: "Widget" }) const cell = content.content[rowIndex].content[cellIndex] const transformed = transformWidget({ path: cellPath, key, apiID, content: cell.content, model: TableCellModel, }) if (transformed && transformed.__TYPE__ === RichTextContentType) { traversedContent.push({ ...cell, content: transformed }) } else { traversedContent.push({ ...cell, content: { __TYPE__: RichTextContentType, value: [], }, }) } } traversed.push({ ...content.content[rowIndex], content: traversedContent }) } return transformWidget({ path, key, apiID, content: { ...content, content: traversed }, model, }) } // Internals export type TraverseSliceContentWithModelFunction = (args: { path: contentPath.Path key: string apiID: string content?: TContent model: SliceContentModel }) => TContent | SharedSliceItemContent | undefined export type TraverseWidgetContentWithModelFunction< TContentTransformMode extends "preserve" | "widen" = "preserve", > = (args: { path: contentPath.Path key: string apiID: string content?: TContent model: StaticWidgetModel }) => (TContentTransformMode extends "preserve" ? TContent : WidgetContent) | undefined type TraverseWidgetContentWithModelConfig = { model: StaticWidgetModel transformWidget?: TraverseWidgetContentWithModelFunction transformSlice?: TraverseSliceContentWithModelFunction } function traverseWidgetContentWithModel( path: contentPath.Path, key: string, apiID: string, content: WidgetContent | undefined, config: TraverseWidgetContentWithModelConfig, ): WidgetContent | undefined { const { model, transformWidget = (args) => args.content, transformSlice = (args) => args.content, } = config if (model.type === "Slices" || model.type === "Choice") { const slices = content?.__TYPE__ === SlicesContentType ? content : undefined return traverseSlicesContentWithModel(path, key, apiID, slices, { transformWidget, transformSlice, model, }) } if (model.type === "Group") { const group = content?.__TYPE__ === GroupContentType ? content : undefined return traverseGroupContentWithModel(path, key, apiID, group, { transformWidget, model }) } if (model.type === "Link" && model.config?.repeat) { const repeatable = content?.__TYPE__ === RepeatableContentType ? content : undefined return traverseRepeatableContentWithModel(path, key, apiID, repeatable, { transformWidget, model, }) } if (model.type === "Table") { const table = content?.__TYPE__ === TableContentType ? content : undefined return traverseTableContentWithModel(path, key, apiID, table, { transformWidget, model }) } const nestable = content?.__TYPE__ !== SlicesContentType && content?.__TYPE__ !== GroupContentType && content?.__TYPE__ !== RepeatableContentType && content?.__TYPE__ !== TableContentType ? content : undefined return transformWidget({ path, key, apiID, content: nestable, model }) }