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:
Louis Knight-Webb
2025-12-05 16:25:23 +00:00
committed by GitHub
parent d81be475c5
commit 86705f9c8e
3 changed files with 2 additions and 192 deletions

View File

@@ -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],
[]
);

View File

@@ -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;
}

View File

@@ -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',
};