// Real LaTeX editor for paper-mode Documents. // Left: CodeMirror 6 editor with LaTeX (stex) syntax highlighting. // Right: live LaTeX.js HTML preview (handles \section, \textbf, \emph, lists, // citations, math via KaTeX — most thesis-shaped documents work). // Plus a "Compile PDF" action that POSTs to LaTeX-Online (latex.ytotech.com) // for real pdflatex output in an iframe. import React, { useState, useEffect, useRef, useMemo } from 'react'; import CodeMirror from '@uiw/react-codemirror'; import { StreamLanguage } from '@codemirror/language'; import { stex } from '@codemirror/legacy-modes/mode/stex'; import Icon from './CanvasIcon'; const fireToast = (msg, kind = 'success') => window.dispatchEvent(new CustomEvent('canvas-toast', { detail: { msg, kind } })); // Wrap a section body as a full standalone .tex document so both LaTeX.js (HTML) // and LaTeX-Online (PDF) can render it. The user's section content is the body. const wrapLatexDoc = (body, title = 'Document') => `\\documentclass[11pt]{article} \\usepackage[utf8]{inputenc} \\usepackage{amsmath,amssymb} \\usepackage{graphicx} \\usepackage{hyperref} \\title{${title}} \\begin{document} ${body} \\end{document}`; // Render LaTeX source to HTML using latex.js, returned as a sandboxed iframe // (latex.js renders to a shadow DOM-style document via createGenerator()). function LatexPreview({ source, title }) { const containerRef = useRef(null); const [error, setError] = useState(null); useEffect(() => { if (!containerRef.current) return; let cancelled = false; setError(null); (async () => { try { // Dynamic import — latex.js is heavy, we only pay the cost when used. const latexJs = await import('latex.js'); if (cancelled) return; const { HtmlGenerator, parse } = latexJs; const generator = new HtmlGenerator({ hyphenate: false }); const doc = parse(wrapLatexDoc(source || '', title), { generator }); if (cancelled || !containerRef.current) return; containerRef.current.innerHTML = ''; const fragment = doc.htmlDocument().body; containerRef.current.appendChild(fragment); } catch (e) { if (!cancelled) setError(e.message || String(e)); } })(); return () => { cancelled = true; }; }, [source, title]); return (
{error && (
LaTeX parse error
{error}
)}
); } // PDF compile via LaTeX-Online API. POST the wrapped doc, get a PDF blob back. function PdfPanel({ source, title, onClose }) { const [busy, setBusy] = useState(true); const [pdfUrl, setPdfUrl] = useState(null); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; let blobUrl = null; (async () => { setBusy(true); setError(null); try { // YtoTech LaTeX-Online accepts JSON with the .tex source. // CORS-enabled. Returns PDF binary on success. const res = await fetch('https://latex.ytotech.com/builds/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ compiler: 'pdflatex', resources: [{ main: true, content: wrapLatexDoc(source, title) }], }), }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(text || `HTTP ${res.status}`); } const blob = await res.blob(); if (cancelled) return; blobUrl = URL.createObjectURL(blob); setPdfUrl(blobUrl); } catch (e) { if (!cancelled) setError(e.message || String(e)); } finally { if (!cancelled) setBusy(false); } })(); return () => { cancelled = true; if (blobUrl) URL.revokeObjectURL(blobUrl); }; }, [source, title]); return (
{ if (e.target === e.currentTarget) onClose(); }}>
e.stopPropagation()}>
PDF preview · pdflatex
Compiled via{' '} latex-on-http {' '}— a free open-source service. CORS-enabled.
{pdfUrl && ( Save PDF )}
{busy && (
Running pdflatex…
)} {error && (
Compile failed
{error}
The free latex-online service can be flaky and doesn't support every package. The HTML preview to the left should still work for most thesis-shaped documents.
)} {pdfUrl && !busy && !error && (