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 { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
||||
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 { InlineCodeNode } from './wysiwyg/nodes/inline-code-node';
|
||||
import { IMAGE_TRANSFORMER } from './wysiwyg/transformers/image-transformer';
|
||||
import { CODE_BLOCK_TRANSFORMER } from './wysiwyg/transformers/code-block-transformer';
|
||||
import { INLINE_CODE_TRANSFORMER } from './wysiwyg/transformers/inline-code-transformer';
|
||||
import {
|
||||
TaskAttemptContext,
|
||||
TaskContext,
|
||||
@@ -142,21 +140,14 @@ function WYSIWYGEditor({
|
||||
CodeHighlightNode,
|
||||
LinkNode,
|
||||
ImageNode,
|
||||
InlineCodeNode,
|
||||
],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// 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(
|
||||
() => [
|
||||
IMAGE_TRANSFORMER,
|
||||
CODE_BLOCK_TRANSFORMER,
|
||||
INLINE_CODE_TRANSFORMER,
|
||||
...TRANSFORMERS.filter((t) => t !== INLINE_CODE),
|
||||
],
|
||||
() => [IMAGE_TRANSFORMER, CODE_BLOCK_TRANSFORMER, ...TRANSFORMERS],
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
@@ -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