import { enumHelper } from '@kentico/kontent-core' ; import { deliveryUrlHelper } from '../utilities' ; import { IDeliveryClientConfig } from '../config' ; import { Contracts } from '../contracts' ; import { ElementModels , Elements , ElementType } from '../elements' ; import { IContentItem , IContentItemsContainer , IMapElementsResult , ILink , IRichTextImage , IContentItemWithRawDataContainer , IContentItemWithRawElements } from '../models' ; interface IRichTextImageUrlRecord { originalUrl : string ; newUrl : string ; } export class ElementMapper { constructor ( private readonly config ) { } mapElements < TContentItem extends IContentItem = IContentItem > ( data ) { const processedItem = data . processedItems [ data . dataToMap . item . system . codename ] as TContentItem | undefined ; if ( processedItem ) { return { item : processedItem , processedItems : data . processedItems , preparedItems : data . preparedItems , processingStartedForCodenames : data . processingStartedForCodenames } ; } const preparedItem = data . preparedItems [ data . dataToMap . item . system . codename ] ; const itemInstance = preparedItem ?. item as TContentItem ; if ( ! itemInstance ) { return undefined ; } const elementCodenames = Object . getOwnPropertyNames ( data . dataToMap . rawItem . elements ) ; for ( const elementCodename of elementCodenames ) { const elementMap = this . resolveElementMap ( itemInstance , elementCodename ) ; const elementWrapper = { system : data . dataToMap . item . system , rawElement : data . dataToMap . rawItem . elements [ elementCodename ] , element : elementMap . resolvedName } ; if ( elementMap . shouldMapElement ) { const mappedElement = this . mapElement ( { elementWrapper : elementWrapper , item : itemInstance , preparedItems : data . preparedItems , processingStartedForCodenames : data . processingStartedForCodenames , processedItems : data . processedItems } ) ; itemInstance . elements [ elementMap . resolvedName ] = mappedElement ; } } return { item : itemInstance , processedItems : data . processedItems , preparedItems : data . preparedItems , processingStartedForCodenames : data . processingStartedForCodenames } ; } private mapElement ( data ) { const elementType = enumHelper . getEnumFromValue < ElementType > ( ElementType , data . elementWrapper . rawElement . type ) ; if ( elementType ) { if ( elementType === ElementType . ModularContent ) { return this . mapLinkedItemsElement ( { elementWrapper : data . elementWrapper , preparedItems : data . preparedItems , processingStartedForCodenames : data . processingStartedForCodenames , processedItems : data . processedItems } ) ; } if ( elementType === ElementType . Text ) { return this . mapTextElement ( data . elementWrapper ) ; } if ( elementType === ElementType . Asset ) { return this . mapAssetsElement ( data . elementWrapper ) ; } if ( elementType === ElementType . Number ) { return this . mapNumberElement ( data . elementWrapper ) ; } if ( elementType === ElementType . MultipleChoice ) { return this . mapMultipleChoiceElement ( data . elementWrapper ) ; } if ( elementType === ElementType . DateTime ) { return this . mapDateTimeElement ( data . elementWrapper ) ; } if ( elementType === ElementType . RichText ) { return this . mapRichTextElement ( data . elementWrapper , data . processedItems , data . processingStartedForCodenames , data . preparedItems ) ; } if ( elementType === ElementType . UrlSlug ) { return this . mapUrlSlugElement ( data . elementWrapper ) ; } if ( elementType === ElementType . Taxonomy ) { return this . mapTaxonomyElement ( data . elementWrapper ) ; } if ( elementType === ElementType . Custom ) { return this . mapCustomElement ( data . elementWrapper ) ; } } console . warn ( `Could not map element '${ data . elementWrapper . rawElement . name }' of type '${ data . elementWrapper . rawElement . type }'. Returning unknown element instead.` ) ; return this . mapUnknowElement ( data . elementWrapper ) ; } private mapRichTextElement ( elementWrapper , processedItems , processingStartedForCodenames , preparedItems ) { const richTextLinkedItems = [ ] ; const rawElement = elementWrapper . rawElement as Contracts . IRichTextElementContract ; for ( const codename of rawElement . modular_content ) { const preparedData = preparedItems [ codename ] ; if ( this . canMapLinkedItems ( ) ) { const existingLinkedItem = this . getOrSaveLinkedItemForElement ( codename , rawElement , processedItems , processingStartedForCodenames , preparedItems ) ; if ( existingLinkedItem ) { richTextLinkedItems . push ( existingLinkedItem ) ; } else { if ( preparedData ) { const mappedLinkedItemResult = this . mapElements ( { dataToMap : preparedData , preparedItems : preparedItems , processingStartedForCodenames : processingStartedForCodenames , processedItems : processedItems } ) ; if ( mappedLinkedItemResult ) { richTextLinkedItems . push ( mappedLinkedItemResult . item ) ; } } } } } const richTextImagesResult = this . getRichTextImages ( rawElement . images ) ; const links = this . mapRichTextLinks ( rawElement . links ) ; const images = richTextImagesResult . richTextImages ; const richTextHtml = this . getRichTextHtml ( rawElement . value , richTextImagesResult . imageUrlRecords ) ; return { images : images , linkedItemCodenames : rawElement . modular_content , links : links , name : rawElement . name , type : ElementType . RichText , value : richTextHtml } ; } private mapDateTimeElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . DateTime , ( ) => elementWrapper . rawElement . value ) ; } private mapMultipleChoiceElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . MultipleChoice , ( ) => elementWrapper . rawElement . value ) ; } private mapNumberElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . Number , ( ) => { if ( elementWrapper . rawElement . value === 0 ) { return 0 ; } else if ( elementWrapper . rawElement . value ) { return + elementWrapper . rawElement . value ; } return null ; } ) ; } private mapTextElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . Text , ( ) => elementWrapper . rawElement . value ) ; } private mapAssetsElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . Asset , ( ) => { const assetContracts = elementWrapper . rawElement . value as Contracts . IAssetContract [ ] ; const assets = [ ] ; for ( const assetContract of assetContracts ) { let renditions = null ; const assetUrl = this . config . assetsDomain ? deliveryUrlHelper . replaceAssetDomain ( assetContract . url , this . config . assetsDomain ) : assetContract . url ; if ( assetContract . renditions ) { renditions = { } ; for ( const renditionKey of Object . keys ( assetContract . renditions ) ) { const rendition = assetContract . renditions [ renditionKey ] ; renditions [ renditionKey ] = { ... rendition , url : `${ assetUrl }?${ rendition . query }` } ; } } const asset = { ... assetContract , url : assetUrl , renditions } ; assets . push ( asset ) ; } return assets ; } ) ; } private mapTaxonomyElement ( elementWrapper ) { return { ... this . buildElement ( elementWrapper , ElementType . Taxonomy , ( ) => elementWrapper . rawElement . value ) , taxonomyGroup : elementWrapper . rawElement . taxonomy_group ?? null } ; } private mapUnknowElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . Unknown , ( ) => elementWrapper . rawElement . value ) ; } private mapCustomElement ( elementWrapper ) { if ( this . config . elementResolver ) { const elementResolverValue = this . config . elementResolver ( elementWrapper ) ; if ( elementResolverValue ) { return this . buildElement ( elementWrapper , ElementType . Custom , ( ) => elementResolverValue ) ; } } return this . buildElement ( elementWrapper , ElementType . Custom , ( ) => elementWrapper . rawElement . value ) ; } private mapUrlSlugElement ( elementWrapper ) { return this . buildElement ( elementWrapper , ElementType . UrlSlug , ( ) => elementWrapper . rawElement . value ) ; } private mapLinkedItemsElement ( data ) { const linkedItems = [ ] ; const linkedItemCodenames = data . elementWrapper . rawElement . value as string [ ] ; for ( const codename of linkedItemCodenames ) { if ( this . canMapLinkedItems ( ) ) { const linkedItem = this . getOrSaveLinkedItemForElement ( codename , data . elementWrapper . rawElement , data . processedItems , data . processingStartedForCodenames , data . preparedItems ) ; if ( linkedItem ) { linkedItems . push ( linkedItem ) ; } } } return { ... this . buildElement ( data . elementWrapper , ElementType . ModularContent , ( ) => linkedItemCodenames ) , linkedItems : linkedItems } ; } private getOrSaveLinkedItemForElement ( codename , element , processedItems , mappingStartedForCodenames , preparedItems ) { const processedItem = processedItems [ codename ] ; if ( processedItem ) { return processedItem ; } const preparedItem = preparedItems [ codename ] ; if ( mappingStartedForCodenames . includes ( codename ) ) { return preparedItem ?. item ; } mappingStartedForCodenames . push ( codename ) ; if ( ! preparedItem ) { return undefined ; } let mappedLinkedItem ; const mappedLinkedItemResult = this . mapElements ( { dataToMap : preparedItem , preparedItems : preparedItems , processingStartedForCodenames : mappingStartedForCodenames , processedItems : processedItems } ) ; if ( mappedLinkedItemResult ) { mappedLinkedItem = mappedLinkedItemResult . item ; processedItems [ codename ] = mappedLinkedItem ; } return mappedLinkedItem ; } private mapRichTextLinks ( linksJson ) { const links = [ ] ; for ( const linkId of Object . keys ( linksJson ) ) { const linkRaw = linksJson [ linkId ] ; links . push ( { codename : linkRaw . codename , linkId : linkId , urlSlug : linkRaw . url_slug , type : linkRaw . type } ) ; } return links ; } private getRichTextHtml ( richTextHtml , richTextImageRecords ) { for ( const richTextImageRecord of richTextImageRecords ) { if ( richTextImageRecord . newUrl !== richTextImageRecord . originalUrl ) { richTextHtml = richTextHtml . replace ( new RegExp ( richTextImageRecord . originalUrl , 'g' ) , richTextImageRecord . newUrl ) ; } } return richTextHtml ; } private getRichTextImages ( imagesJson ) { const images = [ ] ; const imageUrlRecords = [ ] ; for ( const imageId of Object . keys ( imagesJson ) ) { const imageRaw = imagesJson [ imageId ] ; const imageUrl = this . config . assetsDomain ? deliveryUrlHelper . replaceAssetDomain ( imageRaw . url , this . config . assetsDomain ) : imageRaw . url ; images . push ( { description : imageRaw . description ?? null , imageId : imageRaw . image_id , url : imageUrl , height : imageRaw . height ?? null , width : imageRaw . width ?? null } ) ; imageUrlRecords . push ( { originalUrl : imageRaw . url , newUrl : imageUrl } ) ; } return { imageUrlRecords : imageUrlRecords , richTextImages : images } ; } private resolveElementMap ( item , originalElementCodename ) { let resolvedElementPropertyName = undefined ; if ( this . config . propertyNameResolver ) { resolvedElementPropertyName = this . config . propertyNameResolver ( item . system . type , originalElementCodename ) ; } if ( ! resolvedElementPropertyName ) { resolvedElementPropertyName = originalElementCodename ; } return { resolvedName : resolvedElementPropertyName , shouldMapElement : true } ; } private buildElement < TValue > ( elementWrapper , type , valueFactory ) { return { name : elementWrapper . rawElement . name , type : type , value : valueFactory ( ) } ; } private canMapLinkedItems ( ) { if ( ! this . config . linkedItemsReferenceHandler ) { return true ; } return this . config . linkedItemsReferenceHandler === 'map' ; } }