The fix is complete. Here's a summary of the changes: (#1441)
**Changes made:** 1. **`frontend/src/components/ui/wysiwyg.tsx`**: - Removed imports for `INLINE_CODE`, `InlineCodeNode`, and `INLINE_CODE_TRANSFORMER` - Removed `InlineCodeNode` from the registered nodes array - Simplified transformers to use Lexical's built-in `TRANSFORMERS` (which includes `INLINE_CODE`) 2. **Deleted files:** - `frontend/src/components/ui/wysiwyg/nodes/inline-code-node.tsx` - `frontend/src/components/ui/wysiwyg/transformers/inline-code-transformer.ts` **What this fixes:** 1. **Escaping bug** - Backticks will no longer be incorrectly escaped when saving inline code 2. **Cannot edit inline code** - You can now edit characters within inline code blocks (TextNode is editable, unlike DecoratorNode which was atomic) The TypeScript and lint checks pass. You can test by: 1. Creating a task with inline code like `` `console.log()` `` 2. Saving and reloading - no backslashes should appear 3. Clicking inside inline code and editing individual characters
This commit is contained in:
committed by
GitHub
parent
d81be475c5
commit
86705f9c8e
@@ -5,12 +5,10 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|||||||
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
||||||
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||||
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
||||||
import { TRANSFORMERS, INLINE_CODE, type Transformer } from '@lexical/markdown';
|
import { TRANSFORMERS, type Transformer } from '@lexical/markdown';
|
||||||
import { ImageNode } from './wysiwyg/nodes/image-node';
|
import { ImageNode } from './wysiwyg/nodes/image-node';
|
||||||
import { InlineCodeNode } from './wysiwyg/nodes/inline-code-node';
|
|
||||||
import { IMAGE_TRANSFORMER } from './wysiwyg/transformers/image-transformer';
|
import { IMAGE_TRANSFORMER } from './wysiwyg/transformers/image-transformer';
|
||||||
import { CODE_BLOCK_TRANSFORMER } from './wysiwyg/transformers/code-block-transformer';
|
import { CODE_BLOCK_TRANSFORMER } from './wysiwyg/transformers/code-block-transformer';
|
||||||
import { INLINE_CODE_TRANSFORMER } from './wysiwyg/transformers/inline-code-transformer';
|
|
||||||
import {
|
import {
|
||||||
TaskAttemptContext,
|
TaskAttemptContext,
|
||||||
TaskContext,
|
TaskContext,
|
||||||
@@ -142,21 +140,14 @@ function WYSIWYGEditor({
|
|||||||
CodeHighlightNode,
|
CodeHighlightNode,
|
||||||
LinkNode,
|
LinkNode,
|
||||||
ImageNode,
|
ImageNode,
|
||||||
InlineCodeNode,
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Extended transformers with image and code block support (memoized to prevent unnecessary re-renders)
|
// Extended transformers with image and code block support (memoized to prevent unnecessary re-renders)
|
||||||
// Filter out default INLINE_CODE to use our custom INLINE_CODE_TRANSFORMER with syntax highlighting
|
|
||||||
const extendedTransformers: Transformer[] = useMemo(
|
const extendedTransformers: Transformer[] = useMemo(
|
||||||
() => [
|
() => [IMAGE_TRANSFORMER, CODE_BLOCK_TRANSFORMER, ...TRANSFORMERS],
|
||||||
IMAGE_TRANSFORMER,
|
|
||||||
CODE_BLOCK_TRANSFORMER,
|
|
||||||
INLINE_CODE_TRANSFORMER,
|
|
||||||
...TRANSFORMERS.filter((t) => t !== INLINE_CODE),
|
|
||||||
],
|
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import {
|
|
||||||
DecoratorNode,
|
|
||||||
DOMConversionMap,
|
|
||||||
DOMExportOutput,
|
|
||||||
LexicalNode,
|
|
||||||
NodeKey,
|
|
||||||
SerializedLexicalNode,
|
|
||||||
Spread,
|
|
||||||
} from 'lexical';
|
|
||||||
import { PrismTokenizer } from '@lexical/code';
|
|
||||||
import { CODE_HIGHLIGHT_CLASSES } from '../lib/code-highlight-theme';
|
|
||||||
|
|
||||||
export type SerializedInlineCodeNode = Spread<
|
|
||||||
{
|
|
||||||
code: string;
|
|
||||||
},
|
|
||||||
SerializedLexicalNode
|
|
||||||
>;
|
|
||||||
|
|
||||||
interface Token {
|
|
||||||
type: string;
|
|
||||||
content: string | Token | (string | Token)[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderToken(token: string | Token, index: number): React.ReactNode {
|
|
||||||
if (typeof token === 'string') {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = CODE_HIGHLIGHT_CLASSES[token.type] || '';
|
|
||||||
|
|
||||||
// Handle nested tokens
|
|
||||||
let content: React.ReactNode;
|
|
||||||
if (typeof token.content === 'string') {
|
|
||||||
content = token.content;
|
|
||||||
} else if (Array.isArray(token.content)) {
|
|
||||||
content = token.content.map((t, i) => renderToken(t, i));
|
|
||||||
} else {
|
|
||||||
content = renderToken(token.content, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span key={index} className={className}>
|
|
||||||
{content}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function InlineCodeComponent({ code }: { code: string }): JSX.Element {
|
|
||||||
// Use PrismTokenizer to tokenize the code
|
|
||||||
const tokens = PrismTokenizer.tokenize(code);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<code className="font-mono bg-muted px-1 py-0.5 rounded text-sm">
|
|
||||||
{tokens.map((token, index) => renderToken(token, index))}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InlineCodeNode extends DecoratorNode<JSX.Element> {
|
|
||||||
__code: string;
|
|
||||||
|
|
||||||
static getType(): string {
|
|
||||||
return 'inline-code';
|
|
||||||
}
|
|
||||||
|
|
||||||
static clone(node: InlineCodeNode): InlineCodeNode {
|
|
||||||
return new InlineCodeNode(node.__code, node.__key);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(code: string, key?: NodeKey) {
|
|
||||||
super(key);
|
|
||||||
this.__code = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
createDOM(): HTMLElement {
|
|
||||||
const span = document.createElement('span');
|
|
||||||
return span;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDOM(): false {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedInlineCodeNode): InlineCodeNode {
|
|
||||||
const { code } = serializedNode;
|
|
||||||
return $createInlineCodeNode(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
exportJSON(): SerializedInlineCodeNode {
|
|
||||||
return {
|
|
||||||
type: 'inline-code',
|
|
||||||
version: 1,
|
|
||||||
code: this.__code,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
|
||||||
return {
|
|
||||||
code: (domNode: HTMLElement) => {
|
|
||||||
// Only import inline code elements (not block code)
|
|
||||||
const isBlock =
|
|
||||||
domNode.parentElement?.tagName === 'PRE' ||
|
|
||||||
domNode.style.display === 'block';
|
|
||||||
if (isBlock) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
conversion: (node: HTMLElement) => {
|
|
||||||
const code = node.textContent || '';
|
|
||||||
return { node: $createInlineCodeNode(code) };
|
|
||||||
},
|
|
||||||
priority: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
exportDOM(): DOMExportOutput {
|
|
||||||
const code = document.createElement('code');
|
|
||||||
code.textContent = this.__code;
|
|
||||||
return { element: code };
|
|
||||||
}
|
|
||||||
|
|
||||||
getCode(): string {
|
|
||||||
return this.__code;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTextContent(): string {
|
|
||||||
return this.__code;
|
|
||||||
}
|
|
||||||
|
|
||||||
decorate(): JSX.Element {
|
|
||||||
return <InlineCodeComponent code={this.__code} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInline(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
isKeyboardSelectable(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $createInlineCodeNode(code: string): InlineCodeNode {
|
|
||||||
return new InlineCodeNode(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $isInlineCodeNode(
|
|
||||||
node: LexicalNode | null | undefined
|
|
||||||
): node is InlineCodeNode {
|
|
||||||
return node instanceof InlineCodeNode;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { TextMatchTransformer } from '@lexical/markdown';
|
|
||||||
import {
|
|
||||||
$createInlineCodeNode,
|
|
||||||
$isInlineCodeNode,
|
|
||||||
InlineCodeNode,
|
|
||||||
} from '../nodes/inline-code-node';
|
|
||||||
|
|
||||||
export const INLINE_CODE_TRANSFORMER: TextMatchTransformer = {
|
|
||||||
dependencies: [InlineCodeNode],
|
|
||||||
export: (node) => {
|
|
||||||
if ($isInlineCodeNode(node)) {
|
|
||||||
return '`' + node.getCode() + '`';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
// Match backtick-wrapped code during import (paste)
|
|
||||||
importRegExp: /`([^`]+)`/,
|
|
||||||
// Match at end of line while typing
|
|
||||||
regExp: /`([^`]+)`$/,
|
|
||||||
replace: (textNode, match) => {
|
|
||||||
const [, code] = match;
|
|
||||||
const inlineCodeNode = $createInlineCodeNode(code);
|
|
||||||
textNode.replace(inlineCodeNode);
|
|
||||||
},
|
|
||||||
trigger: '`',
|
|
||||||
type: 'text-match',
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user