# ProseMirror Schema

The [Kendo UI for Angular Editor](https://www.telerik.com/kendo-angular-ui/components/editor) component is based on the [ProseMirror library](https://prosemirror.net/). ProseMirror provides a set of tools and concepts for building rich text editors, using user interface inspired by what-you-see-is-what-you-get.

## Concepts

ProseMirror defines its own data structure, the [`Node`](https://prosemirror.net/docs/ref/#model.Node), to represent content documents. The [ProseMirror document](https://prosemirror.net/docs/guide/#doc) is a tree-like structure comprised of nodes. Therefore, a document is an instance of `Node` with children that are also instances of `Node`.

Each ProseMirror `document` conforms to a specific [`schema`](https://prosemirror.net/docs/guide/#schema). Document schemas allow you to edit documents with a custom structure without writing your own editor from scratch.

The `schema` describes all `nodes` that may occur in the document, the way they are nested, and any [marks](https://prosemirror.net/docs/ref/#model.Mark) applied to them. A `mark` is a piece of information that can be attached to a `node`, such as it being emphasized, in code font, or a link. It has a type and optionally a set of attributes that provide further information (such as the target of the link).

For more details, refer to the [ProseMirror schema guide](https://prosemirror.net/docs/guide/#schema).

## Editor Schema

The Editor uses a built-in [`schema`](https://prosemirror.net/docs/ref/#model.Schema) with predefined [`nodes`](https://prosemirror.net/docs/ref/#model.Node) and [`marks`](https://prosemirror.net/docs/ref/#model.Mark), which describes some of the most common HTML tags and their basic attributes like `id`, `class`, and `style`.

To modify existing `nodes` and `marks` or define new ones, you can customize the default `schema` or create a new one that best fits your needs.

Creating a custom Editor schema enables you to:

* Define new types of `nodes` and `marks`.
* Add new attributes to existing `nodes` and `marks`.
* Remove existing `nodes` and `marks`.

To create a custom `schema` object, use one of the following approaches:

* [Modify the default schema](https://www.telerik.com/kendo-angular-ui/components/editor/schema#modifying-the-default-schema)
* [Create a new schema](https://www.telerik.com/kendo-angular-ui/components/editor/schema#creating-a-new-schema)

To replace the default schema with the custom one, bind the [Editor `schema`](https://www.telerik.com/kendo-angular-ui/components/editor/api/editorcomponent#schema) option to the new `schema` object.

> You can set a custom schema only once—upon the initialization of the Editor component. Further changes to the object to which the `schema` input is bound will not be reflected, and the Editor will continue to use the initially provided schema or, if no schema is initially provided, the built-in one.

### Modifying the Default Schema

The following example demonstrates how to:

* Add a new node to the `nodes` object of the schema.
* Add a new mark to the `marks` object of the schema.
* Add a new attribute to an existing node.

[Demo Source](https://demos.telerik.com/kendo-angular-ui/demos/editor/custom-schema/modify-schema/source.txt)

The following step-by-step guide shows how to recreate the above example by enhancing the built-in [`schema`](https://prosemirror.net/docs/ref/#model.Schema) through adding definitions for HTML tags and attributes which are not available out-of-the-box. The demonstrated approach relies on importing the default `schema`, and modifying its [`nodes`](https://prosemirror.net/docs/ref/#model.NodeSpec) and [`marks`](https://prosemirror.net/docs/ref/#model.MarkSpec) collections. The `schema` can store `nodes` and `marks` in a custom data type, [`OrderedMap`](https://github.com/marijnh/orderedmap#readme). The `OrderedMap` API exposes utility methods for accessing specific items and updating the collection.

1. Import `schema` (the default Editor `schema` object) and `Schema` (the [ProseMirror `Schema` class](https://prosemirror.net/docs/ref/#model.Schema)) from the `@progress/kendo-angular-editor` package:

   ```ts
       import { schema, Schema } from '@progress/kendo-angular-editor';
   ```

2. Customize the existing nodes by using the relevant properties and methods from the [`NodeSpec`](https://prosemirror.net/docs/ref/#model.NodeSpec) and [`OrderedMap`](https://github.com/marijnh/orderedmap#methods) APIs. For example, to add a new `dir` attribute to the existing `paragraph` node, follow the steps:

   1. Use the [`OrderedMap`](https://github.com/marijnh/orderedmap#methods) `get` method to access the `paragraph` node.

      ```ts
          const paragraph = { ...schema.spec.nodes.get('paragraph') };
      ```

   2. Use the [`NodeSpec attrs`](https://prosemirror.net/docs/ref/#model.NodeSpec.attrs) property to define the new attribute.

      ```ts
          paragraph.attrs['dir'] = { default: null };
      ```

   3. Use the [`OrderedMap`](https://github.com/marijnh/orderedmap#methods) `update` method to replace the existing `paragraph` node with the customized one.

      ```ts
          let nodes = schema.spec.nodes.update('paragraph', paragraph);
      ```

3. Create new nodes and add them to the existing `nodes` collection. For example, to provide support for the `<iframe>` tag, create and add an `iframe` node:

   1. Define the `iframe` node as a configuration object which complies with the [`NodeSpec`](http://prosemirror.net/docs/ref/#model.NodeSpec) interface.

      ```ts
          const iframe = {
              attrs: {
                  src: { default: null },
                  style: { default: null }
              },
              group: 'block',
              // Iframes remain non-selectable within Editor content.
              selectable: false,
              // Keep parsed attributes aligned with iframe output.
              parseDOM: [ {
                  tag: 'iframe',
                  getAttrs: (dom) => ({
                      src: dom.getAttribute('src'),
                      style: dom.getAttribute('style')
                  })
              } ],
              toDOM: node => {
                  const attrs = {
                      src: node.attrs.src,
                      style: node.attrs.style,
                      frameborder: '0',
                      allowfullscreen: 'true'
                  };
                  return [ 'iframe', attrs ];
              }
          };
      ```

   2. Append the new `node` to the `nodes` collection by using the [`OrderedMap`](https://github.com/marijnh/orderedmap#methods) `addToEnd` method.

      ```ts
          nodes = schema.spec.nodes.addToEnd('iframe', iframe);
      ```

4. Create new marks and add them to the existing `marks` collection. For example, to provide support for the `<s>` tag, create and add an `s` mark:

   1. Define the mark as a configuration object, which complies with the [`MarkSpec`](https://prosemirror.net/docs/ref/#model.MarkSpec) interface. Optionally, use a helper function that generates tags by a predefined tag name, passed as an argument.

      ```ts
          // Keep parsing and serialization aligned for generated marks.
          const tagMark = tag => ({
              [tag]: {
                  name: tag,
                  // Keep generated marks active at cursor boundaries.
                  inclusive: true,
                  parseDOM: [ { tag } ],
                  toDOM: () => [ tag, 0 ]
              }
          });
          const mark = tagMark('s');
      ```

   2. Append the new `mark` to the `marks` collection by using the [`OrderedMap`](https://github.com/marijnh/orderedmap#methods) `append` method.

      ```ts
          let marks = schema.spec.marks.append(mark);
      ```

5. Use the [`Schema`](https://prosemirror.net/docs/ref/#model.Schema) constructor to create the customized schema with the modified and newly added `nodes` and `marks`.

   ```ts
       public mySchema = new Schema({
           marks,
           nodes
       });
   ```

6. Bind the [Editor `schema`](https://www.telerik.com/kendo-angular-ui/components/editor/api/editorcomponent#schema) input to the new schema.

   ```ts
       <kendo-editor [schema]="mySchema" ...></kendo-editor>

       public mySchema = new Schema({
           marks,
           nodes
       });
   ```

### Creating a New Schema

The following example demonstrates how to create a custom Editor schema.

[Demo Source](https://demos.telerik.com/kendo-angular-ui/demos/editor/custom-schema/create-schema/source.txt)

The following step-by-step guide shows how to create a new [`schema`](https://prosemirror.net/docs/guide/#schema) by configuring all [`nodes`](https://prosemirror.net/docs/ref/#model.Node) and [`marks`](https://prosemirror.net/docs/ref/#model.Mark) which the [document](https://prosemirror.net/docs/guide/#doc) may contain. The demonstrated approach relies on creating new `nodes` and `marks` objects, and passing them to the [`Schema` class](https://prosemirror.net/docs/ref/#model.Schema) constructor.

1. Define all `marks` and their respective attributes so that they comply with the [ProseMirror `MarkSpec` interface](https://prosemirror.net/docs/ref/#model.MarkSpec). Optionally, use a helper function that generates tags by a predefined tag name, passed as an argument.

   ```ts
       // Keep parsing and serialization aligned for generated marks.
       const tagMark = tag => {
           return {
               [tag]: {
                   name: tag,
                   // Keep generated marks active at cursor boundaries.
                   inclusive: true,
                   parseDOM: [ { tag: tag } ],
                   toDOM: () => [ tag, 0 ]
               }
           };
       };

       const marks = {
           link: {
               attrs: {
                   ...commonAttributes(),
                   href: { default: null },
                   target: { default: null },
                   title: { default: null }
               },
               // Do not extend links when the cursor reaches their boundary.
               inclusive: false,
               parseDOM: [ { tag: 'a', getAttrs: getAttributes } ],
               toDOM(node) { return [ 'a', getAttrs(node.attrs), 0 ]; }
           },

           ...tagMark('strong'),
           ...tagMark('b'),
           ...tagMark('em'),
           ...tagMark('i'),
           ...tagMark('u'),
           ...tagMark('del'),
           ...tagMark('sub'),
           ...tagMark('sup'),
           ...tagMark('code'),

           style: {
               attrs: {
                   ...commonAttributes()
               },
               parseDOM: [ {
                   tag: 'span',
                   getAttrs: getAttributes
               } ],
               // Omit attributes when the span has no custom styling.
               toDOM: node => (hasAttrs(node.attrs) ?
                   [ 'span', getAttrs(node.attrs), 0 ] : [ 'span', 0 ])
           }
       };
   ```

2. Define all `nodes` and their respective attributes so that they comply with the [ProseMirror `NodeSpec` interface](http://prosemirror.net/docs/ref/#model.NodeSpec).

   ```ts
       const nodes = {
           // Require the document to contain at least one block.
           doc: {
               content: 'block+'
           },

           // Preserve custom attributes when converting between HTML and ProseMirror nodes.
           paragraph: {
               content: 'inline*',
               group: 'block',
               attrs: {
                   ...commonAttributes()
               },
               parseDOM: [ {
                   tag: 'p',
                   getAttrs: getAttributes
               } ],
               toDOM: node => hasAttrs(node.attrs) ? [ 'p', getAttrs(node.attrs), 0 ] : [ 'p', 0 ]
           },

           div: {
               content: 'block*',
               group: 'block',
               attrs: {
                   ...commonAttributes()
               },
               parseDOM: [ {
                   tag: 'div',
                   getAttrs: getAttributes
               } ],
               toDOM: node => hasAttrs(node.attrs) ? [ 'div', getAttrs(node.attrs), 0 ] : [ 'div', 0 ]
           },
           // TODO: define every node type that the document supports.
       }
   ```

3. Use the `Schema` constructor to create the new schema with the `nodes` and `marks` defined in the previous steps.

   ```ts
       import { Schema } from '@progress/kendo-angular-editor';

       export const mySchema = new Schema({ nodes, marks });
   ```

4. Bind the [Editor `schema`](https://www.telerik.com/kendo-angular-ui/components/editor/api/editorcomponent#schema) input to the newly created schema.

   ```ts
       import { mySchema } from './custom-schema';

       <kendo-editor [schema]="mySchema" ...></kendo-editor>

       public mySchema = mySchema;
   ```

## Custom Elements in the Editor Schema

You can create domain-specific document elements in the Kendo UI for Angular Editor by extending the ProseMirror `schema`. This allows you to insert and manipulate custom elements, such as specialized dynamic placeholders, or even unique entities like weather nodes.

> This implementation is inspired by the [ProseMirror Dino example](https://prosemirror.net/examples/dino/), which demonstrates how to create custom document elements by extending the schema with site-specific elements.

1. Define the supported types of values for the custom element.

   ```ts
       const weatherTypes = ['cloudy', 'sunny', 'rainy'];
   ```

2. Define the custom node specification (`NodeSpec`) for the weather element.

   ```ts
       const getWeatherImage = (type: string): string => {
           switch (type) {
               case 'cloudy':
                   return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/cloudy.png';
               case 'sunny':
                   return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/sunny.png';
               case 'rainy':
                   return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/rainy.png';
               default:
                   return 'https://demos.telerik.com/kendo-angular-ui/assets/layout/tabstrip/cloudy.png';
           }
       };

       export const weatherNodeSpec: NodeSpec = {
           attrs: { type: { default: 'cloudy' } },
           // Allow weather nodes within text content.
           inline: true,
           group: 'inline',
           // Let users move weather nodes within Editor content.
           draggable: true,

           toDOM: (node) => [
               'img',
               {
                   // Preserve the weather type when parsing serialized HTML.
                   'weather-type': node.attrs.type,
                   src: getWeatherImage(node.attrs.type),
                   title: node.attrs.type,
                   class: 'weather-icon',
                   style: 'width: 20px; height: 20px; vertical-align: middle;',
               },
           ],

           parseDOM: [
               {
                   tag: 'img[weather-type]',
                   getAttrs: (dom) => {
                       let type = dom.getAttribute('weather-type');
                       // Reject weather types outside the supported set.
                       return weatherTypes.indexOf(type) > -1 ? { type } : false;
                   },
               },
           ],
       };
   ```

3. Extend the default schema with the custom node specification.

   ```ts
       import { Schema, schema } from '@progress/kendo-angular-editor';
       import { weatherNodeSpec } from './new-node';

       // Keep weather before image in the default node order.
       const nodes = schema.spec.nodes.addBefore('image', 'weather', weatherNodeSpec);

       const marks = schema.spec.marks;

       export const mySchema = new Schema({
           nodes,
           marks,
       });
   ```

4. Load the custom schema in the Editor component and test it with some sample weather elements.

   ```ts
       import { Component } from '@angular/core';
       import { KENDO_EDITOR, Schema } from '@progress/kendo-angular-editor';
       import { KENDO_TOOLBAR } from '@progress/kendo-angular-toolbar';

       import { mySchema } from './custom-schema';

       @Component({
           selector: 'my-app',
           standalone: true,
           imports: [KENDO_EDITOR, KENDO_TOOLBAR],
           template: `
               <kendo-editor [value]="value" [schema]="mySchema">
               </kendo-editor>
           `,
       })
       export class AppComponent {
           public value = `
           <img class="weather-icon" weather-type="sunny">
           <img class="weather-icon" weather-type="cloudy">
           <img class="weather-icon" weather-type="rainy">
         `;

           public mySchema: Schema = mySchema;
       }
   ```

The following example demonstrates how to create a custom document element in the Kendo UI for Angular Editor by extending the ProseMirror `schema`.

[Demo Source](https://demos.telerik.com/kendo-angular-ui/demos/editor/custom-schema/custom-document-element/source.txt)

## Enabling Clickable Links

By default, links in the Kendo UI for Angular Editor are non-interactive and only trigger selection behavior. To make links clickable and allow users to open them directly, you need to define a custom [ProseMirror Mark](https://prosemirror.net/docs/ref/version/0.11.0.html#model.Mark) and update the schema to use it. The following steps demonstrate how to achieve this:

1. Define a custom ProseMirror mark for clickable links. The mark should handle the `onclick` event and customize the rendering of anchor tags:

   ```ts
       export const clickableLinkMarkSpec: MarkSpec = {
           attrs: {
               href: {},
               title: { default: null },
               target: { default: null },
           },
           // Keep link marks from extending at cursor boundaries.
           inclusive: false,
           parseDOM: [
               {
                   tag: 'a[href]',
                   getAttrs(dom: any) {
                       return {
                           href: dom.getAttribute('href'),
                           title: dom.getAttribute('title'),
                           target: dom.getAttribute('target'),
                       };
                   },
               },
           ],
           toDOM(mark) {
               const { href, title, target } = mark.attrs;
               const attrs: any = {
                   href,
                   // Prevent link clicks from changing the Editor selection.
                   onclick:
                       "event.stopPropagation(); window.open(this.href, this.target || '_blank'); return false;",
                   style: 'cursor: pointer; color: #0066cc; text-decoration: underline;',
               };

               if (title) attrs.title = title;
               if (target) attrs.target = target;

               return ['a', attrs, 0];
           },
       };
   ```

2. Update the schema's marks collection to use your clickable link mark instead of the default one. This is done using the [`OrderedMap.update`](https://github.com/marijnh/orderedmap#methods) method from ProseMirror, which returns a new marks collection with the updated mark:

   ```ts
       import { Schema, schema } from "@progress/kendo-angular-editor";
       import { clickableLinkMarkSpec } from "./clickable-link-mark";

       const nodes = schema.spec.nodes;
       // Retain default marks except for the interactive link behavior.
       const marks = schema.spec.marks.update("link", clickableLinkMarkSpec);

       export const mySchema = new Schema({
           nodes,
           marks,
       });
   ```

3. Load the custom schema in the Editor component by binding the Editor's `schema` input to your custom schema:

   ```ts
   import { Component } from '@angular/core';
   import { mySchema } from './custom-schema';

   @Component({
     selector: 'my-app',
     template: `
       <kendo-editor [schema]="mySchema"></kendo-editor>
     `
   })
   export class AppComponent {
     public mySchema = mySchema;
   }
   ```

This approach parses `<a>` elements with `href`, `title`, and `target` attributes, and renders them as interactive links. Users can open links in a new tab or window directly from the Editor.

The following example demonstrates how to replace the default link mark with a custom clickable link mark in the Editor component.

[Demo Source](https://demos.telerik.com/kendo-angular-ui/demos/editor/custom-schema/clickable-links/source.txt)

## Suggested Links

* [API Index of the Editor](https://www.telerik.com/kendo-angular-ui/components/editor/api)
* [API Reference of the EditorComponent](https://www.telerik.com/kendo-angular-ui/components/editor/api/editorcomponent)
* [ProseMirror Schema Guide](https://prosemirror.net/docs/guide/#schema)
* [ProseMirror `Schema` Class](https://prosemirror.net/docs/ref/#model.Document_Schema)
* [ProseMirror `MarkSpec` Interface](https://prosemirror.net/docs/ref/#model.MarkSpec)
* [ProseMirror `NodeSpec` Interface](http://prosemirror.net/docs/ref/#model.NodeSpec)
* [Angular Editor](https://www.telerik.com/kendo-angular-ui/editor)
