Skip to content

Commit

Permalink
feat
Browse files Browse the repository at this point in the history
  • Loading branch information
zbeyens committed Dec 7, 2024
1 parent ae20b36 commit c112c1a
Show file tree
Hide file tree
Showing 28 changed files with 1,165 additions and 806 deletions.
2 changes: 1 addition & 1 deletion apps/www/content/docs/lint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This package is experimental. Expect breaking changes in future releases.

</Callout>

<ComponentPreview name="lint-emoji-demo" />
<ComponentPreview name="lint-demo" />

<PackageInfo>

Expand Down
38 changes: 38 additions & 0 deletions apps/www/public/r/styles/default/lint-demo.json
Original file line number Diff line number Diff line change
@@ -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 <div className=\"mx-auto flex max-w-md flex-col items-center justify-center p-4\">\n <Plate editor={editor}>\n <EmojiPlateEditorContent />\n </Plate>\n </div>\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 <div className=\"mb-4 flex gap-4\">\n <Button size=\"md\" className=\"mb-4 px-4\" onClick={runFirst}>\n Emoji\n </Button>\n <Button size=\"md\" className=\"mb-4 px-4\" onClick={runMax}>\n Max Length\n </Button>\n <Button size=\"md\" className=\"mb-4 px-4\" onClick={runCase}>\n Case\n </Button>\n <Button size=\"md\" className=\"mb-4 px-4\" onClick={runBoth}>\n Both\n </Button>\n <Button size=\"md\" className=\"mb-4 px-4\" onClick={api.lint.reset}>\n Reset\n </Button>\n </div>\n <EditorContainer>\n <Editor variant=\"demo\" placeholder=\"Type...\" />\n </EditorContainer>\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<string, WordSource>();\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<string, any>;\n plugins?: any[];\n readOnly?: boolean;\n } & Omit<CreatePlateEditorOptions, 'plugins'> = {},\n deps: any[] = []\n) => {\n return usePlateEditor<Value, (typeof editorPlugins)[number]>(\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"
}
2 changes: 1 addition & 1 deletion apps/www/public/r/styles/default/mode-toggle.json
Original file line number Diff line number Diff line change
@@ -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 <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"size-8 px-0\"\n onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}\n >\n {mounted && theme === 'dark' ? (\n <MoonIcon className=\"size-[1.2rem]\" />\n ) : (\n <SunIcon className=\"size-[1.2rem]\" />\n )}\n <span className=\"sr-only\">Toggle theme</span>\n </Button>\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 <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"size-8 px-0\"\n onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}\n >\n {theme === 'dark' ? (\n <MoonIcon className=\"size-[1.2rem]\" />\n ) : (\n <SunIcon className=\"size-[1.2rem]\" />\n )}\n <span className=\"sr-only\">Toggle theme</span>\n </Button>\n );\n}\n",
"path": "example/mode-toggle.tsx",
"target": "components/mode-toggle.tsx",
"type": "registry:example"
Expand Down
8 changes: 4 additions & 4 deletions apps/www/src/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3733,21 +3733,21 @@ export const Index: Record<string, any> = {
subcategory: "",
chunks: []
},
"lint-emoji-demo": {
name: "lint-emoji-demo",
"lint-demo": {
name: "lint-demo",
description: "",
type: "registry:example",
registryDependencies: ["editor","button","lint-leaf","lint-popover"],
files: [{
path: "src/registry/default/example/lint-emoji-demo.tsx",
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-emoji-demo.tsx")),
component: React.lazy(() => import("@/registry/default/example/lint-demo.tsx")),
source: "",
category: "",
subcategory: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,14 @@ function EmojiPlateEditorContent() {
api.lint.run([
{
...replaceLintPlugin.configs.all,
targets: [{ id: editor.children[0].id as string }],
},
{
settings: {
replaceMap: emojiMap,
replace: {
replaceMap: emojiMap,
},
},
targets: [{ id: editor.children[0].id as string }],
},
]);
};
Expand All @@ -90,13 +94,13 @@ function EmojiPlateEditorContent() {
api.lint.run([
replaceLintPlugin.configs.all,
{
languageOptions: {
parserOptions: {
maxLength: 4,
},
},
settings: {
replaceMap: emojiMap,
replace: {
parserOptions: {
maxLength: 4,
},
replaceMap: emojiMap,
},
},
},
]);
Expand All @@ -107,7 +111,9 @@ function EmojiPlateEditorContent() {
caseLintPlugin.configs.all,
{
settings: {
ignoredWords: ['iPhone', 'iOS', 'iPad'],
case: {
ignoredWords: ['iPhone', 'iOS', 'iPad'],
},
},
},
]);
Expand All @@ -119,8 +125,12 @@ function EmojiPlateEditorContent() {
caseLintPlugin.configs.all,
{
settings: {
ignoredWords: ['iPhone', 'iOS', 'iPad'],
replaceMap: emojiMap,
case: {
ignoredWords: ['iPhone', 'iOS', 'iPad'],
},
replace: {
replaceMap: emojiMap,
},
},
},
]);
Expand Down
20 changes: 15 additions & 5 deletions apps/www/src/registry/default/plate-ui/lint-leaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@ export const LintLeaf = withRef<typeof PlateLeaf>(
<PlateLeaf
ref={ref}
as="mark"
className={cn('bg-inherit text-red-400', className)}
onClick={(e) => {
e.preventDefault();
console.log(leaf.annotation);
setOption('activeAnnotation', leaf.annotation);
className={cn(
'bg-inherit',
leaf.annotations.some((annotation) => annotation.type === 'emoji') &&
'text-orange-400',
leaf.annotations.some(
(annotation) => annotation.type === undefined
) &&
'underline decoration-red-500 underline-offset-2 selection:underline selection:decoration-red-500',

className
)}
onMouseDown={() => {
setTimeout(() => {
setOption('activeAnnotations', leaf.annotations);
}, 0);
}}
{...props}
>
Expand Down
Loading

0 comments on commit c112c1a

Please sign in to comment.