diff --git a/apps/www/content/docs/lint.mdx b/apps/www/content/docs/lint.mdx
new file mode 100644
index 0000000000..14295a3a3c
--- /dev/null
+++ b/apps/www/content/docs/lint.mdx
@@ -0,0 +1,97 @@
+---
+title: Lint
+description: Lint your document with custom rules.
+---
+
+
+
+This package is experimental. Expect breaking changes in future releases.
+
+
+
+
+
+
+
+The Lint feature allows you to enforce custom rules and configurations on your documents.
+
+## Features
+
+- Customizable linting config, plugins and rules with a similar API to ESLint
+- Provides suggestions and fixes for each rule
+
+
+
+## Installation
+
+```bash
+npm install @udecode/plate-lint
+```
+
+## Usage
+
+```tsx
+import { resolveLintConfigs } from '@udecode/plate-lint/react';
+import { emojiLintPlugin } from '@udecode/plate-lint/plugins';
+
+const lintConfigs = resolveLintConfigs([
+ emojiLintPlugin.configs.all,
+ // ...otherConfigs
+]);
+```
+
+- [resolveLintConfigs](/docs/components/resolve-lint-configs)
+
+### Configuration
+
+To configure the linting rules, you can use the `resolveLintConfigs` function to merge multiple configurations:
+
+```tsx
+const configs = [
+ emojiLintPlugin.configs.all,
+ {
+ languageOptions: {
+ parserOptions: {
+ minLength: 4,
+ },
+ },
+ settings: {
+ emojiMap: wordToEmojisMap,
+ maxSuggestions: 5,
+ },
+ },
+];
+```
+
+### Plugins
+
+#### EmojiLintPlugin
+
+A plugin that provides linting rules for replacing text with emojis.
+
+
+
+Map of words to their corresponding emoji suggestions.
+
+
+Maximum number of emoji suggestions to provide. Default: `8`
+
+
+
+## API
+
+### resolveLintConfigs
+
+Merges multiple lint configurations into a single set of resolved rules.
+
+
+
+ Array of lint configurations to merge.
+
+
+
+
+
+ Object containing the resolved lint rules.
+
+
diff --git a/apps/www/package.json b/apps/www/package.json
index 4f73ba3fc1..cf3a2312e7 100644
--- a/apps/www/package.json
+++ b/apps/www/package.json
@@ -109,6 +109,7 @@
"@udecode/plate-layout": "workspace:^",
"@udecode/plate-line-height": "workspace:^",
"@udecode/plate-link": "workspace:^",
+ "@udecode/plate-lint": "workspace:^",
"@udecode/plate-list": "workspace:^",
"@udecode/plate-markdown": "workspace:^",
"@udecode/plate-media": "workspace:^",
diff --git a/apps/www/public/r/styles/default/lint-demo.json b/apps/www/public/r/styles/default/lint-demo.json
new file mode 100644
index 0000000000..e4c744b91a
--- /dev/null
+++ b/apps/www/public/r/styles/default/lint-demo.json
@@ -0,0 +1,38 @@
+{
+ "dependencies": [
+ "@udecode/plate-lint",
+ "@udecode/plate-basic-marks",
+ "@udecode/plate-node-id"
+ ],
+ "doc": {
+ "description": "Lint your document with emoji suggestions.",
+ "docs": [
+ {
+ "route": "/docs/lint",
+ "title": "Lint"
+ }
+ ]
+ },
+ "files": [
+ {
+ "content": "'use client';\n\nimport { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';\nimport { Plate, useEditorPlugin } from '@udecode/plate-common/react';\nimport {\n ExperimentalLintPlugin,\n caseLintPlugin,\n replaceLintPlugin,\n} from '@udecode/plate-lint/react';\nimport { NodeIdPlugin } from '@udecode/plate-node-id';\nimport { type Gemoji, gemoji } from 'gemoji';\n\nimport {\n useCreateEditor,\n viewComponents,\n} from '@/components/editor/use-create-editor';\nimport { Button } from '@/components/plate-ui/button';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\nimport { LintLeaf } from '@/components/plate-ui/lint-leaf';\nimport { LintPopover } from '@/components/plate-ui/lint-popover';\n\nexport default function LintEmojiDemo() {\n const editor = useCreateEditor({\n override: {\n components: viewComponents,\n },\n plugins: [\n ExperimentalLintPlugin.configure({\n render: {\n afterEditable: LintPopover,\n node: LintLeaf,\n },\n }),\n NodeIdPlugin,\n BasicMarksPlugin,\n ],\n value: [\n {\n children: [\n {\n text: \"I'm happy to see my cat and dog. I love them even when I'm sad.\",\n },\n ],\n type: 'p',\n },\n {\n children: [\n {\n text: 'I like to eat pizza and ice cream.',\n },\n ],\n type: 'p',\n },\n {\n children: [\n {\n text: 'hello world! this is a test. new sentence here. the cat is happy.',\n },\n ],\n type: 'p',\n },\n ],\n });\n\n return (\n
\n );\n}\n\nfunction EmojiPlateEditorContent() {\n const { api, editor } = useEditorPlugin(ExperimentalLintPlugin);\n\n const runFirst = () => {\n api.lint.run([\n {\n ...replaceLintPlugin.configs.all,\n targets: [{ id: editor.children[0].id as string }],\n },\n {\n settings: {\n replace: {\n replaceMap: emojiMap,\n },\n },\n },\n ]);\n };\n\n const runMax = () => {\n api.lint.run([\n replaceLintPlugin.configs.all,\n {\n settings: {\n replace: {\n parserOptions: {\n maxLength: 4,\n },\n replaceMap: emojiMap,\n },\n },\n },\n ]);\n };\n\n const runCase = () => {\n api.lint.run([\n caseLintPlugin.configs.all,\n {\n settings: {\n case: {\n ignoredWords: ['iPhone', 'iOS', 'iPad'],\n },\n },\n },\n ]);\n };\n\n const runBoth = () => {\n api.lint.run([\n replaceLintPlugin.configs.all,\n caseLintPlugin.configs.all,\n {\n settings: {\n case: {\n ignoredWords: ['iPhone', 'iOS', 'iPad'],\n },\n replace: {\n parserOptions: {\n maxLength: 4,\n },\n replaceMap: emojiMap,\n },\n },\n },\n ]);\n };\n\n return (\n <>\n \n \n \n \n \n \n
\n \n \n \n >\n );\n}\n\nconst excludeWords = new Set([\n 'a',\n 'an',\n 'and',\n 'are',\n 'as',\n 'at',\n 'be',\n 'but',\n 'by',\n 'for',\n 'from',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'no',\n 'not',\n 'of',\n 'on',\n 'or',\n 'such',\n 'that',\n 'the',\n 'their',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'to',\n 'was',\n 'was',\n 'will',\n 'with',\n]);\n\ntype WordSource = 'description' | 'exact_name' | 'name' | 'tag';\n\nfunction splitWords(text: string): string[] {\n return text.toLowerCase().split(/[^\\d_a-z]+/);\n}\n\nconst emojiMap = new Map<\n string,\n (Gemoji & { text: string; type: 'emoji' })[]\n>();\n\ngemoji.forEach((emoji) => {\n const wordSources = new Map();\n\n // Priority 1: Exact name matches (highest priority)\n emoji.names.forEach((name) => {\n const nameLower = name.toLowerCase();\n splitWords(name).forEach((word) => {\n if (!excludeWords.has(word)) {\n // If the name is exactly this word, it gets highest priority\n wordSources.set(word, word === nameLower ? 'exact_name' : 'name');\n }\n });\n });\n\n // Priority 3: Tags\n emoji.tags.forEach((tag) => {\n splitWords(tag).forEach((word) => {\n if (!excludeWords.has(word) && !wordSources.has(word)) {\n wordSources.set(word, 'tag');\n }\n });\n });\n\n // Priority 4: Description (lowest priority)\n if (emoji.description) {\n splitWords(emoji.description).forEach((word) => {\n if (!excludeWords.has(word) && !wordSources.has(word)) {\n wordSources.set(word, 'description');\n }\n });\n }\n\n wordSources.forEach((source, word) => {\n if (!emojiMap.has(word)) {\n emojiMap.set(word, []);\n }\n\n const emojis = emojiMap.get(word)!;\n\n const insertIndex = emojis.findIndex((e) => {\n const existingSource = getWordSource(e, word);\n\n return source > existingSource;\n });\n\n if (insertIndex === -1) {\n emojis.push({\n ...emoji,\n text: emoji.emoji,\n type: 'emoji',\n });\n } else {\n emojis.splice(insertIndex, 0, {\n ...emoji,\n text: emoji.emoji,\n type: 'emoji',\n });\n }\n });\n});\n\nfunction getWordSource(emoji: Gemoji, word: string): WordSource {\n // Check for exact name match first\n if (emoji.names.some((name) => name.toLowerCase() === word))\n return 'exact_name';\n // Then check for partial name matches\n if (emoji.names.some((name) => splitWords(name).includes(word)))\n return 'name';\n if (emoji.tags.some((tag) => splitWords(tag).includes(word))) return 'tag';\n\n return 'description';\n}\n",
+ "path": "example/lint-demo.tsx",
+ "target": "components/lint-demo.tsx",
+ "type": "registry:example"
+ },
+ {
+ "content": "'use client';\n\nimport type { Value } from '@udecode/plate-common';\n\nimport { withProps } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n SubscriptPlugin,\n SuperscriptPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport {\n CodeBlockPlugin,\n CodeLinePlugin,\n CodeSyntaxPlugin,\n} from '@udecode/plate-code-block/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport {\n type CreatePlateEditorOptions,\n ParagraphPlugin,\n PlateLeaf,\n usePlateEditor,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { EmojiInputPlugin } from '@udecode/plate-emoji/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { KbdPlugin } from '@udecode/plate-kbd/react';\nimport { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n PlaceholderPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n MentionInputPlugin,\n MentionPlugin,\n} from '@udecode/plate-mention/react';\nimport { SlashInputPlugin } from '@udecode/plate-slash-command/react';\nimport {\n TableCellHeaderPlugin,\n TableCellPlugin,\n TablePlugin,\n TableRowPlugin,\n} from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\n\nimport { AILeaf } from '@/components/plate-ui/ai-leaf';\nimport { BlockquoteElement } from '@/components/plate-ui/blockquote-element';\nimport { CodeBlockElement } from '@/components/plate-ui/code-block-element';\nimport { CodeLeaf } from '@/components/plate-ui/code-leaf';\nimport { CodeLineElement } from '@/components/plate-ui/code-line-element';\nimport { CodeSyntaxLeaf } from '@/components/plate-ui/code-syntax-leaf';\nimport { ColumnElement } from '@/components/plate-ui/column-element';\nimport { ColumnGroupElement } from '@/components/plate-ui/column-group-element';\nimport { CommentLeaf } from '@/components/plate-ui/comment-leaf';\nimport { DateElement } from '@/components/plate-ui/date-element';\nimport { EmojiInputElement } from '@/components/plate-ui/emoji-input-element';\nimport { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HighlightLeaf } from '@/components/plate-ui/highlight-leaf';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { ImageElement } from '@/components/plate-ui/image-element';\nimport { KbdLeaf } from '@/components/plate-ui/kbd-leaf';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { MediaAudioElement } from '@/components/plate-ui/media-audio-element';\nimport { MediaEmbedElement } from '@/components/plate-ui/media-embed-element';\nimport { MediaFileElement } from '@/components/plate-ui/media-file-element';\nimport { MediaPlaceholderElement } from '@/components/plate-ui/media-placeholder-element';\nimport { MediaVideoElement } from '@/components/plate-ui/media-video-element';\nimport { MentionElement } from '@/components/plate-ui/mention-element';\nimport { MentionInputElement } from '@/components/plate-ui/mention-input-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\nimport { withPlaceholders } from '@/components/plate-ui/placeholder';\nimport { SlashInputElement } from '@/components/plate-ui/slash-input-element';\nimport {\n TableCellElement,\n TableCellHeaderElement,\n} from '@/components/plate-ui/table-cell-element';\nimport { TableElement } from '@/components/plate-ui/table-element';\nimport { TableRowElement } from '@/components/plate-ui/table-row-element';\nimport { TocElement } from '@/components/plate-ui/toc-element';\nimport { ToggleElement } from '@/components/plate-ui/toggle-element';\nimport { withDraggables } from '@/components/plate-ui/with-draggables';\n\nimport { editorPlugins, viewPlugins } from './plugins/editor-plugins';\n\nexport const viewComponents = {\n [AudioPlugin.key]: MediaAudioElement,\n [BlockquotePlugin.key]: BlockquoteElement,\n [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),\n [CodeBlockPlugin.key]: CodeBlockElement,\n [CodeLinePlugin.key]: CodeLineElement,\n [CodePlugin.key]: CodeLeaf,\n [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,\n [ColumnItemPlugin.key]: ColumnElement,\n [ColumnPlugin.key]: ColumnGroupElement,\n [CommentsPlugin.key]: CommentLeaf,\n [DatePlugin.key]: DateElement,\n [FilePlugin.key]: MediaFileElement,\n [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),\n [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),\n [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),\n [HighlightPlugin.key]: HighlightLeaf,\n [HorizontalRulePlugin.key]: HrElement,\n [ImagePlugin.key]: ImageElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [KbdPlugin.key]: KbdLeaf,\n [LinkPlugin.key]: LinkElement,\n [MediaEmbedPlugin.key]: MediaEmbedElement,\n [MentionPlugin.key]: MentionElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [PlaceholderPlugin.key]: MediaPlaceholderElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),\n [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),\n [TableCellHeaderPlugin.key]: TableCellHeaderElement,\n [TableCellPlugin.key]: TableCellElement,\n [TablePlugin.key]: TableElement,\n [TableRowPlugin.key]: TableRowElement,\n [TocPlugin.key]: TocElement,\n [TogglePlugin.key]: ToggleElement,\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n [VideoPlugin.key]: MediaVideoElement,\n};\n\nexport const editorComponents = {\n ...viewComponents,\n [AIPlugin.key]: AILeaf,\n [EmojiInputPlugin.key]: EmojiInputElement,\n [MentionInputPlugin.key]: MentionInputElement,\n [SlashInputPlugin.key]: SlashInputElement,\n};\n\nexport const useCreateEditor = (\n {\n components,\n override,\n readOnly,\n ...options\n }: {\n components?: Record;\n plugins?: any[];\n readOnly?: boolean;\n } & Omit = {},\n deps: any[] = []\n) => {\n return usePlateEditor(\n {\n override: {\n components: {\n ...(readOnly\n ? viewComponents\n : withPlaceholders(withDraggables(editorComponents))),\n ...components,\n },\n ...override,\n },\n plugins: (readOnly ? viewPlugins : editorPlugins) as any,\n ...options,\n },\n deps\n );\n};\n",
+ "path": "components/editor/use-create-editor.ts",
+ "target": "components/use-create-editor.ts",
+ "type": "registry:example"
+ }
+ ],
+ "name": "lint-demo",
+ "registryDependencies": [
+ "editor",
+ "button",
+ "lint-leaf",
+ "lint-popover"
+ ],
+ "type": "registry:example"
+}
\ No newline at end of file
diff --git a/apps/www/public/r/styles/default/lint-emoji-demo.json b/apps/www/public/r/styles/default/lint-emoji-demo.json
new file mode 100644
index 0000000000..2734ab6619
--- /dev/null
+++ b/apps/www/public/r/styles/default/lint-emoji-demo.json
@@ -0,0 +1,38 @@
+{
+ "dependencies": [
+ "@udecode/plate-lint",
+ "@udecode/plate-basic-marks",
+ "@udecode/plate-node-id"
+ ],
+ "doc": {
+ "description": "Lint your document with emoji suggestions.",
+ "docs": [
+ {
+ "route": "/docs/lint",
+ "title": "Lint"
+ }
+ ]
+ },
+ "files": [
+ {
+ "content": "'use client';\n\nimport { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';\nimport { Plate, useEditorPlugin } from '@udecode/plate-common/react';\nimport {\n ExperimentalLintPlugin,\n replaceLintPlugin,\n} from '@udecode/plate-lint/react';\nimport { NodeIdPlugin } from '@udecode/plate-node-id';\n\nimport {\n useCreateEditor,\n viewComponents,\n} from '@/components/editor/use-create-editor';\nimport { Button } from '@/components/plate-ui/button';\nimport { Editor, EditorContainer } from '@/components/plate-ui/editor';\nimport { LintLeaf } from '@/components/plate-ui/lint-leaf';\nimport { LintPopover } from '@/components/plate-ui/lint-popover';\n\nexport default function LintEmojiDemo() {\n const editor = useCreateEditor({\n override: {\n components: viewComponents,\n },\n plugins: [\n ExperimentalLintPlugin.configure({\n render: {\n afterEditable: LintPopover,\n node: LintLeaf,\n },\n }),\n NodeIdPlugin,\n BasicMarksPlugin,\n ],\n value: [\n {\n children: [\n {\n text: \"I'm happy to see my cat and dog. I love them even when I'm sad.\",\n },\n ],\n type: 'p',\n },\n {\n children: [\n {\n text: 'I like to eat pizza and ice cream.',\n },\n ],\n type: 'p',\n },\n ],\n });\n\n return (\n \n );\n}\n\nfunction EmojiPlateEditorContent() {\n const { api, editor } = useEditorPlugin(ExperimentalLintPlugin);\n\n const runFirst = () => {\n api.lint.run([\n {\n ...replaceLintPlugin.configs.all,\n targets: [\n { id: editor.children[0].id as string },\n { id: editor.children[1].id as string },\n ],\n },\n {\n languageOptions: {\n parserOptions: {\n minLength: 4,\n },\n },\n targets: [{ id: editor.children[0].id as string }],\n },\n {\n settings: {\n emojiMap: emojiMap,\n maxSuggestions: 5,\n },\n },\n ]);\n };\n\n const runMax = () => {\n api.lint.run([\n replaceLintPlugin.configs.all,\n {\n languageOptions: {\n parserOptions: {\n maxLength: 4,\n },\n },\n settings: {\n emojiMap: emojiMap,\n },\n },\n ]);\n };\n\n const runAll = () => {\n api.lint.run([\n replaceLintPlugin.configs.all,\n {\n settings: {\n emojiMap: emojiMap,\n },\n },\n ]);\n };\n\n return (\n <>\n \n \n \n \n \n
\n \n \n \n >\n );\n}\n\nconst excludeWords = new Set([\n 'a',\n 'an',\n 'and',\n 'are',\n 'as',\n 'at',\n 'be',\n 'but',\n 'by',\n 'for',\n 'from',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'no',\n 'not',\n 'of',\n 'on',\n 'or',\n 'such',\n 'that',\n 'the',\n 'their',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'to',\n 'was',\n 'was',\n 'will',\n 'with',\n]);\n\ntype WordSource = 'description' | 'exact_name' | 'name' | 'tag';\n\nfunction splitWords(text: string): string[] {\n return text.toLowerCase().split(/[^\\d_a-z]+/);\n}\n\nconst emojiMap = new Map();\n\ngemoji.forEach((emoji) => {\n const wordSources = new Map();\n\n // Priority 1: Exact name matches (highest priority)\n emoji.names.forEach((name) => {\n const nameLower = name.toLowerCase();\n splitWords(name).forEach((word) => {\n if (!excludeWords.has(word)) {\n // If the name is exactly this word, it gets highest priority\n wordSources.set(word, word === nameLower ? 'exact_name' : 'name');\n }\n });\n });\n\n // Priority 3: Tags\n emoji.tags.forEach((tag) => {\n splitWords(tag).forEach((word) => {\n if (!excludeWords.has(word) && !wordSources.has(word)) {\n wordSources.set(word, 'tag');\n }\n });\n });\n\n // Priority 4: Description (lowest priority)\n if (emoji.description) {\n splitWords(emoji.description).forEach((word) => {\n if (!excludeWords.has(word) && !wordSources.has(word)) {\n wordSources.set(word, 'description');\n }\n });\n }\n\n wordSources.forEach((source, word) => {\n if (!emojiMap.has(word)) {\n emojiMap.set(word, []);\n }\n\n const emojis = emojiMap.get(word)!;\n\n const insertIndex = emojis.findIndex((e) => {\n const existingSource = getWordSource(e, word);\n\n return source > existingSource;\n });\n\n if (insertIndex === -1) {\n emojis.push(emoji);\n } else {\n emojis.splice(insertIndex, 0, emoji);\n }\n });\n});\n\nfunction getWordSource(emoji: Gemoji, word: string): WordSource {\n // Check for exact name match first\n if (emoji.names.some((name) => name.toLowerCase() === word))\n return 'exact_name';\n // Then check for partial name matches\n if (emoji.names.some((name) => splitWords(name).includes(word)))\n return 'name';\n if (emoji.tags.some((tag) => splitWords(tag).includes(word))) return 'tag';\n\n return 'description';\n}\n",
+ "path": "example/lint-emoji-demo.tsx",
+ "target": "components/lint-emoji-demo.tsx",
+ "type": "registry:example"
+ },
+ {
+ "content": "'use client';\n\nimport type { Value } from '@udecode/plate-common';\n\nimport { withProps } from '@udecode/cn';\nimport { AIPlugin } from '@udecode/plate-ai/react';\nimport {\n BoldPlugin,\n CodePlugin,\n ItalicPlugin,\n StrikethroughPlugin,\n SubscriptPlugin,\n SuperscriptPlugin,\n UnderlinePlugin,\n} from '@udecode/plate-basic-marks/react';\nimport { BlockquotePlugin } from '@udecode/plate-block-quote/react';\nimport {\n CodeBlockPlugin,\n CodeLinePlugin,\n CodeSyntaxPlugin,\n} from '@udecode/plate-code-block/react';\nimport { CommentsPlugin } from '@udecode/plate-comments/react';\nimport {\n type CreatePlateEditorOptions,\n ParagraphPlugin,\n PlateLeaf,\n usePlateEditor,\n} from '@udecode/plate-common/react';\nimport { DatePlugin } from '@udecode/plate-date/react';\nimport { EmojiInputPlugin } from '@udecode/plate-emoji/react';\nimport { HEADING_KEYS } from '@udecode/plate-heading';\nimport { TocPlugin } from '@udecode/plate-heading/react';\nimport { HighlightPlugin } from '@udecode/plate-highlight/react';\nimport { HorizontalRulePlugin } from '@udecode/plate-horizontal-rule/react';\nimport { KbdPlugin } from '@udecode/plate-kbd/react';\nimport { ColumnItemPlugin, ColumnPlugin } from '@udecode/plate-layout/react';\nimport { LinkPlugin } from '@udecode/plate-link/react';\nimport {\n AudioPlugin,\n FilePlugin,\n ImagePlugin,\n MediaEmbedPlugin,\n PlaceholderPlugin,\n VideoPlugin,\n} from '@udecode/plate-media/react';\nimport {\n MentionInputPlugin,\n MentionPlugin,\n} from '@udecode/plate-mention/react';\nimport { SlashInputPlugin } from '@udecode/plate-slash-command/react';\nimport {\n TableCellHeaderPlugin,\n TableCellPlugin,\n TablePlugin,\n TableRowPlugin,\n} from '@udecode/plate-table/react';\nimport { TogglePlugin } from '@udecode/plate-toggle/react';\n\nimport { AILeaf } from '@/components/plate-ui/ai-leaf';\nimport { BlockquoteElement } from '@/components/plate-ui/blockquote-element';\nimport { CodeBlockElement } from '@/components/plate-ui/code-block-element';\nimport { CodeLeaf } from '@/components/plate-ui/code-leaf';\nimport { CodeLineElement } from '@/components/plate-ui/code-line-element';\nimport { CodeSyntaxLeaf } from '@/components/plate-ui/code-syntax-leaf';\nimport { ColumnElement } from '@/components/plate-ui/column-element';\nimport { ColumnGroupElement } from '@/components/plate-ui/column-group-element';\nimport { CommentLeaf } from '@/components/plate-ui/comment-leaf';\nimport { DateElement } from '@/components/plate-ui/date-element';\nimport { EmojiInputElement } from '@/components/plate-ui/emoji-input-element';\nimport { HeadingElement } from '@/components/plate-ui/heading-element';\nimport { HighlightLeaf } from '@/components/plate-ui/highlight-leaf';\nimport { HrElement } from '@/components/plate-ui/hr-element';\nimport { ImageElement } from '@/components/plate-ui/image-element';\nimport { KbdLeaf } from '@/components/plate-ui/kbd-leaf';\nimport { LinkElement } from '@/components/plate-ui/link-element';\nimport { MediaAudioElement } from '@/components/plate-ui/media-audio-element';\nimport { MediaEmbedElement } from '@/components/plate-ui/media-embed-element';\nimport { MediaFileElement } from '@/components/plate-ui/media-file-element';\nimport { MediaPlaceholderElement } from '@/components/plate-ui/media-placeholder-element';\nimport { MediaVideoElement } from '@/components/plate-ui/media-video-element';\nimport { MentionElement } from '@/components/plate-ui/mention-element';\nimport { MentionInputElement } from '@/components/plate-ui/mention-input-element';\nimport { ParagraphElement } from '@/components/plate-ui/paragraph-element';\nimport { withPlaceholders } from '@/components/plate-ui/placeholder';\nimport { SlashInputElement } from '@/components/plate-ui/slash-input-element';\nimport {\n TableCellElement,\n TableCellHeaderElement,\n} from '@/components/plate-ui/table-cell-element';\nimport { TableElement } from '@/components/plate-ui/table-element';\nimport { TableRowElement } from '@/components/plate-ui/table-row-element';\nimport { TocElement } from '@/components/plate-ui/toc-element';\nimport { ToggleElement } from '@/components/plate-ui/toggle-element';\nimport { withDraggables } from '@/components/plate-ui/with-draggables';\n\nimport { editorPlugins, viewPlugins } from './plugins/editor-plugins';\n\nexport const viewComponents = {\n [AudioPlugin.key]: MediaAudioElement,\n [BlockquotePlugin.key]: BlockquoteElement,\n [BoldPlugin.key]: withProps(PlateLeaf, { as: 'strong' }),\n [CodeBlockPlugin.key]: CodeBlockElement,\n [CodeLinePlugin.key]: CodeLineElement,\n [CodePlugin.key]: CodeLeaf,\n [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,\n [ColumnItemPlugin.key]: ColumnElement,\n [ColumnPlugin.key]: ColumnGroupElement,\n [CommentsPlugin.key]: CommentLeaf,\n [DatePlugin.key]: DateElement,\n [FilePlugin.key]: MediaFileElement,\n [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: 'h1' }),\n [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: 'h2' }),\n [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: 'h3' }),\n [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: 'h4' }),\n [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: 'h5' }),\n [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: 'h6' }),\n [HighlightPlugin.key]: HighlightLeaf,\n [HorizontalRulePlugin.key]: HrElement,\n [ImagePlugin.key]: ImageElement,\n [ItalicPlugin.key]: withProps(PlateLeaf, { as: 'em' }),\n [KbdPlugin.key]: KbdLeaf,\n [LinkPlugin.key]: LinkElement,\n [MediaEmbedPlugin.key]: MediaEmbedElement,\n [MentionPlugin.key]: MentionElement,\n [ParagraphPlugin.key]: ParagraphElement,\n [PlaceholderPlugin.key]: MediaPlaceholderElement,\n [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: 's' }),\n [SubscriptPlugin.key]: withProps(PlateLeaf, { as: 'sub' }),\n [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: 'sup' }),\n [TableCellHeaderPlugin.key]: TableCellHeaderElement,\n [TableCellPlugin.key]: TableCellElement,\n [TablePlugin.key]: TableElement,\n [TableRowPlugin.key]: TableRowElement,\n [TocPlugin.key]: TocElement,\n [TogglePlugin.key]: ToggleElement,\n [UnderlinePlugin.key]: withProps(PlateLeaf, { as: 'u' }),\n [VideoPlugin.key]: MediaVideoElement,\n};\n\nexport const editorComponents = {\n ...viewComponents,\n [AIPlugin.key]: AILeaf,\n [EmojiInputPlugin.key]: EmojiInputElement,\n [MentionInputPlugin.key]: MentionInputElement,\n [SlashInputPlugin.key]: SlashInputElement,\n};\n\nexport const useCreateEditor = (\n {\n components,\n override,\n readOnly,\n ...options\n }: {\n components?: Record;\n plugins?: any[];\n readOnly?: boolean;\n } & Omit = {},\n deps: any[] = []\n) => {\n return usePlateEditor(\n {\n override: {\n components: {\n ...(readOnly\n ? viewComponents\n : withPlaceholders(withDraggables(editorComponents))),\n ...components,\n },\n ...override,\n },\n plugins: (readOnly ? viewPlugins : editorPlugins) as any,\n ...options,\n },\n deps\n );\n};\n",
+ "path": "components/editor/use-create-editor.ts",
+ "target": "components/use-create-editor.ts",
+ "type": "registry:example"
+ }
+ ],
+ "name": "lint-emoji-demo",
+ "registryDependencies": [
+ "editor",
+ "button",
+ "lint-leaf",
+ "lint-popover"
+ ],
+ "type": "registry:example"
+}
\ No newline at end of file
diff --git a/apps/www/public/r/styles/default/mode-toggle.json b/apps/www/public/r/styles/default/mode-toggle.json
index a73e7d9523..2d77d9f78f 100644
--- a/apps/www/public/r/styles/default/mode-toggle.json
+++ b/apps/www/public/r/styles/default/mode-toggle.json
@@ -1,7 +1,7 @@
{
"files": [
{
- "content": "'use client';\n\nimport * as React from 'react';\n\nimport { MoonIcon, SunIcon } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nimport { useMounted } from '@/hooks/use-mounted';\nimport { Button } from '@/components/plate-ui/button';\n\nexport default function ModeToggle() {\n const { setTheme, theme } = useTheme();\n\n const mounted = useMounted();\n\n return (\n \n );\n}\n",
+ "content": "'use client';\n\nimport * as React from 'react';\n\nimport { MoonIcon, SunIcon } from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nimport { Button } from '@/components/plate-ui/button';\n\nexport default function ModeToggle() {\n const { setTheme, theme } = useTheme();\n\n return (\n \n );\n}\n",
"path": "example/mode-toggle.tsx",
"target": "components/mode-toggle.tsx",
"type": "registry:example"
diff --git a/apps/www/src/__registry__/index.tsx b/apps/www/src/__registry__/index.tsx
index 911bd6352e..ac75d2e57f 100644
--- a/apps/www/src/__registry__/index.tsx
+++ b/apps/www/src/__registry__/index.tsx
@@ -3733,6 +3733,26 @@ export const Index: Record = {
subcategory: "",
chunks: []
},
+ "lint-demo": {
+ name: "lint-demo",
+ description: "",
+ type: "registry:example",
+ registryDependencies: ["editor","button","lint-leaf","lint-popover"],
+ files: [{
+ path: "src/registry/default/example/lint-demo.tsx",
+ type: "registry:example",
+ target: ""
+ },{
+ path: "src/registry/default/components/editor/use-create-editor.ts",
+ type: "registry:example",
+ target: ""
+ }],
+ component: React.lazy(() => import("@/registry/default/example/lint-demo.tsx")),
+ source: "",
+ category: "",
+ subcategory: "",
+ chunks: []
+ },
"mode-toggle": {
name: "mode-toggle",
description: "",
diff --git a/apps/www/src/config/docs-plugins.ts b/apps/www/src/config/docs-plugins.ts
index 59e80a176d..e3229b6fc6 100644
--- a/apps/www/src/config/docs-plugins.ts
+++ b/apps/www/src/config/docs-plugins.ts
@@ -173,6 +173,12 @@ export const pluginsNavItems: SidebarNavItem[] = [
label: 'Element',
title: 'Link',
},
+ {
+ description: 'Lint your document with custom rules.',
+ href: '/docs/lint',
+ label: 'New',
+ title: 'Lint',
+ },
{
description: 'Organize nestable items in a bulleted or numbered list.',
href: '/docs/list',
diff --git a/apps/www/src/registry/default/example/lint-demo.tsx b/apps/www/src/registry/default/example/lint-demo.tsx
new file mode 100644
index 0000000000..49a7b76eef
--- /dev/null
+++ b/apps/www/src/registry/default/example/lint-demo.tsx
@@ -0,0 +1,285 @@
+'use client';
+
+import { BasicMarksPlugin } from '@udecode/plate-basic-marks/react';
+import { Plate, useEditorPlugin } from '@udecode/plate-common/react';
+import {
+ ExperimentalLintPlugin,
+ caseLintPlugin,
+ replaceLintPlugin,
+} from '@udecode/plate-lint/react';
+import { NodeIdPlugin } from '@udecode/plate-node-id';
+import { type Gemoji, gemoji } from 'gemoji';
+
+import {
+ useCreateEditor,
+ viewComponents,
+} from '@/registry/default/components/editor/use-create-editor';
+import { Button } from '@/registry/default/plate-ui/button';
+import { Editor, EditorContainer } from '@/registry/default/plate-ui/editor';
+import { LintLeaf } from '@/registry/default/plate-ui/lint-leaf';
+import { LintPopover } from '@/registry/default/plate-ui/lint-popover';
+
+export default function LintEmojiDemo() {
+ const editor = useCreateEditor({
+ override: {
+ components: viewComponents,
+ },
+ plugins: [
+ ExperimentalLintPlugin.configure({
+ render: {
+ afterEditable: LintPopover,
+ node: LintLeaf,
+ },
+ }),
+ NodeIdPlugin,
+ BasicMarksPlugin,
+ ],
+ value: [
+ {
+ children: [
+ {
+ text: "I'm happy to see my cat and dog. I love them even when I'm sad.",
+ },
+ ],
+ type: 'p',
+ },
+ {
+ children: [
+ {
+ text: 'I like to eat pizza and ice cream.',
+ },
+ ],
+ type: 'p',
+ },
+ {
+ children: [
+ {
+ text: 'hello world! this is a test. new sentence here. the cat is happy.',
+ },
+ ],
+ type: 'p',
+ },
+ ],
+ });
+
+ return (
+
+ );
+}
+
+function EmojiPlateEditorContent() {
+ const { api, editor } = useEditorPlugin(ExperimentalLintPlugin);
+
+ const runFirst = () => {
+ api.lint.run([
+ {
+ ...replaceLintPlugin.configs.all,
+ targets: [{ id: editor.children[0].id as string }],
+ },
+ {
+ settings: {
+ replace: {
+ replaceMap: emojiMap,
+ },
+ },
+ },
+ ]);
+ };
+
+ const runMax = () => {
+ api.lint.run([
+ replaceLintPlugin.configs.all,
+ {
+ settings: {
+ replace: {
+ parserOptions: {
+ maxLength: 4,
+ },
+ replaceMap: emojiMap,
+ },
+ },
+ },
+ ]);
+ };
+
+ const runCase = () => {
+ api.lint.run([
+ caseLintPlugin.configs.all,
+ {
+ settings: {
+ case: {
+ ignoredWords: ['iPhone', 'iOS', 'iPad'],
+ },
+ },
+ },
+ ]);
+ };
+
+ const runBoth = () => {
+ api.lint.run([
+ replaceLintPlugin.configs.all,
+ caseLintPlugin.configs.all,
+ {
+ settings: {
+ case: {
+ ignoredWords: ['iPhone', 'iOS', 'iPad'],
+ },
+ replace: {
+ replaceMap: emojiMap,
+ },
+ },
+ },
+ ]);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+const excludeWords = new Set([
+ 'a',
+ 'an',
+ 'and',
+ 'are',
+ 'as',
+ 'at',
+ 'be',
+ 'but',
+ 'by',
+ 'for',
+ 'from',
+ 'if',
+ 'in',
+ 'into',
+ 'is',
+ 'it',
+ 'no',
+ 'not',
+ 'of',
+ 'on',
+ 'or',
+ 'such',
+ 'that',
+ 'the',
+ 'their',
+ 'then',
+ 'there',
+ 'these',
+ 'they',
+ 'this',
+ 'to',
+ 'was',
+ 'was',
+ 'will',
+ 'with',
+]);
+
+type WordSource = 'description' | 'exact_name' | 'name' | 'tag';
+
+function splitWords(text: string): string[] {
+ return text.toLowerCase().split(/[^\d_a-z]+/);
+}
+
+const emojiMap = new Map<
+ string,
+ (Gemoji & { text: string; type: 'emoji' })[]
+>();
+
+gemoji.forEach((emoji) => {
+ const wordSources = new Map();
+
+ // Priority 1: Exact name matches (highest priority)
+ emoji.names.forEach((name) => {
+ const nameLower = name.toLowerCase();
+ splitWords(name).forEach((word) => {
+ if (!excludeWords.has(word)) {
+ // If the name is exactly this word, it gets highest priority
+ wordSources.set(word, word === nameLower ? 'exact_name' : 'name');
+ }
+ });
+ });
+
+ // Priority 3: Tags
+ emoji.tags.forEach((tag) => {
+ splitWords(tag).forEach((word) => {
+ if (!excludeWords.has(word) && !wordSources.has(word)) {
+ wordSources.set(word, 'tag');
+ }
+ });
+ });
+
+ // Priority 4: Description (lowest priority)
+ if (emoji.description) {
+ splitWords(emoji.description).forEach((word) => {
+ if (!excludeWords.has(word) && !wordSources.has(word)) {
+ wordSources.set(word, 'description');
+ }
+ });
+ }
+
+ wordSources.forEach((source, word) => {
+ if (!emojiMap.has(word)) {
+ emojiMap.set(word, []);
+ }
+
+ const emojis = emojiMap.get(word)!;
+
+ const insertIndex = emojis.findIndex((e) => {
+ const existingSource = getWordSource(e, word);
+
+ return source > existingSource;
+ });
+
+ if (insertIndex === -1) {
+ emojis.push({
+ ...emoji,
+ text: emoji.emoji,
+ type: 'emoji',
+ });
+ } else {
+ emojis.splice(insertIndex, 0, {
+ ...emoji,
+ text: emoji.emoji,
+ type: 'emoji',
+ });
+ }
+ });
+});
+
+function getWordSource(emoji: Gemoji, word: string): WordSource {
+ // Check for exact name match first
+ if (emoji.names.some((name) => name.toLowerCase() === word))
+ return 'exact_name';
+ // Then check for partial name matches
+ if (emoji.names.some((name) => splitWords(name).includes(word)))
+ return 'name';
+ if (emoji.tags.some((tag) => splitWords(tag).includes(word))) return 'tag';
+
+ return 'description';
+}
diff --git a/apps/www/src/registry/default/example/mode-toggle.tsx b/apps/www/src/registry/default/example/mode-toggle.tsx
index a60fa954b9..f9d8af150f 100644
--- a/apps/www/src/registry/default/example/mode-toggle.tsx
+++ b/apps/www/src/registry/default/example/mode-toggle.tsx
@@ -5,14 +5,11 @@ import * as React from 'react';
import { MoonIcon, SunIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
-import { useMounted } from '@/registry/default/hooks/use-mounted';
import { Button } from '@/registry/default/plate-ui/button';
export default function ModeToggle() {
const { setTheme, theme } = useTheme();
- const mounted = useMounted();
-
return (