// 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(); }}>
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.