Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import localFont from 'next/font/local';

import './globals.css';
import 'katex/dist/katex.min.css';
import { Metadata } from 'next';
import { getServerSession } from 'next-auth';
import { authOptions } from './api/auth/[...nextauth]/auth.config';
Expand Down
1 change: 0 additions & 1 deletion app/notebook/NotebookClientLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { ReactNode, useState } from 'react';
import './globals.css';
import 'cal-sans/index.css';
import 'katex/dist/katex.min.css';

import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
Expand Down
22 changes: 22 additions & 0 deletions components/Comment/components/EditorToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Link2,
ImageIcon,
AtSign,
Calculator,
ChevronLeft,
ChevronRight,
} from 'lucide-react';
Expand Down Expand Up @@ -224,6 +225,27 @@
{inDropdown && <span className="ml-2">Add Image</span>}
</ToolbarButton>

<ToolbarButton
onClick={() => {
const { from, to, empty } = editor.state.selection;
const selected = empty ? '' : editor.state.doc.textBetween(from, to, ' ');
const latex = window.prompt('Enter a LaTeX equation', selected || 'E = mc^2');
if (latex == null || !latex.trim()) return;

Check warning on line 233 in components/Comment/components/EditorToolbar.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using an optional chain expression instead, as it's more concise and easier to read.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaF05mTFGh8uurdk4m5L&open=AaF05mTFGh8uurdk4m5L&pullRequest=1249
const chain = editor.chain().focus();
if (!empty) {
chain.deleteSelection();
}
chain.insertInlineMath({ latex: latex.trim() }).run();
}}
isActive={editor.isActive('inlineMath')}
tooltip="Insert equation"
className={buttonClasses}
inDropdown={inDropdown}
>
<Calculator className="h-4 w-4" />
{inDropdown && <span className="ml-2">Equation</span>}
</ToolbarButton>

{showMentions && (
<ToolbarButton
onClick={() => {
Expand Down
52 changes: 52 additions & 0 deletions components/Comment/lib/KatexMath.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

import katex from 'katex';
import { cn } from '@/utils/styles';

/** `$...$` inline only — excludes `$$` so display math is not partially eaten. */
export const INLINE_MATH_REGEX = /(?<!\$)\$(?!\$|\d+\$)(.+?)(?<!\$)\$(?!\$|\d)/g;

export const BLOCK_MATH_REGEX = /^\$\$([\s\S]+?)\$\$$/;

interface KatexMathProps {
latex: string;
display?: boolean;
className?: string;
}

/**
* Renders a LaTeX string with KaTeX. Falls back to the raw source when
* KaTeX cannot parse it so comments stay readable instead of blank.
*/
export function KatexMath({ latex, display = false, className }: KatexMathProps) {

Check warning on line 21 in components/Comment/lib/KatexMath.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaF05mZUGh8uurdk4m5M&open=AaF05mZUGh8uurdk4m5M&pullRequest=1249
try {
const html = katex.renderToString(latex, {
displayMode: display,
throwOnError: false,
});
return (
<span
className={cn(
'tiptap-math',
display
? 'tiptap-block-math my-3 block overflow-x-auto text-center'
: 'tiptap-inline-math',
className
)}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
} catch {
return (
<code
className={cn(
'tiptap-math-error bg-gray-100 px-1 rounded text-sm',
display && 'my-3 block overflow-x-auto',
className
)}
>
{latex}
</code>
);
}
}
82 changes: 79 additions & 3 deletions components/Comment/lib/TipTapRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client';

import React, { ReactNode, useState } from 'react';
import React, { Fragment, ReactNode, useState } from 'react';
import { SectionHeaderProps } from './renderUtils';
import hljs from 'highlight.js';
import { useEffect } from 'react';
import { buildWorkUrl, classifyUrl } from '@/utils/url';
import { navigateToAuthorProfile } from '@/utils/navigation';
import { InlineRichLink } from '@/components/Embed';
import { normalizeRichLinks } from './embedDoc';
import { BLOCK_MATH_REGEX, INLINE_MATH_REGEX, KatexMath } from './KatexMath';

interface TipTapRendererProps {
content: any;
Expand Down Expand Up @@ -68,6 +69,48 @@ export const renderTextWithMarks = (text: string, marks: any[]): ReactNode => {
return result;
};

/**
* Renders text, turning legacy `$...$` segments into KaTeX (comments saved
* before the Mathematics extension). Code marks are left as literal source.
*/
export const renderTextWithInlineMath = (text: string, marks: any[] = []): ReactNode => {
if (!text) return null;

if (marks.some((mark) => mark.type === 'code')) {
return renderTextWithMarks(text, marks);
}

const nodes: ReactNode[] = [];
let lastIndex = 0;
let key = 0;
const regex = new RegExp(INLINE_MATH_REGEX.source, INLINE_MATH_REGEX.flags);
let match: RegExpExecArray | null;

while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push(
<Fragment key={key++}>
{renderTextWithMarks(text.slice(lastIndex, match.index), marks)}
</Fragment>
);
}
nodes.push(<KatexMath key={key++} latex={match[1]} />);
lastIndex = match.index + match[0].length;
}

if (nodes.length === 0) {
return renderTextWithMarks(text, marks);
}

if (lastIndex < text.length) {
nodes.push(
<Fragment key={key++}>{renderTextWithMarks(text.slice(lastIndex), marks)}</Fragment>
);
}

return <>{nodes}</>;
};

function demoteUrlToLinkedText(url: string) {
return {
type: 'text',
Expand Down Expand Up @@ -119,6 +162,10 @@ export const extractPlainText = (node: any): string => {
return name;
}

if (node.type === 'inlineMath' || node.type === 'blockMath') {
return node.attrs?.latex || '';
}

// If it has content, recursively extract text from all children
if (node.content && Array.isArray(node.content)) {
return node.content.map(extractPlainText).join('');
Expand Down Expand Up @@ -413,17 +460,46 @@ const RenderNode: React.FC<RenderNodeProps> = ({
if (debug) console.log('TRUNCATING TEXT to', truncatedText + '...');
return (
<>
{renderTextWithMarks(truncatedText, node.marks || [])}
{renderTextWithInlineMath(truncatedText, node.marks || [])}
<span className="text-gray-500 font-semibold">...</span>
</>
);
}

return renderTextWithMarks(text, node.marks || []);
return renderTextWithInlineMath(text, node.marks || []);
}

// TipTap Mathematics nodes (saved after the extension was enabled)
if (node.type === 'inlineMath') {
const latex = node.attrs?.latex || '';
if (!latex) return null;
return <KatexMath latex={latex} />;
}

if (node.type === 'blockMath') {
const latex = node.attrs?.latex || '';
if (!latex) return null;
return <KatexMath latex={latex} display />;
}

// Handle paragraph nodes
if (node.type === 'paragraph') {
// A paragraph whose only content is `$$...$$` is treated as display math
// for comments authored before blockMath nodes existed.
if (
Array.isArray(node.content) &&
node.content.length === 1 &&
node.content[0]?.type === 'text' &&
typeof node.content[0].text === 'string' &&
!node.content[0].marks?.some((mark: { type?: string }) => mark.type === 'code')
) {
const trimmed = node.content[0].text.trim();
const blockMatch = trimmed.match(BLOCK_MATH_REGEX);
if (blockMatch) {
return <KatexMath latex={blockMatch[1].trim()} display />;
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const { children } = renderChildrenWithTruncation(node.content, {
debug,
renderSectionHeader,
Expand Down
2 changes: 2 additions & 0 deletions components/Comment/lib/commentContentUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ export const extractTextFromTipTap = (tipTapContent: any): string => {
const processNode = (node: any): void => {
if (node.text) {
text += node.text;
} else if (node.type === 'inlineMath' || node.type === 'blockMath') {
text += node.attrs?.latex || '';
} else if (node.content && Array.isArray(node.content)) {
node.content.forEach(processNode);
}
Expand Down
11 changes: 11 additions & 0 deletions components/Comment/lib/hooks/useCommentEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEditor, Content, JSONContent } from '@tiptap/react';
import { StarterKit } from '@tiptap/starter-kit';
import { Underline } from '@tiptap/extension-underline';
import { Mathematics, migrateMathStrings } from '@tiptap/extension-mathematics';
import { CommentLink } from '../extensions/CommentLink';
import { Image } from '@tiptap/extension-image';
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
Expand Down Expand Up @@ -147,6 +148,9 @@ export const useCommentEditor = ({
},
}),
MentionExtension,
Mathematics.configure({
katexOptions: { throwOnError: false },
}),
Placeholder.configure({
placeholder,
emptyEditorClass: 'is-editor-empty',
Expand All @@ -167,6 +171,11 @@ export const useCommentEditor = ({
class: 'prose prose-sm max-w-none focus:outline-none min-h-[100px] px-4 py-2',
},
},
onCreate: ({ editor }) => {
// Convert legacy `$...$` text (and drafts saved before Mathematics)
// into inlineMath nodes so KaTeX can render them.
migrateMathStrings(editor);
},
onUpdate: ({ editor }) => {
if (!isReadOnly) {
const json = editor.getJSON();
Expand Down Expand Up @@ -214,6 +223,7 @@ export const useCommentEditor = ({
// upgraded into richLink nodes when the draft is restored.
if (debug) console.log('Loading content from localStorage:', loadedContent);
editor.commands.setContent(normalizeRichLinks(loadedContent), { emitUpdate: false });
migrateMathStrings(editor);
} else if (initialContent) {
// If format is HTML, set the content directly. TipTap's HTML parser
// will route any `a[data-type="rich-link"]` tags through the
Expand All @@ -234,6 +244,7 @@ export const useCommentEditor = ({

editor.commands.setContent(normalized, { emitUpdate: false });
}
migrateMathStrings(editor);
}
}
}, [editor, initialContent, loadedContent, isReadOnly, debug, format]);
Expand Down
7 changes: 4 additions & 3 deletions components/Editor/extensions/SlashCommand/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,10 @@
editor
.chain()
.focus()
.insertContent(
'$\\text{Total Supply} = \\sum_{i=0}^{32} \\left( 210{,}000 \\times \\frac{50}{2^i} \\right)$'
)
.insertInlineMath({
latex:
'\\text{Total Supply} = \\sum_{i=0}^{32} \\left( 210{,}000 \\times \\frac{50}{2^i} \\right)',

Check warning on line 216 in components/Editor/extensions/SlashCommand/groups.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=ResearchHub_web&issues=AaF05mH7Gh8uurdk4m5K&open=AaF05mH7Gh8uurdk4m5K&pullRequest=1249
})
.run();
},
},
Expand Down
4 changes: 3 additions & 1 deletion components/Editor/extensions/extension-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ export const ExtensionKit = ({ customDocument, placeholderConfig }: ExtensionKit
}),
Subscript,
Superscript,
Mathematics,
Mathematics.configure({
katexOptions: { throwOnError: false },
}),
Table,
TableCell,
TableHeader,
Expand Down
Loading