diff --git a/apps/landing/package.json b/apps/landing/package.json index 74454ba..9dd1d0b 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -18,6 +18,7 @@ "next": "15.3.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-latex-next": "^3.0.0", "react-syntax-highlighter": "15.6.1" }, "devDependencies": { @@ -25,7 +26,7 @@ "@types/node": "^24", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^5", - "@types/react-syntax-highlighter": "^15.5.13" + "@types/react-syntax-highlighter": "^15.5.13", + "typescript": "^5" } } \ No newline at end of file diff --git a/apps/landing/src/app/test-case/page.tsx b/apps/landing/src/app/test-case/page.tsx index a6f1d87..5003d04 100644 --- a/apps/landing/src/app/test-case/page.tsx +++ b/apps/landing/src/app/test-case/page.tsx @@ -1,6 +1,9 @@ +import 'katex/dist/katex.min.css' + import { Box, Grid, Text, VStack } from '@devup-ui/react' import { readFile } from 'fs/promises' import { Metadata } from 'next' +import Latex from 'react-latex-next' import TestCaseCircle from '@/components/test-case/TestCaseCircle' @@ -81,24 +84,34 @@ export default async function TestCasePage() { gridTemplateColumns="repeat(auto-fill, minmax(16px, 1fr))" > {testStatus[key][2].map( - ([text, expected, actual, isSuccess], idx) => ( - - - {text} -
- 정답 : {expected} -
- 결과 : {actual} -
- {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'} -
-
- ), + ([text, expected, actual, isSuccess], idx) => { + const textParts = parseTextWithLaTeX(text) + + return ( + + + {textParts.map((part, partIdx) => + part.type === 'latex' ? ( + ${part.content}$ + ) : ( + {part.content} + ), + )} +
+ 정답 : {expected} +
+ 결과 : {actual} +
+ {isSuccess ? '✅ 테스트 성공' : '❌ 테스트 실패'} +
+
+ ) + }, )} @@ -107,3 +120,50 @@ export default async function TestCasePage() { ) } + +/** + * This function parses text with LaTeX expressions and returns an array of parts. + * It assumes that LaTeX is wrapped in double dollar delimiters ($$...$$). + * Note that single dollar delimiters ($...$) are not rendered. + * @param input - The input text to parse. + * @returns An array of parts, where each part is either a text or a LaTeX expression. + */ +const parseTextWithLaTeX = (input: string) => { + const parts: Array<{ + type: 'text' | 'latex' + content: string + }> = [] + const latexRegex = /\$\$([^$]+(?:\$(?!\$)[^$]*)*)\$\$/g + let lastIndex = 0 + let match + + while ((match = latexRegex.exec(input)) !== null) { + // if there is text before the LaTeX expression, add it as a text part: + if (match.index > lastIndex) { + const textContent = input.slice(lastIndex, match.index) + if (textContent) { + parts.push({ type: 'text', content: textContent }) + } + } + + // add the LaTeX expression from double dollars: + const latexContent = match[1] + parts.push({ type: 'latex', content: latexContent }) + lastIndex = match.index + match[0].length + } + + // add remaining text after the last LaTeX expression: + if (lastIndex < input.length) { + const remainingText = input.slice(lastIndex) + if (remainingText) { + parts.push({ type: 'text', content: remainingText }) + } + } + + // if no LaTeX found, return the original text as a single text part: + if (!parts.length) { + parts.push({ type: 'text', content: input }) + } + + return parts +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 401c50a..5cd8aa3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-latex-next: + specifier: ^3.0.0 + version: 3.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-syntax-highlighter: specifier: 15.6.1 version: 15.6.1(react@19.1.0) @@ -1130,6 +1133,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1820,6 +1827,10 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2234,6 +2245,13 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-latex-next@3.0.0: + resolution: {integrity: sha512-x70f1b1G7TronVigsRgKHKYYVUNfZk/3bciFyYX1lYLQH2y3/TXku3+5Vap8MDbJhtopePSYBsYWS6jhzIdz+g==} + engines: {node: '>=12', npm: '>=5'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-syntax-highlighter@15.6.1: resolution: {integrity: sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==} peerDependencies: @@ -3794,6 +3812,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@8.3.0: {} + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -4686,6 +4706,10 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + katex@0.16.22: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5281,6 +5305,12 @@ snapshots: react-is@16.13.1: {} + react-latex-next@3.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + katex: 0.16.22 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-syntax-highlighter@15.6.1(react@19.1.0): dependencies: '@babel/runtime': 7.27.6