import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Layout from './components/Layout'; import Toolbar from './components/Toolbar'; import EditorPanel from './components/EditorPanel'; import ActionDialog, { type ActionDialogAction, } from './components/ActionDialog'; import HelpDialog from './components/HelpDialog'; import SnippetToolbox from './components/SnippetToolbox'; import { useWorkbenchState } from './workspace/useWorkbenchState'; import type { WorkbenchDocumentKind } from './workspace/workspaceTypes'; import type { CodeMirrorEditorHandle } from './editor/editorTypes'; import { openTextFile, saveTextFile } from './file/fileService'; import { validateXml, formatXml } from './validation/xmlValidation'; import { validateXslt } from './validation/xsltValidation'; import { hasErrors, type DiagnosticMessage, } from './validation/validationTypes'; import { runTransformation } from './transform/transformService'; import { createTransformationRun } from './transform/nativeXsltEngine'; import type { TransformEngineId } from './transform/transformTypes'; import { createApproximateTrace } from './transform/traceAnalyzer'; interface DialogState { title: string; content: React.ReactNode; actions: ActionDialogAction[]; onCancel?: () => void; } const App: React.FC = () => { const { state, setState, updateDocumentText, replaceDocument, markDocumentSaved, setLastTransformation, resetWorkspace, } = useWorkbenchState(); const xmlInputEditorRef = useRef(null); const xsltEditorRef = useRef(null); const outputEditorRef = useRef(null); const [dialog, setDialog] = useState(null); const [helpOpen, setHelpOpen] = useState(false); const [busy, setBusy] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [runtimeDiagnostics, setRuntimeDiagnostics] = useState< DiagnosticMessage[] >([]); const [validationTouched, setValidationTouched] = useState(false); const xmlDiagnostics = useMemo( () => validateXml(state.xmlInput.text, 'XML input'), [state.xmlInput.text] ); const xsltDiagnostics = useMemo( () => validateXslt(state.xsltCode.text, state.selectedEngine), [state.xsltCode.text, state.selectedEngine] ); const outputDiagnostics = useMemo(() => { if (!state.xmlOutput.text.trim()) return []; return validateXml(state.xmlOutput.text, 'XML output'); }, [state.xmlOutput.text]); const closeDialog = useCallback(() => { dialog?.onCancel?.(); setDialog(null); }, [dialog]); const askForConfirmation = useCallback( (options: { title: string; content: React.ReactNode; proceedLabel?: string; danger?: boolean; }): Promise => { return new Promise((resolve) => { const finish = (confirmed: boolean) => { setDialog(null); resolve(confirmed); }; setDialog({ title: options.title, content: options.content, onCancel: () => resolve(false), actions: [ { label: 'Cancel', variant: 'secondary', autoFocus: true, onClick: () => finish(false), }, { label: options.proceedLabel ?? 'Proceed', variant: options.danger ? 'danger' : 'primary', onClick: () => finish(true), }, ], }); }); }, [] ); const confirmOverwrite = useCallback( async (kind: WorkbenchDocumentKind, reason: string): Promise => { const documentState = state[kind]; if (!documentState.text.trim() && !documentState.dirty) return true; return askForConfirmation({ title: `Overwrite ${documentState.label}?`, content: (

{reason} This will replace the current contents of{' '} {documentState.label}.

), proceedLabel: 'Overwrite', danger: true, }); }, [askForConfirmation, state] ); const handleOpenHelp = useCallback(() => { setHelpOpen(true); }, []); const handleCloseHelp = useCallback(() => { setHelpOpen(false); }, []); const handleOpenFile = async (kind: WorkbenchDocumentKind) => { try { const opened = await openTextFile(kind); if (!opened) return; const documentState = state[kind]; if (documentState.dirty) { const mayProceed = await askForConfirmation({ title: `Overwrite ${documentState.label}?`, content: (

Opening {opened.name} will replace the current unsaved contents of {documentState.label}.

), proceedLabel: 'Overwrite', danger: true, }); if (!mayProceed) return; } replaceDocument(kind, opened.text, opened.name, false); setStatusMessage(`Loaded ${opened.name}.`); } catch (error) { if (isAbortError(error)) return; setStatusMessage( error instanceof Error ? error.message : 'Failed to open file.' ); } }; const handleSaveFile = async (kind: WorkbenchDocumentKind) => { try { const documentState = state[kind]; const suggestedName = documentState.fileName ?? defaultFileName(kind); const savedName = await saveTextFile({ suggestedName, text: documentState.text, kind, }); if (savedName) { markDocumentSaved(kind, savedName); setStatusMessage(`Saved ${savedName}.`); } } catch (error) { if (isAbortError(error)) return; setStatusMessage( error instanceof Error ? error.message : 'Failed to save file.' ); } }; const handleApplyTransformation = async () => { setValidationTouched(true); setRuntimeDiagnostics([]); const diagnostics = [...xmlDiagnostics, ...xsltDiagnostics]; if (hasErrors(diagnostics)) { setRuntimeDiagnostics(diagnostics); setStatusMessage('Please fix validation errors before transforming.'); return; } if (state.options.askBeforeOverwritingOutput) { const mayOverwriteOutput = await confirmOverwrite( 'xmlOutput', 'Applying the transformation writes a new result.' ); if (!mayOverwriteOutput) return; } setBusy(true); try { const request = { xmlText: state.xmlInput.text, xsltText: state.xsltCode.text, engine: state.selectedEngine, }; const result = await runTransformation(request); setRuntimeDiagnostics(result.diagnostics); if (hasErrors(result.diagnostics)) { setStatusMessage( 'Transformation failed because the input or stylesheet is invalid.' ); return; } const output = state.options.prettifyOutputAfterTransform ? tryFormatXml(result.output) : result.output; replaceDocument('xmlOutput', output, state.xmlOutput.fileName, true); const run = await createTransformationRun(request, output); setLastTransformation(run); setStatusMessage(`Transformation completed with ${result.engine}.`); } catch (error) { setRuntimeDiagnostics([ { severity: 'error', source: 'Transformation engine', message: error instanceof Error ? error.message : 'Transformation failed.', }, ]); setStatusMessage('Transformation failed.'); } finally { setBusy(false); } }; const handleMoveOutputToInput = async () => { if (!state.xmlOutput.text.trim()) { setStatusMessage('The output editor is empty. Nothing to move.'); return; } const mayProceed = await confirmOverwrite( 'xmlInput', 'Moving output to input is useful for chained transformations.' ); if (!mayProceed) return; replaceDocument('xmlInput', state.xmlOutput.text, 'from-output.xml', true); setStatusMessage('Moved output to XML input.'); }; const handleFormat = async (kind: WorkbenchDocumentKind) => { const documentState = state[kind]; try { const formatted = formatXml(documentState.text); if (formatted === documentState.text) { setStatusMessage(`${documentState.label} is already formatted.`); return; } const mayProceed = await askForConfirmation({ title: `Format ${documentState.label}?`, content: (

Formatting rewrites whitespace in{' '} {documentState.label}.

), proceedLabel: 'Format', }); if (!mayProceed) return; replaceDocument(kind, formatted, documentState.fileName, true); setStatusMessage(`Formatted ${documentState.label}.`); } catch (error) { setStatusMessage( error instanceof Error ? error.message : 'Could not format XML.' ); } }; const handleValidate = () => { setValidationTouched(true); const allDiagnostics = [ ...xmlDiagnostics, ...xsltDiagnostics, ...outputDiagnostics, ]; setRuntimeDiagnostics(allDiagnostics); if (allDiagnostics.length === 0) { setStatusMessage('All current documents are well-formed.'); return; } setStatusMessage( hasErrors(allDiagnostics) ? 'Validation found errors.' : 'Validation completed with warnings.' ); }; const handleReset = async () => { const mayProceed = await askForConfirmation({ title: 'Reset workspace?', content: (

This restores the example XML and XSLT, clears the output, and removes the saved LocalStorage workspace.

), proceedLabel: 'Reset workspace', danger: true, }); if (!mayProceed) return; resetWorkspace(); setRuntimeDiagnostics([]); setValidationTouched(false); setStatusMessage('Workspace reset.'); }; const handleEngineChange = (engine: TransformEngineId) => { setState((current) => ({ ...current, selectedEngine: engine })); }; const handlePrettifyOutputAfterTransformChange = (enabled: boolean) => { setState((current) => ({ ...current, options: { ...current.options, prettifyOutputAfterTransform: enabled, }, })); }; const handleAskBeforeOverwritingOutputChange = (enabled: boolean) => { setState((current) => ({ ...current, options: { ...current.options, askBeforeOverwritingOutput: enabled, }, })); }; const traceItems = useMemo(() => { if (!state.xmlOutput.text.trim()) return []; return createApproximateTrace( state.xmlInput.text, state.xsltCode.text, state.xmlOutput.text ); }, [state.xmlInput.text, state.xsltCode.text, state.xmlOutput.text]); const displayedRuntimeDiagnostics = validationTouched ? runtimeDiagnostics : []; useEffect(() => { const handleGlobalKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented || event.repeat || event.isComposing) { return; } if (event.key === 'F1') { event.preventDefault(); event.stopPropagation(); handleOpenHelp(); return; } const isApplyShortcut = event.key === 'Enter' && event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey; if (isApplyShortcut) { event.preventDefault(); event.stopPropagation(); if (!busy) { void handleApplyTransformation(); } } }; window.addEventListener('keydown', handleGlobalKeyDown, true); return () => { window.removeEventListener('keydown', handleGlobalKeyDown, true); }; }, [busy, handleApplyTransformation, handleOpenHelp]); return ( handleOpenHelp()}>
updateDocumentText('xmlInput', value)} onOpen={() => void handleOpenFile('xmlInput')} onSave={() => void handleSaveFile('xmlInput')} onUndo={() => xmlInputEditorRef.current?.undo()} onRedo={() => xmlInputEditorRef.current?.redo()} onCopy={() => void xmlInputEditorRef.current?.copySelection()} onCut={() => void xmlInputEditorRef.current?.cutSelection()} onPaste={() => void xmlInputEditorRef.current?.pasteText()} onPrettify={() => void handleFormat('xmlInput')} placeholderText="Paste or open XML here." /> updateDocumentText('xsltCode', value)} onOpen={() => void handleOpenFile('xsltCode')} onSave={() => void handleSaveFile('xsltCode')} onUndo={() => xsltEditorRef.current?.undo()} onRedo={() => xsltEditorRef.current?.redo()} onCopy={() => void xsltEditorRef.current?.copySelection()} onCut={() => void xsltEditorRef.current?.cutSelection()} onPaste={() => void xsltEditorRef.current?.pasteText()} onPrettify={() => void handleFormat('xsltCode')} placeholderText="Write or open XSLT here." actions={ xsltEditorRef.current?.insertText(snippet) } /> } /> updateDocumentText('xmlOutput', value)} onOpen={null} onSave={() => void handleSaveFile('xmlOutput')} onUndo={() => outputEditorRef.current?.undo()} onRedo={() => outputEditorRef.current?.redo()} onCopy={() => void outputEditorRef.current?.copySelection()} onCut={() => void outputEditorRef.current?.cutSelection()} onPaste={() => void outputEditorRef.current?.pasteText()} onPrettify={() => void handleFormat('xmlOutput')} placeholderText="Transformation output appears here." />
void handleApplyTransformation()} onMoveOutputToInput={() => void handleMoveOutputToInput()} onValidate={handleValidate} onReset={() => void handleReset()} prettifyOutputAfterTransform={state.options.prettifyOutputAfterTransform} onPrettifyOutputAfterTransformChange={ handlePrettifyOutputAfterTransformChange } askBeforeOverwritingOutput={state.options.askBeforeOverwritingOutput} onAskBeforeOverwritingOutputChange={ handleAskBeforeOverwritingOutputChange } busy={busy} /> {statusMessage &&
{statusMessage}
} {displayedRuntimeDiagnostics.length > 0 && (

Validation / runtime messages

    {displayedRuntimeDiagnostics.map((diagnostic, index) => (
  • {diagnostic.source} {diagnostic.message}
  • ))}
)}

Explain transformation

MVP approximation: result nodes are compared with simple stylesheet template matches. Full execution tracing requires an instrumented engine later.

{state.lastTransformation && ( Last run:{' '} {new Date( state.lastTransformation.transformedAt ).toLocaleString()} )}
{traceItems.length === 0 ? (

Run a transformation to see approximate result explanations.

) : (
{traceItems.map((item) => ( ))}
Result Likely source Likely template Confidence
{item.resultPath} {item.likelySourcePath ?? '—'} {item.likelyTemplate ?? '—'} {item.confidence}
)}
{dialog?.content} {helpOpen && ( )}
); }; function tryFormatXml(text: string): string { try { return text.trim() ? formatXml(text) : text; } catch { return text; } } function defaultFileName(kind: WorkbenchDocumentKind): string { switch (kind) { case 'xmlInput': return 'input.xml'; case 'xsltCode': return 'transform.xsl'; case 'xmlOutput': return 'output.xml'; } } function isAbortError(error: unknown): boolean { return error instanceof DOMException && error.name === 'AbortError'; } export default App;