Skip to content

Tool for transforming rich text content from HTML into a JSON structure and Portable Text format.

License

Notifications You must be signed in to change notification settings

kontent-ai/rich-text-resolver-js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

resolverlogo

Kontent.ai rich text resolver

Last modified Issues Contributors MIT License codecov Stack Overflow Discord

This package provides utilities for transforming Kontent.ai rich text into structured formats suitable for resolution and rendering in various environments.

Installation

Install the package via npm

npm i @kontent-ai/rich-text-resolver


Features

API Overview

Module API

Parsing rich text HTML to an array of simplified nodes

The tool provides environment-aware (browser or Node.js) parseHTML function to transform HTML into an array of DomNode trees. Any valid HTML is parsed, including all attributes. Together with built-in transformation methods, this tool is a suitable option for processing HTML and rich text from external sources, to make it compatible with Kontent.ai rich text format. See dedicated HTML transformer docs for further information.

Portable text resolution

Portable Text is a universal standard for rich text representation, with tools available for its transformation and rendering in majority of popular frameworks and languages:

Tip

This module re-exports modified toHTML function and <PortableText> component from to-html and react-portabletext packages respectively. These modified helpers provide default resolution for tags which are either unsupported or only partially supported in the original packages (sub and sup tags, images, tables and links).

Make sure to use these re-exports if you want to take advantage of the default resolution.

The tool provides transformToPortableText function to convert rich text content into an array of Portable Text blocks, with custom blocks defined for Kontent.ai-specific objects.

Combined with a suitable package for the framework of your choice, this makes for an optimal solution for resolving rich text.

Important

The provided Portable Text transformation functions expect a valid Kontent.ai rich text content, otherwise you risk errors or invalid blocks in the resulting array.

Custom portable text blocks

Besides default blocks for common elements, Portable Text supports custom blocks, which can represent other entities. Each custom block should extend ArbitraryTypedObject to ensure _key and _type properties are present. Key should be a unique identifier (e.g. guid), while type should indicate what the block represents. Value of _type property is used for mapping purposes in subsequent resolution.

This package comes with built-in custom block definitions for representing Kontent.ai rich text entities:

Component/linked item – PortableTextComponentOrItem

const portableTextComponent: PortableTextComponentOrItem = {
_type: "componentOrItem",
_key: "guid",
component: {
_type: "reference",
_ref: "linkedItemOrComponentCodename",
referenceType: "codename",
},
dataType: "component"
};

Image – PortableTextImage

const portableTextImage: PortableTextImage = {
_type: "image",
_key: "guid",
asset: {
_type: "reference",
_ref: "bc6f3ce5-935d-4446-82d4-ce77436dd412",
url: "https://assets-us-01.kc-usercontent.com:443/.../image.jpg",
alt: "",
referenceType: "id",
}
};

Item link – PortableTextItemLink

const portableTextItemLink: PortableTextItemLink = {
_type: "contentItemLink",
_key: "guid",
reference: {
_type: "reference",
_ref: "0184a8ac-9781-4292-9e30-1fb56f648a6c",
referenceType: "id",
}
};

Table – PortableTextTable

const portableTextTable: PortableTextTable = {
_type: "table",
_key: "guid",
rows: [
{
_type: "row",
_key: "guid",
cells: [
{
_type: "cell",
_key: "guid",
content: [
{
_type: "block",
_key: "guid",
markDefs: [],
style: "normal",
children: [
{
_type: "span",
_key: "guid",
marks: [],
text: "cell text content",
}
]
}
]

Examples

Plain HTML resolution

HTML resolution using a slightly modified version of toHTML function from @portabletext/to-html package.

import {
  transformToPortableText,
  resolveTable,
  resolveImage,
  PortableTextHtmlResolvers,
  toHTML
} from "@kontent-ai/rich-text-resolver";

const richTextValue = "<rich text html>";
const linkedItems = ["<array of linked items>"]; // e.g. from SDK
const portableText = transformToPortableText(richTextValue);

const resolvers: PortableTextHtmlResolvers = {
  components: {
    types: {
      image: ({ value }) => {
        // helper method for resolving images
        return resolveImage(value); 
      },
      componentOrItem: ({ value }) => {
        const linkedItem = linkedItems.find(
          (item) => item.system.codename === value.component._ref
        );
        switch (linkedItem?.system.type) {
          case "component_type_codename": {
            return `<p>resolved value of text_element: ${linkedItem?.elements.text_element.value}</p>`;
          }
          default: {
            return `Resolver for type ${linkedItem?.system.type} not implemented.`;
          }
        };
      },
      table: ({ value }) => {
        // helper method for resolving tables
        const tableHtml = resolveTable(value, toHTML);
        return tableHtml;
      },
    },
    marks: {
      contentItemLink: ({ children, value }) => {
        return `<a href="https://website.com/${value.reference._ref}">${children}</a>`;
      },
      link: ({ children, value }) => {
        return `<a href=${value?.href} data-new-window=${value?.["data-new-window"]}>${children}</a>`;
      },
    },
  },
};

const resolvedHtml = toHTML(portableText, resolvers);

React resolution

React, using a slightly modified version of PortableText component from @portabletext/react package.

import { toPlainText } from "@portabletext/react";
import {
  PortableTextReactResolvers,
  PortableText,
  TableComponent,
  ImageComponent,
} from "@kontent-ai/rich-text-resolver/utils/react";
import {
  transformToPortableText,
} from "@kontent-ai/rich-text-resolver";

// assumes richTextElement from SDK

const resolvers: PortableTextReactResolvers = {
  types: {
    componentOrItem: ({ value }) => {
      const item = richTextElement.linkedItems.find(item => item.system.codename === value.component._ref);
      return <div>{item?.elements.text_element.value}</div>;
    },
    // Image and Table components are used as a default fallback if a resolver isn't explicitly specified
    table: ({ value }) => <TableComponent {...value} />,
    image: ({ value }) => <ImageComponent {...value} />,
  },
  marks: {
    link: ({ value, children }) => {
      return (
        <a href={value?.href} rel={value?.rel} title={value?.title} data-new-window={value?.['data-new-window']}>
          {children}
        </a>
      )
    },
    contentItemLink: ({ value, children }) => {
      const item = richTextElement.linkedItems.find(item => item.system.id === value?.reference._ref);
      return (
        <a href={"https://website.xyz/" + item?.system.codename}>
          {children}
        </a>
      )
    }
  }
}

const MyComponent = ({ props }) => {
  // https://github.com/portabletext/react-portabletext#customizing-components
  const portableText = transformToPortableText(props.element.value);

  return (
    <PortableText value={portableText} components={resolvers} />
  );
};

Vue resolution

Using @portabletext/vue package

<script setup>
import {
  PortableText,
  PortableTextComponentProps,
  PortableTextComponents,
  toPlainText,
} from "@portabletext/vue";
import {
  resolveTableVue as resolveTable,
  resolveImageVue as resolveImage,
  toVueImageDefault,
} from "@kontent-ai/rich-text-resolver";


const components: PortableTextComponents = {
  types: {
    image: ({ value }: PortableTextComponentProps<PortableTextImage>) =>
      resolveImage(value, h, toVueImageDefault),
    table: ({ value }: PortableTextComponentProps<PortableTextTable>) =>
      resolveTable(value, h, toPlainText),
  },
  // marks etc.
};
</script>

<template>
  <PortableText :value="props.value" :components="components" />
</template>

Modifying portable text nodes

Package exports a traversePortableText method, which accepts an array of PortableTextObject and a callback function. The method recursively traverses all nodes and their subnodes, optionally modifying them with the provided callback:

    import {
      PortableTextObject,
      transformToPortableText,
      traversePortableText,
    } from "@kontent-ai/rich-text-resolver";

    const input = `<figure data-asset-id="guid" data-image-id="guid"><img src="https://asseturl.xyz" data-asset-id="guid" data-image-id="guid" alt=""></figure>`;

    // Adds height parameter to asset reference and changes _type.  
    const processBlocks = (block: PortableTextObject) => {
      if (block._type === "image") {
        const modifiedReference = {
          ...block.asset,
          height: 300
        }
  
        return {
          ...block,
          asset: modifiedReference,
          _type: "modifiedImage"
        }
      }

      // logic for modifying other object types...

        // return original block if no modifications required
        return block;
    }

    const portableText = transformToPortableText(input);
    const modifiedPortableText = traversePortableText(portableText, processBlocks);

MAPI transformation

toManagementApiFormat is a custom transformation method built upon toHTML package, allowing you to restore portable text previously created from management API rich text back into MAPI supported format.

  const richTextContent =
    `<p>Here is an <a data-item-id="12345"><strong>internal link</strong></a> in some text.</p>`;

  const portableText = transformToPortableText(richTextContent);
  
  // your logic to modify the portable text

  const validManagementApiFormat = toManagementApiFormat(portableText);

Important

MAPI transformation logic expects Portable Text that had been previously created from management API rich text and performs only minimal validation. It doesn't provide implicit transformation capabilities from other formats (such as delivery API).

If you're interested in transforming external HTML or rich text to a MAPI compatible format, see HTML transformer docs instead.