import { EmptyContent, GroupContent, isEmptyContent, isGroupContent, isNestableContent, isRepeatableContent, isSlicesContent, isUIDContent, NestableContent, RepeatableContent, UIDContent, WidgetContent, } from "@prismicio/types-internal/lib/content" import { NestableWidget, StaticWidget } from "@prismicio/types-internal/lib/customtypes" import { type LinkResolver, type RelatedDocument, type RenderContext, CustomTypeDef, Fetch } from "./models" import type { DocumentReader } from "./models/ApiDocument/DocumentReader" import { formatDateTimeIsoString } from "./utils" import { StaticWidgetRenderer, UIDRenderer } from "./widgets" import SlicesRenderer from "./widgets/slices/SlicesRenderer" const DEPRECATED_ARRAY_REGEX = /(\w+)\[\d+]/ function isStaticWidgetContent(content: WidgetContent): content is GroupContent | RepeatableContent | NestableContent { return isGroupContent(content) || isRepeatableContent(content) || isNestableContent(content) } function i18nWriter(doc: RelatedDocument): object { const result: object = { id: doc.id, type: doc.typ, lang: doc.language, } if (doc.uid) { return { ...result, uid: doc.uid, } } return result } function renderWidgetV1(ctx: RenderContext) { return (content: WidgetContent, fetchOpt: Fetch.Field | undefined): unknown => { if (isEmptyContent(content)) { return undefined } else if (isUIDContent(content)) { return UIDRenderer.renderV1(content) } else if (isStaticWidgetContent(content) && !EmptyContent.is(content)) { return StaticWidgetRenderer(ctx).renderV1(content, Fetch.extractGroupOrFieldIfAny(fetchOpt)) } else { return SlicesRenderer(ctx).renderV1(content, Fetch.extractSliceFetchIfAny(fetchOpt)) } } } function renderWidgetsV1(ctx: RenderContext) { return (docReader: DocumentReader, content: [string, WidgetContent][], fetch: Fetch.Doc | undefined): unknown => { const renderedContent: Record = {} for (const [key, widgetContent] of content) { if (!isUIDContent(widgetContent)) { const deprecatedArray = key.match(DEPRECATED_ARRAY_REGEX) const isDeprecatedArray = deprecatedArray && deprecatedArray[1] // we cast to string since the TS compiler doesn't understand than if `deprecatedArray` exists, the regexp matched so `deprecatedArray[1]` is defined. const newKey = (isDeprecatedArray ? deprecatedArray[1] : key) as string const maybeContent: undefined | unknown = isDeprecatedArray ? renderedContent[newKey] : undefined if (fetch !== undefined) { const field = fetch.fields[key] if (field !== undefined) { if (isDeprecatedArray) { renderedContent[newKey] = Array.isArray(maybeContent) ? [...(maybeContent as unknown[]), renderWidgetV1(ctx)(widgetContent, field)] : [renderWidgetV1(ctx)(widgetContent, field)] } else { renderedContent[newKey] = renderWidgetV1(ctx)(widgetContent, field) } } } else { if (isDeprecatedArray) { renderedContent[newKey] = Array.isArray(maybeContent) ? [...(maybeContent as unknown[]), renderWidgetV1(ctx)(widgetContent, undefined)] : [renderWidgetV1(ctx)(widgetContent, undefined)] } else { renderedContent[newKey] = renderWidgetV1(ctx)(widgetContent, undefined) } } } } return { [docReader.type]: renderedContent } } } function renderV1(ctx: RenderContext) { return ( docReader: DocumentReader, content: [string, WidgetContent][], masterLang: string, searchURL: string, i18n: { [k: string]: RelatedDocument[] }, withMeta: boolean, brokenRoute?: string, linkResolver?: LinkResolver, fetchDoc?: Fetch.Doc, ) => { const relatedDocs = (i18n[docReader.groupLangId] || []) .filter((relatedDocument) => relatedDocument.id !== docReader.id) .map((relatedDocument) => i18nWriter(relatedDocument)) const widgets = renderWidgetsV1(ctx)(docReader, content, fetchDoc) return { id: docReader.id, uid: docReader.uid ?? null, url: ctx.LinkResolver.buildUrl({ linkResolver: linkResolver, pageType: docReader.type, masterLang, brokenRoute, docReader, }) ?? null, type: docReader.type, href: ctx.urlRewriter.enforceCDN(searchURL), tags: docReader.tags, first_publication_date: docReader.first_publication_date ? formatDateTimeIsoString(docReader.first_publication_date) : null, last_publication_date: docReader.last_publication_date ? formatDateTimeIsoString(docReader.last_publication_date) : null, slugs: docReader.slugs, linked_documents: [], lang: docReader.language, alternate_languages: relatedDocs, data: widgets, ...(withMeta ? docReader.metadata : {}), } } } function renderWidgetV2(ctx: RenderContext) { return (mask: StaticWidget, content: WidgetContent, fetchOpt: Fetch.Field | undefined): unknown => { if (isSlicesContent(content) && mask.type === "Slices") { return SlicesRenderer(ctx).renderV2(mask, content, Fetch.extractSliceFetchIfAny(fetchOpt)) } else if (isGroupContent(content) && mask.type === "Group") { return StaticWidgetRenderer(ctx).renderV2(mask, content, Fetch.extractGroupOrFieldIfAny(fetchOpt)) } else if (isUIDContent(content) && mask.type === "UID") { return UIDRenderer.renderV2(mask, content) } else if (isNestableContent(content) && NestableWidget.is(mask)) { return StaticWidgetRenderer(ctx).renderV2(mask, content, Fetch.extractGroupOrFieldIfAny(fetchOpt)) } else { return renderDefaultWidget(ctx)(mask) } } } function renderV2(ctx: RenderContext) { return ( mask: CustomTypeDef, docReader: DocumentReader, masterLang: string, content: [string, WidgetContent][], searchURL: string, i18n: { [k: string]: RelatedDocument[] }, withMeta: boolean, brokenRoute?: string, linkResolver?: LinkResolver, fetchDoc?: Fetch.Doc, ) => { const widgets = Object.entries(mask.fields) .map<[string, unknown, boolean] | undefined>(([apiId, widgetDef]) => { if (widgetDef.type === "UID") return const correspondingContent = content.find(([id]) => id === apiId) const fetchField = fetchDoc?.fields[apiId] const deprecatedArray = apiId.match(DEPRECATED_ARRAY_REGEX) const isDeprecatedArray = Boolean(deprecatedArray && deprecatedArray[1]) const newKey = (isDeprecatedArray ? (deprecatedArray as RegExpExecArray)[1] : apiId) as string if (correspondingContent) { const [, contentValue] = correspondingContent if (isUIDContent(contentValue)) return if (fetchDoc === undefined || fetchField !== undefined) { return [newKey, renderWidgetV2(ctx)(widgetDef, contentValue, fetchField), isDeprecatedArray] } else { return undefined } } else { //TODO: not sure about that if (fetchDoc === undefined || fetchField !== undefined) { return [newKey, renderDefaultWidget(ctx)(widgetDef), isDeprecatedArray] } else { return undefined } } }) .reduce<{ [_: string]: unknown }>((acc, maybeWidget) => { if (maybeWidget) { const [key, widget, isDeprecatedArray] = maybeWidget if (isDeprecatedArray) { const maybeContent: unknown = acc[key] if (maybeContent) { if (Array.isArray(maybeContent)) { return { ...acc, [key]: [...(maybeContent as unknown[]), widget], } } else { throw new Error(`[UNEXPECTED ERROR] ${JSON.stringify(maybeContent)} should be an Array`) } } return { ...acc, [key]: [widget], } } return { ...acc, [key]: widget, } } return acc }, {}) const relatedDocs = (i18n[docReader.groupLangId] || []) .filter((relatedDocument) => relatedDocument.id !== docReader.id) .map((relatedDocument) => i18nWriter(relatedDocument)) return { id: docReader.id, uid: docReader.uid ?? null, url: ctx.LinkResolver.buildUrl({ linkResolver: linkResolver, pageType: docReader.type, masterLang, brokenRoute, docReader, }) ?? null, type: docReader.type, href: ctx.urlRewriter.enforceCDN(searchURL), tags: docReader.tags, first_publication_date: docReader.first_publication_date ? formatDateTimeIsoString(docReader.first_publication_date) : null, last_publication_date: docReader.last_publication_date ? formatDateTimeIsoString(docReader.last_publication_date) : null, slugs: docReader.slugs, linked_documents: [], lang: docReader.language, alternate_languages: relatedDocs, data: widgets, ...(withMeta ? docReader.metadata : {}), } } } function renderWidgetMocks(ctx: RenderContext) { return (mask: StaticWidget, content: WidgetContent): unknown => { if (content.__TYPE__ === "SliceContentType" && mask.type === "Slices") { return SlicesRenderer(ctx).renderMocks(mask, content) } else if (isGroupContent(content) && mask.type === "Group") { return StaticWidgetRenderer(ctx).renderMocks(mask, content) } else if (isNestableContent(content) && NestableWidget.is(mask)) { return StaticWidgetRenderer(ctx).renderMocks(mask, content) } else { return renderDefaultWidget(ctx)(mask) } } } function renderMocks(ctx: RenderContext) { return (mask: CustomTypeDef, content: [string, WidgetContent][]) => { const uid = content.reduce((_acc, [, widget]) => { if (UIDContent.is(widget)) return widget.value return }, undefined) const widgets = Object.entries(mask.fields) .map<[string, unknown, boolean] | undefined>(([apiId, widgetDef]) => { if (widgetDef.type === "UID") return const correspondingContent = content.find(([id]) => id === apiId) const deprecatedArray = apiId.match(DEPRECATED_ARRAY_REGEX) const isDeprecatedArray = Boolean(deprecatedArray && deprecatedArray[1]) const newKey = (isDeprecatedArray ? (deprecatedArray as RegExpExecArray)[1] : apiId) as string if (correspondingContent) { return [newKey, renderWidgetMocks(ctx)(widgetDef, correspondingContent[1]), isDeprecatedArray] } else { return [newKey, renderDefaultWidget(ctx)(widgetDef), isDeprecatedArray] } }) .reduce<{ [_: string]: unknown }>((acc, maybeWidget) => { if (maybeWidget) { const [key, widget, isDeprecatedArray] = maybeWidget if (isDeprecatedArray) { const maybeContent: unknown = acc[key] if (maybeContent) { if (Array.isArray(maybeContent)) { return { ...acc, [key]: [...(maybeContent as unknown[]), widget], } } else { throw new Error(`[UNEXPECTED ERROR] ${JSON.stringify(maybeContent)} should be an Array`) } } return { ...acc, [key]: [widget], } } return { ...acc, [key]: widget, } } return acc }, {}) return { id: "mock-doc-id", uid, url: uid ? `/${uid}` : null, type: mask.customTypeId, href: null, tags: [], first_publication_date: "1970-01-01T00:00:01+0000", last_publication_date: "1970-01-01T00:00:01+0000", slugs: [], linked_documents: [], lang: "en-us", alternate_languages: [], data: widgets, } } } function renderDefaultWidget(ctx: RenderContext) { return (widgetDef: StaticWidget) => { switch (widgetDef.type) { case "Choice": case "Slices": return SlicesRenderer(ctx).renderDefault(widgetDef) case "UID": return default: return StaticWidgetRenderer(ctx).renderDefault(widgetDef) } } } const DocumentRenderer = (ctx: RenderContext) => { return { renderV1: renderV1(ctx), renderWidgetV1: renderWidgetV1(ctx), renderWidgetV2: renderWidgetV2(ctx), renderV2: renderV2(ctx), renderMocks: renderMocks(ctx), renderDefaultWidget: renderDefaultWidget(ctx), } } export default DocumentRenderer