Skip to content
10 changes: 10 additions & 0 deletions components/drops/create/lexical/lexical.styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@
height: 200px;
}

.editor-code,
.editor-text-code {
color: #e5e7eb;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}

.editor-code {
background-color: transparent;
}

.editor-nested-listitem {
list-style-type: none;
}
51 changes: 51 additions & 0 deletions components/drops/create/lexical/plugins/PlainTextPastePlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
COMMAND_PRIORITY_LOW,
PASTE_COMMAND,
$getSelection,
$isRangeSelection,
} from "lexical";
import { useEffect } from "react";

const TEXT_MIME_TYPE = "text/plain";

export default function PlainTextPastePlugin(): null {
const [editor] = useLexicalComposerContext();

useEffect(() => {
return editor.registerCommand<ClipboardEvent>(
PASTE_COMMAND,
(event) => {
const clipboardData = event.clipboardData;
if (!clipboardData) {
return false;
}

if (clipboardData.files.length > 0) {
return false;
}

const text = clipboardData.getData(TEXT_MIME_TYPE);
if (!text.length) {
return false;
}

event.preventDefault();

editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertRawText(text);
}
});

return true;
},
COMMAND_PRIORITY_LOW
);
}, [editor]);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import HashtagsTypeaheadMenu from "./HashtagsTypeaheadMenu";
import { isEthereumAddress } from "../../../../../../helpers/AllowlistToolHelpers";
import { ReferencedNft } from "../../../../../../entities/IDrop";
import { ReservoirTokensResponseTokenElement } from "../../../../../../entities/IReservoir";
import { isInCodeContext } from "../../utils/codeContextDetection";

const PUNCTUATION =
"\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%'\"~=<>_:;";
Expand Down Expand Up @@ -238,6 +239,10 @@ const NewHashtagsPlugin = forwardRef<

const checkForHashtagMatch = useCallback(
(text: string) => {
if (isInCodeContext(editor)) {
return null;
}

const slashMatch = checkForSlashTriggerMatch(text, editor);
if (slashMatch !== null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { $createMentionNode } from "../../nodes/MentionNode";
import MentionsTypeaheadMenu from "./MentionsTypeaheadMenu";
import { MentionedUser } from "../../../../../../entities/IDrop";
import { useIdentitiesSearch } from "../../../../../../hooks/useIdentitiesSearch";
import { isInCodeContext } from "../../utils/codeContextDetection";

const PUNCTUATION =
"\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%'\"~=<>_:;";
Expand Down Expand Up @@ -210,6 +211,10 @@ const NewMentionsPlugin = forwardRef<

const checkForMentionMatch = useCallback(
(text: string) => {
if (isInCodeContext(editor)) {
return null;
}

const slashMatch = checkForSlashTriggerMatch(text, editor);
if (slashMatch !== null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TRANSFORMERS, type Transformer } from "@lexical/markdown";

const UNDERSCORE_TAGS = new Set(["__", "___", "_"]);

const isObjectTransformer = (
transformer: unknown
): transformer is Transformer =>
typeof transformer === "object" && transformer !== null;

const BASE_SAFE_TRANSFORMERS = TRANSFORMERS.filter((transformer) => {
if (!isObjectTransformer(transformer)) {
return true;
}

const maybeTag = (transformer as { tag?: unknown }).tag;
if (typeof maybeTag !== "string") {
return true;
}

return !UNDERSCORE_TAGS.has(maybeTag);
});

const isCodeTransformer = (transformer: Transformer): boolean => {
const dependencies = (transformer as { dependencies?: unknown }).dependencies;
if (!Array.isArray(dependencies)) {
return false;
}

return dependencies.some((dependency) => {
if (
!dependency ||
typeof dependency !== "object" ||
typeof (dependency as { getType?: unknown }).getType !== "function"
) {
return false;
}

return (dependency as { getType: () => string }).getType() === "code";
});
};

export const SAFE_MARKDOWN_TRANSFORMERS = BASE_SAFE_TRANSFORMERS;

export const SAFE_MARKDOWN_TRANSFORMERS_WITHOUT_CODE =
BASE_SAFE_TRANSFORMERS.filter(
(transformer) =>
!isObjectTransformer(transformer) || !isCodeTransformer(transformer)
);
34 changes: 34 additions & 0 deletions components/drops/create/lexical/utils/codeContextDetection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { $getSelection, $isRangeSelection } from "lexical";
import type { LexicalEditor, LexicalNode } from "lexical";
import { $isCodeNode } from "@lexical/code";

export function isInCodeContext(editor: LexicalEditor): boolean {
return editor.getEditorState().read(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return false;
}

if (selection.hasFormat("code")) {
return true;
}

const anchorNode = selection.anchor.getNode();
const focusNode = selection.focus.getNode();

const isNodeWithinCode = (node: LexicalNode | null) => {
if (!node) {
return false;
}

if ($isCodeNode(node)) {
return true;
}

const topLevel = node.getTopLevelElement();
return $isCodeNode(topLevel);
};

return isNodeWithinCode(anchorNode) || isNodeWithinCode(focusNode);
});
}
9 changes: 6 additions & 3 deletions components/drops/create/utils/CreateDropContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { MaxLengthPlugin } from "../lexical/plugins/MaxLengthPlugin";
import ToggleViewButtonPlugin from "../lexical/plugins/ToggleViewButtonPlugin";
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
import { $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown";
import { $convertToMarkdownString } from "@lexical/markdown";

import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
import { ListNode, ListItemNode } from "@lexical/list";
Expand Down Expand Up @@ -63,11 +63,13 @@ import { ImageNode } from "../lexical/nodes/ImageNode";
import CreateDropParts from "./storm/CreateDropParts";
import CreateDropActionsRow from "./CreateDropActionsRow";
import { IMAGE_TRANSFORMER } from "../lexical/transformers/ImageTransformer";
import { SAFE_MARKDOWN_TRANSFORMERS } from "../lexical/transformers/markdownTransformers";
import EnterKeyPlugin from "../lexical/plugins/enter/EnterKeyPlugin";
import AutoFocusPlugin from "../lexical/plugins/AutoFocusPlugin";
import { EmojiNode } from "../lexical/nodes/EmojiNode";
import CreateDropEmojiPicker from "../../../waves/CreateDropEmojiPicker";
import EmojiPlugin from "../lexical/plugins/emoji/EmojiPlugin";
import PlainTextPastePlugin from "../lexical/plugins/PlainTextPastePlugin";

export interface CreateDropContentHandles {
clearEditorState: () => void;
Expand Down Expand Up @@ -194,7 +196,7 @@ const CreateDropContent = forwardRef<
editorState?.read(() =>
setCharsCount(
$convertToMarkdownString([
...TRANSFORMERS,
...SAFE_MARKDOWN_TRANSFORMERS,
MENTION_TRANSFORMER,
HASHTAG_TRANSFORMER,
IMAGE_TRANSFORMER,
Expand Down Expand Up @@ -283,7 +285,8 @@ const CreateDropContent = forwardRef<
<MaxLengthPlugin maxLength={25000} />
<DragDropPastePlugin />
<ListPlugin />
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
<PlainTextPastePlugin />
<MarkdownShortcutPlugin transformers={SAFE_MARKDOWN_TRANSFORMERS} />
<TabIndentationPlugin />
<LinkPlugin validateUrl={validateUrl} />
<ClearEditorPlugin ref={clearEditorRef} />
Expand Down
5 changes: 3 additions & 2 deletions components/drops/create/utils/CreateDropWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
ReferencedNft,
} from "../../../../entities/IDrop";
import { createBreakpoint } from "react-use";
import { $convertToMarkdownString, TRANSFORMERS } from "@lexical/markdown";
import { $convertToMarkdownString } from "@lexical/markdown";
import { CreateDropType, CreateDropViewType } from "../types";
import { MENTION_TRANSFORMER } from "../lexical/transformers/MentionTransformer";
import { HASHTAG_TRANSFORMER } from "../lexical/transformers/HastagTransformer";
Expand All @@ -34,6 +34,7 @@ import { ApiWaveMetadataType } from "../../../../generated/models/ApiWaveMetadat
import { ApiWaveParticipationRequirement } from "../../../../generated/models/ApiWaveParticipationRequirement";
import { ProfileMinWithoutSubs } from "../../../../helpers/ProfileTypes";
import { IMAGE_TRANSFORMER } from "../lexical/transformers/ImageTransformer";
import { SAFE_MARKDOWN_TRANSFORMERS } from "../lexical/transformers/markdownTransformers";
import { QueryKey } from "../../../react-query-wrapper/ReactQueryWrapper";
import { useSeizeConnectContext } from "@/components/auth/SeizeConnectContext";
import { WalletValidationError } from "../../../../src/errors/wallet";
Expand Down Expand Up @@ -195,7 +196,7 @@ const CreateDropWrapper = forwardRef<
const getMarkdown = () =>
editorState?.read(() =>
$convertToMarkdownString([
...TRANSFORMERS,
...SAFE_MARKDOWN_TRANSFORMERS,
MENTION_TRANSFORMER,
HASHTAG_TRANSFORMER,
IMAGE_TRANSFORMER,
Expand Down
88 changes: 76 additions & 12 deletions components/drops/view/part/DropPartMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
memo,
useEffect,
useMemo,
useRef,
type ComponentPropsWithoutRef,
type ElementType,
type ReactNode,
Expand All @@ -25,6 +27,7 @@ import {
createLinkRenderer,

} from "./dropPartMarkdown/linkHandlers";
import { highlightCodeElement } from "./dropPartMarkdown/highlight";

const BreakComponent = () => <br />;

Expand Down Expand Up @@ -126,6 +129,8 @@ function DropPartMarkdown({
allowedAttributes: {
a: ["href", "title"],
img: ["src", "alt", "title"],
code: ["className"],
pre: ["className"],
},
},
],
Expand Down Expand Up @@ -170,6 +175,71 @@ function DropPartMarkdown({
return HeadingRenderer;
};

type MarkdownCodeProps = MarkdownRendererProps<"code"> & {
inline?: boolean;
};

const InlineCodeRenderer = ({
children,
className,
style,
...props
}: MarkdownCodeProps) => (
<code
{...props}
style={{ ...style, textOverflow: "unset" }}
className={mergeClassNames(
"tw-text-iron-200 tw-whitespace-pre-wrap tw-break-words",
className
)}
>
{children}
</code>
);

const CodeBlockRenderer = ({
children,
className,
style,
...props
}: MarkdownCodeProps) => {
const codeRef = useRef<HTMLElement>(null);

const language = useMemo(() => {
const match = typeof className === "string"
? /language-([\w+-]+)/.exec(className)
: null;
return match?.[1] ?? null;
}, [className]);

useEffect(() => {
if (typeof window === "undefined") {
return;
}

const element = codeRef.current;
if (!element) {
return;
}

void highlightCodeElement(element, language);
}, [language, children]);

return (
<code
{...props}
ref={codeRef}
style={{ ...style, textOverflow: "unset" }}
className={mergeClassNames(
"tw-text-iron-200 tw-whitespace-pre-wrap tw-break-words",
className
)}
>
{children}
</code>
);
};

return {
h1: createHeadingRenderer("h1"),
h2: createHeadingRenderer("h2"),
Expand All @@ -188,18 +258,12 @@ function DropPartMarkdown({
{customRenderer(children)}
</li>
),
code: ({ children, className, style, ...props }) => (
<code
{...props}
style={{ ...style, textOverflow: "unset" }}
className={mergeClassNames(
"tw-text-iron-200 tw-whitespace-pre-wrap tw-break-words",
className
)}
>
{customRenderer(children)}
</code>
),
code: ({ inline, ...props }: MarkdownCodeProps) =>
inline ? (
<InlineCodeRenderer {...props} />
) : (
<CodeBlockRenderer {...props} />
),
a: renderAnchor,
img: renderImage,
br: BreakComponent,
Expand Down
Loading