Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
Svelte
at mathquill
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305<script lang="ts" generics="T extends Expression2d | Expression3d"> import { onDestroy, onMount } from 'svelte'; import { acceptVisualDraft, rejectVisualDraft } from '#lib/document/expressions.js'; import { convertExpressionSource } from '#lib/engine/cartesium.js'; import type { Expression2d, Expression3d } from '#lib/engine/types.js'; import MathInput from './MathInput.svelte';
let { expression, label, displaySource, editorGeneration, invalid = false, diagnostic = '', onupdate, onfocus }: { expression: T; label: string; displaySource?: string; editorGeneration: number; invalid?: boolean; diagnostic?: string; onupdate: (expression: T) => void; onfocus?: () => void; } = $props();
let visualValue = $state(''); let status = $state(''); let displayRevision = 0; let editRevision = 0; let alive = true; let unrenderableSource = $state<string | undefined>(); let currentDiagnostic = $derived(expression.visual_draft?.diagnostic || diagnostic || ''); let displayedText = $derived(displaySource ?? expression.source);
let sourcePreview = $derived( !expression.visual_draft && unrenderableSource === displayedText );
onDestroy(() => { alive = false; editRevision += 1; displayRevision += 1; });
function updateVisualSource(latex: string) { const revision = ++editRevision; // Keep MathQuill's controlled value aligned with the exact keystroke // before the parent publishes the draft. This avoids a stale async // source-to-LaTeX refresh resetting MathQuill's insertion point. displayRevision += 1; visualValue = latex; unrenderableSource = undefined; status = '';
if (!latex.trim()) { // An empty MathQuill field is a completed empty row, not a draft that // could permanently block global TXT editing. onupdate({ ...expression, input_mode: 'text', source: '', visual_source: '', visual_source_for: '', visual_draft: undefined });
return; }
const generation = editorGeneration; const expressionId = expression.id; const lastGoodSource = expression.visual_draft?.last_good_source ?? expression.source; status = 'Checking visual expression…'; // Clear the evaluated source immediately. The in-flight parse must never // leave the previous graph visible as if it represented this draft. onupdate({ ...expression, input_mode: 'text', source: '', visual_source: undefined, visual_source_for: undefined, visual_draft: { latex, last_good_source: lastGoodSource, diagnostic: 'Checking visual expression…' } }); void convertExpressionSource(latex, 'latex', 'text') .then((ascii) => { if ( !alive || editorGeneration !== generation || revision !== editRevision || expression.id !== expressionId ) return; const updated = acceptVisualDraft(expression, latex, ascii);
if (!updated) return; status = ''; onupdate(updated); }) .catch((error: unknown) => { if ( !alive || editorGeneration !== generation || revision !== editRevision || expression.id !== expressionId ) return; const message = conversionMessage(error, 'This visual expression is incomplete or unsupported.'); const updated = rejectVisualDraft(expression, latex, message);
if (!updated) return; status = message; onupdate(updated); }); }
function clearVisualDraft() { editRevision += 1; displayRevision += 1; visualValue = ''; status = ''; unrenderableSource = undefined; onupdate({ ...expression, input_mode: 'text', source: '', visual_source: undefined, visual_source_for: undefined, visual_draft: undefined }); }
async function refreshVisualValue(source: string, current: T) { const revision = ++displayRevision; const generation = editorGeneration;
if (current.visual_draft && displaySource === undefined) { visualValue = current.visual_draft.latex; unrenderableSource = undefined;
return; }
if (current.input_mode === 'latex' && displaySource === undefined) { visualValue = source; unrenderableSource = undefined; status = '';
return; }
if (current.visual_source_for === source && current.visual_source !== undefined) { visualValue = current.visual_source; unrenderableSource = undefined; status = '';
return; }
if (!source.trim()) { visualValue = ''; unrenderableSource = undefined; status = '';
return; }
if (unrenderableSource === source) return; const expressionId = current.id;
try { const latex = await convertExpressionSource(source, 'text', 'latex');
if ( !alive || editorGeneration !== generation || revision !== displayRevision || expression.id !== expressionId || displayedText !== source ) return; visualValue = latex; unrenderableSource = undefined; status = ''; } catch (error) { if ( !alive || editorGeneration !== generation || revision !== displayRevision || expression.id !== expressionId || displayedText !== source ) return; visualValue = ''; status = conversionMessage(error, 'This expression cannot be displayed in the visual editor.'); unrenderableSource = source; } }
$effect(() => { void refreshVisualValue(displayedText, expression); });
function conversionMessage(error: unknown, fallback: string): string { if (error instanceof Error) return error.message;
if (typeof error === 'string' && error.trim()) return error;
return fallback; }
let restorePendingDraft = $state(false);
onMount(() => { restorePendingDraft = expression.visual_draft?.diagnostic === 'Checking visual expression…'; });
$effect(() => { const current = expression; const draft = current.visual_draft; const generation = editorGeneration;
if (!restorePendingDraft || !draft || draft.diagnostic !== 'Checking visual expression…') return; restorePendingDraft = false; const id = current.id; const latex = draft.latex; const lastGoodSource = draft.last_good_source; void convertExpressionSource(latex, 'latex', 'text') .then((source) => { if ( !alive || editorGeneration !== generation || expression.id !== id || expression.visual_draft?.latex !== latex || expression.visual_draft?.last_good_source !== lastGoodSource || expression.visual_draft?.diagnostic !== 'Checking visual expression…' ) return; onupdate({ ...expression, input_mode: 'text', source, visual_source: latex, visual_source_for: source, visual_draft: undefined }); }) .catch((error: unknown) => { if ( !alive || editorGeneration !== generation || expression.id !== id || expression.visual_draft?.latex !== latex || expression.visual_draft?.last_good_source !== lastGoodSource || expression.visual_draft?.diagnostic !== 'Checking visual expression…' ) return; const message = conversionMessage(error, 'This visual expression is incomplete or unsupported.'); onupdate({ ...expression, input_mode: 'text', source: '', visual_draft: { latex, last_good_source: lastGoodSource, diagnostic: message } }); }); });</script>
<div class="expression-editor"> {#if sourcePreview} <textarea class="source-preview" aria-readonly="true" aria-label={`${label}, original Cartesium source`} value={displayedText} readonly wrap="soft" onfocus={onfocus} ></textarea> <p class="status" role="alert"> This source cannot be displayed visually: {status} The original text is preserved. Open TXT to edit it. </p> {:else} <MathInput value={visualValue} label={`${label}, visual math`} invalid={invalid || Boolean(currentDiagnostic)} oninput={(latex) => updateVisualSource(latex)} {onfocus} /> {/if} {#if expression.visual_draft} <div class="draft-message" role="status"> <p>{expression.visual_draft.diagnostic} This draft is saved and has no active ASCII expression, so it is not evaluated. Finish or clear this draft before opening TXT.</p> <button type="button" onclick={clearVisualDraft} aria-label={`Clear unfinished visual draft for ${label}`}> Clear draft </button> </div> {/if} {#if currentDiagnostic && !expression.visual_draft} <p class="diagnostic" role="status">{currentDiagnostic}</p> {/if}</div>
<style> .expression-editor { display: flex; flex-direction: column; min-width: 0; gap: 5px; } .source-preview { box-sizing: border-box; width: 100%; min-height: 34px; max-height: 180px; overflow: auto; margin: 0; padding: 7px; border: 1px solid #cbd5e1; border-radius: 5px; background: #f8fafc; color: #334155; font: 13px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; resize: none; } .source-preview:focus { outline: 2px solid #93c5fd; outline-offset: 1px; border-color: #3b82f6; } .status, .diagnostic, .draft-message { margin: 0; color: #9f1239; font-size: 11px; line-height: 1.35; } .draft-message { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; color: #92400e; } .draft-message p { flex: 1 1 220px; margin: 0; } button { flex: none; border: 1px solid #cbd5e1; border-radius: 4px; background: white; color: #475569; padding: 3px 7px; font: 600 11px/1.25 system-ui, sans-serif; cursor: pointer; } button:hover { color: #1d4ed8; border-color: #60a5fa; }</style>