View on GitHub

CoreMedia CKEditor 5 Plugins

CoreMedia's CKEditor 5 Plugins provides plugins for CKEditor 5 with focus on integration into CoreMedia CMS.

Download this project as a .zip file Download this project as a tar.gz file

CoreMedia General RichText Support

API Documentation

Module: @coremedia/ckeditor5-coremedia-richtext-support

The General RichText Support (“GRS”) ensures that any valid CoreMedia RichText, especially attributes, may be loaded into CKEditor 5. It does not provide any editing features, but only registers elements, attributes and attribute-values, which are not yet supported by corresponding editing and/or data-processing features.

GRS is based on CKEditor 5’s General HTML Support (“GHS”). GRS shares the same state as GHS, which is, as of now, an experimental state.

Not part of CoreMedia Essentials: This plugin is not part of the CoreMedia Essentials, as it depends on your use-case, if you prefer running CKEditor 5 in CoreMedia CMS with or without it.

Installation

pnpm install @coremedia/ckeditor5-coremedia-richtext-support
import CoreMediaRichTextSupport
  from '@coremedia/ckeditor5-coremedia-richtext/CoreMediaRichTextSupport';

ClassicEditor.create(document.querySelector('.editor'), {
  plugins: [
    CoreMediaRichTextSupport,
    // ...
  ],
});

Do I need to install GRS?

Most likely: Yes.

If all your RichText data are created via CKEditor 5 within CoreMedia CMS: most likely not. You should consider installing GRS, if you answer one of the following question with “yes”:

If not installing GRS, any RichText, which is loaded and edited within CKEditor 5 will get all elements and attributes removed, which cannot be created by the installed CKEditor 5 editing features. This may be a desired behavior, though.

Source Editing

The CKEditor Team recommends enabling the Source Editing feature if using GHS. As GHS is the base technology for GRS, the same recommendation applies here.

Special Note on Content Links and Content Blob Links (Images): Links are represented in a way, so that CoreMedia Studio understands them. Thus, a link to a content with ID 2 is represented as content/2. Similar to that, Content Blob references are expressed as: content/2#properties.data.

Users may be more used to the more verbose Unified API (UAPI) format. For the examples above this would be:

As convenience in Source Editing, both formats are understood. Note though, that when closing Source Editing, the links will be transformed to the CoreMedia Studio format. Thus, when opening Source Editing the next time, you will see content/2 again, for example.

Pitfall: Customizing Default Data-Processing

Short: When you adapt data-processing for new elements and/or attributes to be represented in CoreMedia RichText 1.0, you most likely also have to extend the configuration of General HTML Support, thus, introduce a htmlSupport configuration section.

Architecture: Prior to describing an important pitfall, which is relevant as soon, as you customize data-processing like mapping an element unknown to CoreMedia RichText 1.0 to a known element with some designated class attribute value (such as <mark> to <span class="mark">), let’s rephrase the general architecture overview depicted in CKEditor 5 documentation on the Editing engine:

There is a subtle difference between the data retrieved via editor.getData() and the data transformed to CKEditor model and later to CKEditor view: While editor.getData() will provide CoreMedia RichText 1.0, the internal data view in CKEditor (as used for CoreMedia RichText 1.0 Editing) is represented as HTML (which again is different from the HTML in editing view).

Overview CKEditor Editing Engine & GHS/GRS!

Example:

General RichText/HTML Support is about the Data View: What is important to know, is that any configuration for GRS (and thus GHS) applies to the Data View.

Thus, when a RichText paragraph may have an additional attribute like dir it is insufficient just configuring the allowed attribute for element <p> in GHS: You must also register the same attribute for all elements in Data View, which will be mapped to <p> in Data later on. Thus, we will have to configure GHS similar to this:

{
  htmlSupport: {
    allow: [{
      name: /^(p|h[1-6])$/,
      attributes: {
        dir: /(rtl|ltr)/,
      },
    }],
  },
}

This is, what is done by the GeneralRichTextSupport plugin for all elements pre-configured in RichText Data Processing. But this does not know about any customizations such as introducing new elements.

What to do for custom elements and attributes? Luckily, the GHS configuration is extensible. Thus, a plugin may enrich the configuration, and you may as well decide adding late configuration, which is, when creating the ClassicEditor instance.

Having this, introducing support for a new element such as <mark> from Highlight feature requires two steps, if you want to ensure, that attributes added via Unified API for example are not removed as soon as edited in CKEditor:

  1. Add data-processing to represent <mark> in CoreMedia RichText 1.0.
  2. Extend GHS configuration provided by GRS by the new element.

For the latter, GRS provides a convenience API called RichText Aliases.

If we decide representing <mark> as <span class="mark"> you will require two configuration entries coremedia:richtext and coremedia:richtextSupport:

import CoreMediaRichText
  from "@coremedia/ckeditor5-coremedia-richtext/CoreMediaRichText";
import GeneralRichTextSupport
  from "@coremedia/ckeditor5-coremedia-richtext-support/GeneralRichTextSupport";
import {replaceByElementAndClassBackAndForth}
  from "@coremedia/ckeditor5-coremedia-richtext/rules/ReplaceBy";

/* ... */

ClassicEditor.create(document.querySelector('.editor'), {
  plugins: [
    CoreMediaRichText,
    GeneralRichTextSupport,
    // ...
  ],
  "coremedia:richtext": {
    rules: {
      elements: {
        mark: replaceByElementAndClassBackAndForth("mark", "span", "mark"),
      },
    },
  },
  "coremedia:richtextSupport": {
    aliases: [
      {
        name: "mark",
        inherit: "span",
      }
    ],
  },
});

As you see, this already uses a convenience API provided by the CoreMedia plugins:

The coremedia:richtextSupport configuration will inform the underlying GHS feature, that <mark> may share the same attributes as <span> from CoreMedia RichText 1.0 DTD.

Less Convenient Configuration: A less convenient configuration exposes some details, which may help to understand the processing better. So, if inlining the convenience API from above, the example above will be similar to:

ClassicEditor.create(document.querySelector('.editor'), {
  plugins: [
    CoreMediaRichText,
    GeneralHtmlSupport,
    GeneralRichTextSupport,
    // ...
  ],
  "coremedia:richtext": {
    rules: {
      elements: {
        mark: {
          toData: (params) => {
            const { node } = params;
            node.name = "span";
            node.classList.add("mark");
          },
          toView: {
            span: (params) => {
              const { node } = params;
              if (!node.classList.contains("mark")) {
                // It's another span, we are not responsible.
                return;
              }
              node.classList.remove("mark");
              node.name = "mark";
            },
          },
        },
      },
    },
  },
  "coremedia:richtextSupport": {
    aliases: [
      {
        name: "mark",
        inherit: "span",
      }
    ],
  },
});

Having this example, you may see, how to create a lot more complex scenarios for data-processing and GHS configuration. If you also decided adding an attribute to <mark> element such as <mark data-priority="high">, you also may want to declare this as part of the alias configuration:

{
  "coremedia:richtextSupport": {
    aliases: [
      {
        name: "mark",
        inherit: "span",
        attributes: {
          "data-priority": /^(high|medium|low)$/,
        }
      }
    ],
  },
}

Note, that such an approach is only required/recommended, if you ever expect the corresponding RichText to be loaded in a CKEditor instance, which is unable to handle data-priority attribute. In a default scenario, you would have created a plugin, to edit this attribute, also dealing with up- and downcast.

Side Note: Where to store data-priority in CoreMedia RichText?

CoreMedia RichText 1.0 does not support data attributes. One possible solution is to transform it to some dedicated class, as can be seen below.

Given this data view (HTML):

<mark data-priority="high">Text</mark>
<mark data-priority="low">Text</mark>

we may transform it to:

<span class="mark priority--high">Text</span>
<span class="mark priority--low">Text</span>

Note on Strictness.LEGACY and Disappearing Attributes

GRS implements attribute registration according to mode Strictness.LOOSE. Thus, it strictly follows attributes and their values, which will pass a CoreMedia RichText 1.0 validation.

If using Strictness.LEGACY for Data-Processing, you most likely want to extend possible values for a given attribute. This can be done by adapting the configuration of the underlying General HTML Support (GHS). An explicit configuration as part of the Editor’s configuration will override the configuration as provided by GRS.

Example: You decided to store any string value in xlink:actuate of a link. On the CMS server you either disabled CoreMedia RichText 1.0 validation or you configured a grammar, only similar but different to CoreMedia RichText 1.0.

You configured the CoreMedia Data-Processing with Strictness.LEGACY. Now you need to tell GHS, that xlink:actuate is not only limited to values onLoad or onRequest but accepts any string. To do so, you extend the htmlSupport configuration similar to the one stated above.

All in all, your configuration will look similar to this:

import CoreMediaRichText
  from "@coremedia/ckeditor5-coremedia-richtext/CoreMediaRichText";
import GeneralRichTextSupport
  from "@coremedia/ckeditor5-coremedia-richtext-support/GeneralRichTextSupport";
import {replaceByElementAndClassBackAndForth}
  from "@coremedia/ckeditor5-coremedia-richtext/rules/ReplaceBy";

/* ... */

ClassicEditor.create(document.querySelector('.editor'), {
  plugins: [
    CoreMediaRichText,
    GeneralRichTextSupport,
    // For completeness you may want to add CKEditor's GeneralHtmlSupport in
    // here. But it also comes as transitive dependency of GeneralRichTextSupport.
    // ...
  ],
  "coremedia:richtext": {
    // Requires changes on server side regarding RichText validation.
    strictness: Strictness.LEGACY,
  },
  // Configuration for GeneralHtmlSupport
  htmlSupport: {
    allow: [
      {
        name: "a",
        attributes: {
          // xlink:actuate is mapped to data-xlink-actuate during data-processing.
          // Thus, as we are in data-view, we need to configure allowed values
          // for the mapped value.
          "data-xlink-actuate": true,
        },
      },
    ],
  },
});

The above configuration for element <a> will be merged automatically by GHS with the configuration provided by GRS. Thus, the attribute dir may still be provided as configured by GRS. Just as any other attributes registered by GRS.

Known Issues