diff --git a/src/components/ProblemWorkspace.css b/src/components/ProblemWorkspace.css index d3d47c7..66593b7 100644 --- a/src/components/ProblemWorkspace.css +++ b/src/components/ProblemWorkspace.css @@ -355,3 +355,16 @@ .problem-workspace-stmt-text sub { white-space: nowrap; } + +.problem-workspace-math { + display: inline-block; + font-family: Georgia, 'Times New Roman', serif; + font-style: italic; + white-space: nowrap; +} + +.problem-workspace-math sup, +.problem-workspace-math sub { + font-size: 0.72em; + line-height: 0; +} diff --git a/src/components/ProblemWorkspace.jsx b/src/components/ProblemWorkspace.jsx index 4e296cc..61f7b1e 100644 --- a/src/components/ProblemWorkspace.jsx +++ b/src/components/ProblemWorkspace.jsx @@ -5,6 +5,7 @@ import { getDraftStorageKey, parseProblemWorkspaceQuery, } from '../utils/problemWorkspace'; +import { renderMathInHtml, textToHtml } from '../utils/renderMath'; import './ProblemWorkspace.css'; const DEFAULT_KOTLIN_STARTER = `fun main() { @@ -21,16 +22,14 @@ function loadDraft(storageKey) { } function StatementHtml({ html, fallback }) { - if (html) { - return ( -
- ); - } + const renderedHtml = renderMathInHtml(html || textToHtml(fallback)); - return

{fallback}

; + return ( +
+ ); } function ProblemWorkspace() { diff --git a/src/utils/renderMath.js b/src/utils/renderMath.js new file mode 100644 index 0000000..30678af --- /dev/null +++ b/src/utils/renderMath.js @@ -0,0 +1,49 @@ +export function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +function normalizeLatex(latex) { + return latex + .replace(/\\\\/g, '\\') + .replace(/\\(?:ldots|dots|cdots)/g, '…') + .replace(/\\leq?/g, '≤') + .replace(/\\geq?/g, '≥') + .replace(/\\ne(?:q)?/g, '≠') + .replace(/\\cdot/g, '·') + .replace(/\\times/g, '×') + .replace(/\\oplus/g, '⊕') + .replace(/\\infty/g, '∞') + .replace(/\\sum/g, '∑') + .replace(/\\min/g, 'min') + .replace(/\\max/g, 'max') + .replace(/\\left|\\right/g, '') + .replace(/\\,/g, ' ') + .replace(/\\ /g, ' '); +} + +export function renderMathExpression(latex) { + let rendered = escapeHtml(normalizeLatex(latex).trim()); + + rendered = rendered.replace(/\^\{([^{}]+)\}/g, '$1'); + rendered = rendered.replace(/_\{([^{}]+)\}/g, '$1'); + rendered = rendered.replace(/\^([A-Za-z0-9+-])/g, '$1'); + rendered = rendered.replace(/_([A-Za-z0-9+-])/g, '$1'); + + return `${rendered}`; +} + +export function renderMathInHtml(html) { + return html.replace(/\${1,3}([\s\S]*?)\${1,3}/g, (_, latex) => renderMathExpression(latex)); +} + +export function textToHtml(text) { + return escapeHtml(text) + .split(/\n{2,}/) + .map(paragraph => `

${paragraph.replaceAll('\n', '
')}

`) + .join(''); +} diff --git a/src/utils/renderMath.test.js b/src/utils/renderMath.test.js new file mode 100644 index 0000000..82a67a0 --- /dev/null +++ b/src/utils/renderMath.test.js @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { renderMathInHtml, textToHtml } from './renderMath.js'; + +describe('renderMathInHtml', () => { + it('renders Codeforces triple-dollar math delimiters', () => { + const html = renderMathInHtml('

There are $$$n+1$$$ vertices.

'); + + expect(html).toContain(' { + const html = renderMathInHtml('

Range $$$0,1,\\dots,n^2$$$.

'); + + expect(html).toContain('0,1,…,n2'); + }); + + it('escapes plain-text fallbacks before adding paragraph markup', () => { + expect(textToHtml('\n\nnext')).toBe( + '

<script>x</script>

next

', + ); + }); +});