diff --git a/README.md b/README.md
index 8812daf..3480043 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,8 @@ App Config UI Location allows you to manage all the app settings centrally. Once
The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements.
+New RTE plugin examples [RTE PLUGIN](/docs/rte-plugin.md)
+
### Sidebar Location
The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content.
diff --git a/docs/rte-plugin.md b/docs/rte-plugin.md
new file mode 100644
index 0000000..4ac6b6e
--- /dev/null
+++ b/docs/rte-plugin.md
@@ -0,0 +1,296 @@
+# JSON RTE Plugin Development Guide
+
+Quick reference for creating JSON Rich Text Editor plugins using the new simplified approach.
+
+## 🚀 Quick Start
+
+```typescript
+import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';
+
+// Create a simple plugin
+const boldPlugin = new PluginBuilder('bold-plugin')
+ .title('Bold')
+ .elementType('inline')
+ .on('exec', (rte) => {
+ rte.addMark('bold', true);
+ })
+ .build();
+
+// Register the plugin
+ContentstackAppSDK.registerRTEPlugins(boldPlugin);
+```
+
+## 📋 Plugin Types
+
+### Inline Plugin
+For text formatting (bold, italic, etc.)
+
+```typescript
+const italicPlugin = new PluginBuilder('italic')
+ .title('Italic')
+ .elementType('inline')
+ .display(['toolbar', 'hoveringToolbar'])
+ .on('exec', (rte) => {
+ rte.addMark('italic', true);
+ })
+ .build();
+```
+
+### Block Plugin
+For block-level elements (headings, paragraphs, etc.)
+
+```typescript
+const headingPlugin = new PluginBuilder('heading')
+ .title('Heading')
+ .elementType('block')
+ .render(({ children, attrs }) => (
+
+ {children}
+
+ ))
+ .on('exec', (rte) => {
+ rte.insertNode({
+ type: 'heading',
+ attrs: { level: 2 },
+ children: [{ text: 'New Heading' }]
+ });
+ })
+ .build();
+```
+
+### Void Plugin
+For self-closing elements (images, embeds, etc.)
+
+```typescript
+const imagePlugin = new PluginBuilder('image')
+ .title('Image')
+ .elementType('void')
+ .render(({ attrs }) => (
+
+ ))
+ .on('exec', (rte) => {
+ const src = prompt('Enter image URL:');
+ if (src) {
+ rte.insertNode({
+ type: 'image',
+ attrs: { src },
+ children: [{ text: '' }]
+ });
+ }
+ })
+ .build();
+```
+
+## 🎛️ Builder Methods
+
+### Basic Configuration
+```typescript
+new PluginBuilder('plugin-id')
+ .title('Plugin Name') // Toolbar button text
+ .icon() // Button icon (React element)
+ .elementType('block') // 'inline' | 'block' | 'void'
+```
+
+### Display Options
+```typescript
+ .display(['toolbar']) // Show in main toolbar only
+ .display(['hoveringToolbar']) // Show in hover toolbar only
+ .display(['toolbar', 'hoveringToolbar']) // Show in both
+```
+
+### Event Handlers
+```typescript
+ .on('exec', (rte) => {}) // Button click
+ .on('keydown', ({ event, rte }) => {}) // Key press
+ .on('paste', ({ rte, preventDefault }) => {}) // Paste event
+```
+
+### Advanced Options
+```typescript
+ .render(ComponentFunction) // Custom render component
+ .shouldOverride((element) => boolean) // Override existing elements
+ .configure(async (sdk) => {}) // Dynamic configuration
+```
+
+## 🔧 Event Handling
+
+### Click Handler
+```typescript
+.on('exec', (rte) => {
+ // Insert text
+ rte.insertText('Hello World');
+
+ // Add formatting
+ rte.addMark('bold', true);
+
+ // Insert node
+ rte.insertNode({
+ type: 'custom-element',
+ attrs: { id: 'unique-id' },
+ children: [{ text: 'Content' }]
+ });
+})
+```
+
+### Keyboard Handler
+```typescript
+.on('keydown', ({ event, rte }) => {
+ if (event.key === 'Enter' && event.ctrlKey) {
+ event.preventDefault();
+ // Custom enter behavior
+ rte.insertBreak();
+ }
+})
+```
+
+## 📦 Container Plugins (Dropdowns)
+
+Create grouped plugins in a dropdown menu:
+
+```typescript
+const mediaContainer = new PluginBuilder('media-dropdown')
+ .title('Media')
+ .icon()
+ .addPlugins(
+ imagePlugin,
+ videoPlugin,
+ audioPlugin
+ )
+ .build();
+```
+
+## 🔄 Plugin Registration
+
+### Single Plugin
+```typescript
+ContentstackAppSDK.registerRTEPlugins(myPlugin);
+```
+
+### Multiple Plugins
+```typescript
+ContentstackAppSDK.registerRTEPlugins(
+ boldPlugin,
+ italicPlugin,
+ headingPlugin,
+ imagePlugin
+);
+```
+
+### With Enhanced SDK Context
+```typescript
+// Register plugins first (captures RTE context)
+await ContentstackAppSDK.registerRTEPlugins(myPlugin);
+
+// Then initialize SDK (gets enhanced context)
+const sdk = await ContentstackAppSDK.init();
+```
+
+## 💡 Real-World Examples
+
+### YouTube Embed Plugin
+```typescript
+const youtubePlugin = new PluginBuilder('youtube')
+ .title('YouTube')
+ .elementType('void')
+ .render(({ attrs }) => (
+
+ ))
+ .on('exec', (rte) => {
+ const url = prompt('Enter YouTube URL:');
+ const videoId = extractVideoId(url);
+ if (videoId) {
+ rte.insertNode({
+ type: 'youtube',
+ attrs: { videoId },
+ children: [{ text: '' }]
+ });
+ }
+ })
+ .build();
+```
+
+### Smart Quote Plugin
+```typescript
+const smartQuotePlugin = new PluginBuilder('smart-quote')
+ .title('Smart Quotes')
+ .elementType('inline')
+ .on('keydown', ({ event, rte }) => {
+ if (event.key === '"') {
+ event.preventDefault();
+ const isStart = rte.selection.isAtStart();
+ rte.insertText(isStart ? '"' : '"');
+ }
+ })
+ .build();
+```
+
+### Dynamic Configuration Plugin
+```typescript
+const configurablePlugin = new PluginBuilder('configurable')
+ .title('Dynamic Plugin')
+ .configure(async (sdk) => {
+ const config = await sdk.getConfig();
+ return {
+ title: config.customTitle || 'Default Title',
+ icon: config.customIcon ||
+ };
+ })
+ .on('exec', (rte) => {
+ // Plugin logic using dynamic config
+ })
+ .build();
+```
+
+## 🎯 Best Practices
+
+1. **Use semantic IDs**: `'heading-h2'` instead of `'plugin1'`
+2. **Provide clear titles**: Users see these in the toolbar
+3. **Handle edge cases**: Check for selection, validate inputs
+4. **Use TypeScript**: Better development experience
+5. **Test thoroughly**: Different content structures, browser compatibility
+
+## 📚 Migration from Legacy
+
+### Old Way (Legacy RTEPlugin)
+```typescript
+const oldPlugin = new RTEPlugin('my-plugin', (rte) => ({
+ title: 'My Plugin',
+ icon: ,
+ display: ['toolbar'],
+ elementType: ['block'],
+ render: MyComponent
+}));
+oldPlugin.on('exec', handler);
+```
+
+### New Way (PluginBuilder)
+```typescript
+const newPlugin = new PluginBuilder('my-plugin')
+ .title('My Plugin')
+ .icon()
+ .display(['toolbar'])
+ .elementType('block')
+ .render(MyComponent)
+ .on('exec', handler)
+ .build();
+```
+
+## 🔗 Resources
+
+- [Contentstack RTE Documentation](https://www.contentstack.com/docs/developers/developer-hub/rte-location)
+- [JSON RTE Structure Guide](https://www.contentstack.com/docs/developers/apis/content-management-api/#json-rich-text-editor)
+- [App SDK API Reference](https://github.com/contentstack/app-sdk-docs)
+
+---
+
+**Happy plugin building! 🚀**
\ No newline at end of file
diff --git a/src/RTE/index.tsx b/src/RTE/index.tsx
index b4cdd78..deeffe7 100644
--- a/src/RTE/index.tsx
+++ b/src/RTE/index.tsx
@@ -1,10 +1,10 @@
import {
+ IConfig,
IConfigCallback,
IContainerMetaData,
IOnFunction,
IPluginMetaData,
IRteParam,
- IConfig,
} from "./types";
export class RTEPlugin {
diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx
index d119c09..e94b263 100644
--- a/src/RTE/types.tsx
+++ b/src/RTE/types.tsx
@@ -1,15 +1,15 @@
import React, { ReactElement } from "react";
import {
+ Editor,
+ ElementEntry,
Location,
+ Node,
NodeEntry,
+ NodeMatch,
Path,
Point,
- Node,
- ElementEntry,
- Transforms,
- Editor,
Span,
- NodeMatch,
+ Transforms,
} from "slate";
import { RTEPlugin } from "./index";
@@ -199,7 +199,7 @@ export declare interface IRteElementType {
children: Array;
}
-type IDynamicFunction = (
+export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude
diff --git a/src/index.ts b/src/index.ts
index b36ddec..fec8bf4 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,12 @@
import postRobot from "post-robot";
-import UiLocation from "./uiLocation";
import { version } from "../package.json";
+import { IRteParam } from "./RTE/types";
+import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin";
import { InitializationData } from "./types";
+import UiLocation from "./uiLocation"
+// Configure post-robot logging
postRobot.CONFIG.LOG_LEVEL = "error";
/**
@@ -20,8 +23,7 @@ postRobot.CONFIG.LOG_LEVEL = "error";
* })
* @return {Promise} A promise object which will be resolved with an instance of the {@link UiLocation} class.
* @hideconstructor
- */
-
+*/
class ContentstackAppSDK {
/**
* A static variable that stores the instance of {@link UiLocation} class after initialization
@@ -29,27 +31,85 @@ class ContentstackAppSDK {
static _uiLocation: UiLocation;
/**
- * Initializes the App SDK and returns an instance of {@link UiLocation} class
- */
+ * Initializes the App SDK and returns an instance of {@link UiLocation} class
+ */
static init(): Promise {
if (this._uiLocation) {
- return Promise.resolve(this._uiLocation);
+ return Promise.resolve(this._uiLocation);
}
+
return UiLocation.initialize(version)
.then((initializationData: InitializationData) => {
+
this._uiLocation = new UiLocation(initializationData);
- return Promise.resolve(this._uiLocation);
+ return this._uiLocation;
})
.catch((e: Error) => Promise.reject(e));
}
+
+ /**
+ * Registers RTE plugins with the Contentstack platform.
+ * This method is the primary entry point for defining and registering custom RTE plugins
+ * built using the PluginBuilder pattern. It returns a function that the Contentstack
+ * platform will invoke at runtime, providing the necessary context.
+ *
+ * @example
+ * // In your plugin's entry file (e.g., src/index.ts):
+ * import ContentstackAppSDK from '@contentstack/app-sdk';
+ * import { PluginBuilder, IRteParam } from '@contentstack/app-sdk/rtePlugin';
+ *
+ * const MyCustomPlugin = new PluginBuilder("my-plugin-id")
+ * .title("My Plugin")
+ * .icon()
+ * .on("exec", (rte: IRteParam) => {
+ * // Access SDK via rte.sdk if needed:
+ * const sdk = rte.sdk;
+ * // ... plugin execution logic ...
+ * })
+ * .build();
+ *
+ * export default ContentstackAppSDK.registerRTEPlugins(
+ * MyCustomPlugin
+ * );
+ *
+ * @param {...PluginDefinition} pluginDefinitions - One or more plugin definitions created using the `PluginBuilder`.
+ * Each `PluginDefinition` describes the plugin's configuration, callbacks, and any child plugins.
+ * @returns {Promise<{ __isPluginBuilder__: boolean; version: string; plugins: (context: RTEContext, rte: IRteParam) => Promise<{ [key: string]: RTEPlugin; }>; }>}
+ * A Promise that resolves to an object containing:
+ * - `__isPluginBuilder__`: A boolean flag indicating this is a builder-based plugin export.
+ * - `version`: The version of the SDK that registered the plugins.
+ * - `plugins`: An asynchronous function. This function is designed to be invoked by the
+ * Contentstack platform loader, providing the `context` (initialization data) and
+ * the `rte` instance. When called, it materializes and returns a map of the
+ * registered `RTEPlugin` instances, keyed by their IDs.
+ */
+
+ static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) {
+ return {
+ __isPluginBuilder__: true,
+ version,
+ plugins: (context: InitializationData, rte: IRteParam) => {
+ return registerPlugins(...pluginDefinitions)(context, rte);
+ }
+ };
+ }
+
/**
- * Version of Contentstack App SDK.
+ * Get SDK version
*/
static get SDK_VERSION() {
return version;
}
}
+// ES6 exports
export default ContentstackAppSDK;
-module.exports = ContentstackAppSDK;
+export { PluginBuilder };
+
+// CommonJS compatibility
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = ContentstackAppSDK;
+ module.exports.default = ContentstackAppSDK;
+ module.exports.PluginBuilder = PluginBuilder;
+}
diff --git a/src/rtePlugin.ts b/src/rtePlugin.ts
new file mode 100644
index 0000000..7cc8d57
--- /dev/null
+++ b/src/rtePlugin.ts
@@ -0,0 +1,211 @@
+import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE";
+import {
+ IConfig,
+ IDisplayOnOptions,
+ IDynamicFunction,
+ IElementTypeOptions,
+ IOnFunction,
+ IRteElementType,
+ IRteParam,
+} from "./RTE/types";
+import { InitializationData, IRTEInitData } from "./types";
+import UiLocation from "./uiLocation";
+
+type PluginConfigCallback = (sdk: UiLocation) => Promise | IConfig;
+
+interface PluginDefinition {
+ id: string;
+ config: Partial;
+ callbacks: Partial;
+ asyncConfigCallback?: PluginConfigCallback;
+ childBuilders: PluginBuilder[];
+}
+
+class PluginBuilder {
+ private id: string;
+ private _config: Partial = {};
+ private _callbacks: Partial = {};
+ private _asyncConfigCallback?: PluginConfigCallback;
+ private _childBuilders: PluginBuilder[] = [];
+
+ constructor(id: string) {
+ this.id = id;
+ this._config.title = id;
+ }
+
+ title(title: string): PluginBuilder {
+ this._config.title = title;
+ return this;
+ }
+ icon(icon: React.ReactElement | null): PluginBuilder {
+ this._config.icon = icon;
+ return this;
+ }
+ display(display: IDisplayOnOptions | IDisplayOnOptions[]): PluginBuilder {
+ this._config.display = display;
+ return this;
+ }
+ elementType(
+ elementType:
+ | IElementTypeOptions
+ | IElementTypeOptions[]
+ | IDynamicFunction
+ ): PluginBuilder {
+ this._config.elementType = elementType;
+ return this;
+ }
+ render(renderFn: (element: React.ReactElement, attrs: { [key: string]: any }, path: number[], rte: IRteParam) => React.ReactElement): PluginBuilder {
+ // Wrap the render function with path validation
+ this._config.render = (element: React.ReactElement, attrs: { [key: string]: any }, path: number[], rte: IRteParam) => {
+ try {
+ // Validate that the path exists in the document
+ if (path && path.length > 0 && rte) {
+ try {
+ // Try to get the node at the given path to validate it exists
+ const nodeEntry = rte.getNode(path);
+ if (!nodeEntry) {
+ console.warn(`Path ${path} does not exist in document, skipping render`);
+ return element;
+ }
+ } catch (pathError) {
+ console.warn(`Invalid path ${path} in render function:`, pathError);
+ return element;
+ }
+ }
+
+ return renderFn(element, attrs, path, rte);
+ } catch (error) {
+ console.error('Error in plugin render function:', error);
+ // Return the original element as fallback
+ return element;
+ }
+ };
+ return this;
+ }
+ shouldOverride(
+ shouldOverrideFn: (element: IRteElementType) => boolean
+ ): PluginBuilder {
+ this._config.shouldOverride = shouldOverrideFn;
+ return this;
+ }
+ on(
+ type: T,
+ callback: IOnFunction[T]
+ ): PluginBuilder {
+ this._callbacks[type] = callback;
+ return this;
+ }
+ configure(callback: PluginConfigCallback): PluginBuilder {
+ this._asyncConfigCallback = callback;
+ return this;
+ }
+ addPlugins(...builders: PluginBuilder[]): PluginBuilder {
+ this._childBuilders.push(...builders);
+ return this;
+ }
+
+ /**
+ * Builds and returns a definition of the RTE Plugin, ready to be materialized
+ * into a concrete RTEPlugin instance later when the SDK and Plugin Factory are available.
+ * This method no longer performs the actual creation of RTEPlugin instances.
+ */
+ build(): PluginDefinition {
+ return {
+ id: this.id,
+ config: this._config,
+ callbacks: this._callbacks,
+ asyncConfigCallback: this._asyncConfigCallback,
+ childBuilders: this._childBuilders,
+ };
+ }
+}
+
+async function materializePlugin(
+ pluginDef: PluginDefinition,
+ sdk: UiLocation
+): Promise {
+ let finalConfig: Partial = { ...pluginDef.config };
+ if (pluginDef.asyncConfigCallback) {
+ const dynamicConfig = await Promise.resolve(
+ pluginDef.asyncConfigCallback(sdk)
+ );
+ finalConfig = { ...finalConfig, ...dynamicConfig };
+ }
+ const plugin = rtePluginInitializer(
+ pluginDef.id,
+ (rte: IRteParam | void) => {
+ // The rte parameter is passed when the plugin is actually used
+ // finalConfig already contains the merged configuration
+ return finalConfig;
+ }
+ );
+ Object.entries(pluginDef.callbacks).forEach(([type, callback]) => {
+ // Wrap callbacks with error handling
+ const wrappedCallback = (params: any) => {
+ try {
+ return callback(params);
+ } catch (error) {
+ console.error(`Error in plugin callback ${type}:`, error);
+ // Don't re-throw to prevent breaking the RTE
+ }
+ };
+ plugin.on(type as keyof IOnFunction, wrappedCallback);
+ });
+ if (pluginDef.childBuilders.length > 0) {
+ const childPlugins = await Promise.all(
+ pluginDef.childBuilders.map((childBuilder) =>
+ materializePlugin(childBuilder.build(), sdk)
+ )
+ );
+ plugin.addPlugins(...childPlugins);
+ }
+
+ return plugin;
+}
+
+function registerPlugins(
+ ...pluginDefinitions: PluginDefinition[]
+): (
+ context: InitializationData,
+ rte: IRteParam
+) => Promise<{ [key: string]: Plugin }> {
+ const definitionsToProcess = [...pluginDefinitions];
+ const plugins = async (context: InitializationData, rte: IRteParam) => {
+ try {
+ const sdk = new UiLocation(context);
+ console.log("sdk", sdk);
+
+ const materializedPlugins: { [key: string]: Plugin } = {};
+ console.log("materializedPlugins", materializedPlugins);
+
+ for (const def of definitionsToProcess) {
+ const pluginInstance = await materializePlugin(def, sdk);
+ materializedPlugins[def.id] = pluginInstance;
+ }
+ rte.sdk = sdk;
+ console.log("rte", rte);
+ console.log("materializedPlugins", materializedPlugins);
+
+ return materializedPlugins;
+ } catch (err) {
+ console.error("Error during plugin registration:", err);
+ throw err;
+ }
+ };
+ return plugins;
+}
+
+export {
+ IConfig,
+ IDisplayOnOptions,
+ IDynamicFunction,
+ IElementTypeOptions,
+ IOnFunction,
+ IRteElementType,
+ IRteParam,
+ Plugin,
+ PluginBuilder,
+ PluginDefinition,
+ registerPlugins
+};
+
diff --git a/src/types.ts b/src/types.ts
index c351baa..bd531aa 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -268,3 +268,20 @@ export type RegionType =
| "AZURE_EU"
| "GCP_NA"
| string;
+
+export type Extension = {
+ app_installation_uid: string
+ app_uid: string
+ config: GenericObjectType
+ created_at: string
+ created_by: string
+ signed: boolean
+ src: string
+ tags: string[]
+ title: string
+ type: string
+ uid: string
+ updated_at: string
+ updated_by: string
+ _version: number
+}
diff --git a/src/types/rte.d.ts b/src/types/rte.d.ts
new file mode 100644
index 0000000..01ce0c9
--- /dev/null
+++ b/src/types/rte.d.ts
@@ -0,0 +1,7 @@
+import type { UiLocation } from '../uiLocation';
+
+declare module "../RTE/types" {
+ export interface IRteParam {
+ sdk: UiLocation;
+ }
+}
\ No newline at end of file
diff --git a/src/uiLocation.ts b/src/uiLocation.ts
index 5507b49..4977f5b 100755
--- a/src/uiLocation.ts
+++ b/src/uiLocation.ts
@@ -29,6 +29,7 @@ import {
Manifest,
IGlobalFullPageLocation,
RegionType,
+ Region,
} from "./types";
import { GenericObjectType } from "./types/common.types";
import { User } from "./types/user.types";
@@ -187,7 +188,7 @@ class UiLocation {
this.modal = new Modal();
- this.region = formatAppRegion(initializationData.region);
+ this.region = formatAppRegion(initializationData.region as Region);
this.endpoints = initializationData.endpoints;
const stack = new Stack(initializationData.stack, postRobot, {
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 7f14cdf..054269a 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -1,6 +1,6 @@
import { AxiosHeaders, AxiosRequestConfig, AxiosResponse } from "axios";
-import { Region, RegionType } from "../types";
+import { RegionType, } from "../types";
export function onData>(data: { data: Data }) {
if (typeof data.data === "string") {
@@ -14,7 +14,7 @@ export function onError(error: Error) {
}
export function formatAppRegion(region: string): RegionType {
- return region ?? Region.UNKNOWN;
+ return region as RegionType;
}
export function getPreferredBodyElement(nodeCollection: HTMLCollection) {