diff --git a/packages/client/src/LinearView/LatexButton.tsx b/packages/client/src/LinearView/LatexButton.tsx new file mode 100644 index 0000000..c35f9c6 --- /dev/null +++ b/packages/client/src/LinearView/LatexButton.tsx @@ -0,0 +1,64 @@ +import React, { useState } from 'react'; +import { Button } from '@mui/material'; +import { Article as ArticleIcon } from '@mui/icons-material'; +import { NodeM } from '@forest/schema'; +import LatexRenderer from './LatexRenderer'; +import TemplateSelector from './TemplateSelector'; + +interface LatexButtonProps { + getHtml: () => string; + nodes: { node: NodeM; level: number; }[]; +} + +const LatexButton: React.FC = ({ getHtml, nodes }) => { + const [showTemplateSelector, setShowTemplateSelector] = useState(false); + const [showLatexRenderer, setShowLatexRenderer] = useState(false); + const [selectedTemplate, setSelectedTemplate] = useState(''); + + const handleClick = () => { + setShowTemplateSelector(true); + }; + + const handleTemplateSelected = (templateName: string) => { + setSelectedTemplate(templateName); + setShowTemplateSelector(false); + setShowLatexRenderer(true); + }; + + const handleClose = () => { + setShowTemplateSelector(false); + setShowLatexRenderer(false); + setSelectedTemplate(''); + }; + + return ( + <> + + + {showTemplateSelector && ( + + )} + + {showLatexRenderer && selectedTemplate && ( + + )} + + ); +}; + +export default LatexButton; \ No newline at end of file diff --git a/packages/client/src/LinearView/LatexRenderer.tsx b/packages/client/src/LinearView/LatexRenderer.tsx new file mode 100644 index 0000000..5985872 --- /dev/null +++ b/packages/client/src/LinearView/LatexRenderer.tsx @@ -0,0 +1,717 @@ +import React, { useState, useEffect } from 'react'; +import { Box, Paper, Typography, Button, CircularProgress, Alert, IconButton, Tabs, Tab, Card, CardContent, TextField, Select, MenuItem, FormControl, InputLabel, Chip } from '@mui/material'; +import { Close as CloseIcon, Add as AddIcon, Delete as DeleteIcon } from '@mui/icons-material'; +import { useTheme } from '@mui/system'; +import { httpUrl } from '@forest/schema/src/config'; +import { NodeM } from '@forest/schema'; +import { EditorNodeTypeM } from '@forest/node-type-editor/src'; + +interface LatexRendererProps { + content: string; + onClose: () => void; + nodes: { node: NodeM; level: number; }[]; + selectedTemplate: string; +} + +interface TemplateField { + name: string; + type: 'string' | 'node' | 'recursive node' | 'list string' | 'list recursive node' | 'list node'; +} + +interface Template { + name: string; + fields: TemplateField[]; +} + +const LatexRenderer: React.FC = ({ content, onClose, nodes, selectedTemplate }) => { + const [latexContent, setLatexContent] = useState(''); + const [templates, setTemplates] = useState([]); + const [fieldValues, setFieldValues] = useState>({}); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [step, setStep] = useState<'fields' | 'preview'>('fields'); // fields = input form, preview = rendered result + const theme = useTheme(); + + // Fetch available templates on component mount + useEffect(() => { + fetchTemplates(); + }, []); + + // Initialize field values when template changes + useEffect(() => { + if (selectedTemplate && templates.length > 0) { + const template = templates.find(t => t.name === selectedTemplate); + if (template) { + initializeFieldValues(template.fields); + } + } + }, [selectedTemplate, templates]); + + const fetchTemplates = async () => { + try { + const response = await fetch(`${httpUrl}/api/html2latex/templates`); + const data = await response.json(); + + if (data.success) { + setTemplates(data.templates); + } else { + setError('Failed to load templates'); + } + } catch (err) { + setError('Failed to connect to server'); + console.error('Template fetch error:', err); + } + }; + + const initializeFieldValues = (fields: TemplateField[]) => { + const initialValues: Record = {}; + fields.forEach(field => { + switch (field.type) { + case 'string': + initialValues[field.name] = ''; + break; + case 'list string': + initialValues[field.name] = ['']; // Start with one empty item + break; + case 'node': + case 'recursive node': + initialValues[field.name] = ''; + break; + case 'list node': + case 'list recursive node': + initialValues[field.name] = []; // Start with empty array + break; + default: + initialValues[field.name] = ''; + } + }); + setFieldValues(initialValues); + }; + + const generateLatex = async () => { + if (!selectedTemplate) return; + + setLoading(true); + setError(''); + + try { + // Convert field values to the format expected by server + const processedFields = processFieldValues(); + + // Get LaTeX from server + const latexResponse = await fetch(`${httpUrl}/api/html2latex/transform`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + template: selectedTemplate, + fields: processedFields, + }), + }); + + const latexData = await latexResponse.json(); + + if (latexData.success) { + setLatexContent(latexData.latex); + + // Simply display the LaTeX source + setStep('preview'); + } else { + setError(latexData.error || 'Failed to generate document'); + } + } catch (err) { + setError('Failed to generate document'); + console.error('Generation error:', err); + } finally { + setLoading(false); + } + }; + + const processFieldValues = () => { + const processed: Record = {}; + + Object.entries(fieldValues).forEach(([fieldName, value]) => { + if (value === null || value === undefined || value === '') return; + + const field = templates.find(t => t.name === selectedTemplate)?.fields.find(f => f.name === fieldName); + if (!field) return; + + switch (field.type) { + case 'node': + case 'recursive node': + if (typeof value === 'string' && value.trim() !== '') { + const node = nodes?.find(n => n.node.id === value); + if (node) { + const nodeData: any = { + node_id: node.node.id, + title: node.node.title(), + content: EditorNodeTypeM.getEditorContent(node.node), + }; + + if (field.type === 'recursive node') { + nodeData.children = getNodeChildren(node.node); + } + + processed[fieldName] = nodeData; + } + } + break; + case 'list node': + case 'list recursive node': + if (Array.isArray(value) && value.length > 0) { + processed[fieldName] = value + .filter((nodeId: string) => nodeId && nodeId.trim() !== '') + .map((nodeId: string) => { + const node = nodes?.find(n => n.node.id === nodeId); + if (node) { + const nodeData: any = { + node_id: node.node.id, + title: node.node.title(), + content: EditorNodeTypeM.getEditorContent(node.node), + }; + + if (field.type === 'list recursive node') { + nodeData.children = getNodeChildren(node.node); + } + + return nodeData; + } + return null; + }).filter(Boolean); + } + break; + case 'list string': + if (Array.isArray(value)) { + const filteredList = value.filter((item: string) => item && item.trim() !== ''); + if (filteredList.length > 0) { + processed[fieldName] = filteredList; + } + } + break; + default: + processed[fieldName] = value; + } + }); + + return processed; + }; + + const getNodeChildren = (node: NodeM): any[] => { + try { + const children = node.children().toJSON(); + const childrenArray = Array.isArray(children) ? children : []; + + return childrenArray.map((childNode: any) => { + const nodeData: any = { + node_id: childNode.id, + title: childNode.title(), + content: EditorNodeTypeM.getEditorContent(childNode), + children: getNodeChildren(childNode) // Recursive call for nested children + }; + + return nodeData; + }); + } catch (error) { + console.warn('Error getting node children:', error); + return []; + } + }; + + const updateFieldValue = (fieldName: string, value: any) => { + setFieldValues(prev => ({ + ...prev, + [fieldName]: value + })); + }; + + const addListItem = (fieldName: string) => { + const field = templates.find(t => t.name === selectedTemplate)?.fields.find(f => f.name === fieldName); + if (!field) return; + + setFieldValues(prev => { + const currentValue = prev[fieldName] || []; + const newItem = field.type === 'list string' ? '' : ''; + return { + ...prev, + [fieldName]: [...currentValue, newItem] + }; + }); + }; + + const removeListItem = (fieldName: string, index: number) => { + setFieldValues(prev => { + const currentValue = prev[fieldName] || []; + return { + ...prev, + [fieldName]: currentValue.filter((_: any, i: number) => i !== index) + }; + }); + }; + + const updateListItem = (fieldName: string, index: number, value: any) => { + setFieldValues(prev => { + const currentValue = prev[fieldName] || []; + const newValue = [...currentValue]; + newValue[index] = value; + return { + ...prev, + [fieldName]: newValue + }; + }); + }; + + const renderField = (field: TemplateField) => { + const value = fieldValues[field.name]; + + switch (field.type) { + case 'string': + return ( + updateFieldValue(field.name, e.target.value)} + variant="outlined" + size="small" + /> + ); + + case 'list string': + const listValue = Array.isArray(value) ? value : []; + return ( + + + {field.name} + + {listValue.map((item: string, index: number) => ( + + updateListItem(field.name, index, e.target.value)} + size="small" + placeholder={`Enter ${field.name.slice(0, -1)}`} + /> + removeListItem(field.name, index)} + size="small" + color="error" + > + + + + ))} + + + ); + + case 'node': + case 'recursive node': + return ( + + {field.name} + + {nodes && ( + + {nodes.length} nodes available + + )} + + ); + + case 'list node': + case 'list recursive node': + const listNodeValue = Array.isArray(value) ? value : []; + return ( + + + {field.name} + + + {listNodeValue.map((nodeId: string, index: number) => { + const nodeItem = nodes?.find(n => n.node.id === nodeId); + return nodeItem ? ( + removeListItem(field.name, index)} + size="small" + /> + ) : ( + removeListItem(field.name, index)} + size="small" + color="error" + /> + ); + })} + {listNodeValue.length === 0 && ( + + No nodes selected + + )} + + + Add {field.name.slice(0, -1) || 'node'} + + + {nodes && ( + + {nodes.length} total nodes, {listNodeValue.length} selected + + )} + + ); + + default: + return Unknown field type: {field.type}; + } + }; + + // No manual LaTeX parsing needed - using react-latex-next! + + const downloadLatex = () => { + const blob = new Blob([latexContent], { type: 'text/plain' }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'document.tex'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + // Removed downloadHtml since we're not generating HTML anymore + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(latexContent); + // Could add a success toast here + } catch (err) { + console.error('Failed to copy to clipboard:', err); + } + }; + + const selectedTemplateData = templates.find(t => t.name === selectedTemplate); + + return ( + + e.stopPropagation()} + > + {/* Header */} + + + {step === 'fields' ? 'Configure Template Fields' : 'Document Preview'} + + + {step === 'preview' && latexContent && ( + <> + + + + + )} + + + + + + + {/* Content */} + + {loading && ( + + + + )} + + {error && ( + + {error} + + )} + + {step === 'fields' && selectedTemplateData && !loading && ( + + + Fill in the template fields below: + + + {selectedTemplateData.fields.map((field) => ( + + + + {field.name} + + + Type: {field.type} + + {renderField(field)} + + + ))} + + + + + + )} + + {step === 'preview' && latexContent && !loading && ( + + + + 📄 LaTeX Document Generated + + + Copy this LaTeX code to Overleaf or any LaTeX editor + + + + + + + LaTeX Source Code + + + + + + + + + {latexContent} + + + + + + 💡 Next Steps: Copy the LaTeX code above and paste it into{' '} + + Overleaf + {' '} + or your preferred LaTeX editor to compile and view the PDF. + + + + )} + + + + ); +}; + +export default LatexRenderer; \ No newline at end of file diff --git a/packages/client/src/LinearView/index.tsx b/packages/client/src/LinearView/index.tsx index 8641a1f..789588a 100644 --- a/packages/client/src/LinearView/index.tsx +++ b/packages/client/src/LinearView/index.tsx @@ -8,6 +8,7 @@ import {currentPageAtom} from "../appState"; import {useTheme} from '@mui/system'; import ReferenceGenButton from './ReferenceGenButton'; import ReferenceIndexButton from './ReferenceIndexButton'; +import LatexButton from './LatexButton'; const linearNodeListAtom = atom((get) => { @@ -49,6 +50,7 @@ const ButtonsSection = ({getHtml, rootNode, nodes}: { + ); } diff --git a/packages/server/assets/template1.tex b/packages/server/assets/template1.tex new file mode 100644 index 0000000..5c6ddae --- /dev/null +++ b/packages/server/assets/template1.tex @@ -0,0 +1,112 @@ +%% + +\documentclass[sigconf,authordraft]{acmart} + +\AtBeginDocument{% + \providecommand\BibTeX{{% + Bib\TeX}}} + +\setcopyright{acmlicensed} +\copyrightyear{2018} +\acmYear{2018} +\acmDOI{XXXXXXX.XXXXXXX} +\acmConference[Conference acronym 'XX]{Make sure to enter the correct + conference title from your rights confirmation email}{June 03--05, + 2018}{Woodstock, NY} + +\acmISBN{978-1-4503-XXXX-X/2018/06} + +\begin{document} + +\title{Untitled} +\author{Author Name} +\affiliation{% + \institution{Institution} + \city{City} + \country{Country} +} + +\renewcommand{\shortauthors}{Trovato et al.} + +\begin{abstract} +This paper presents research findings. +\end{abstract} + +\begin{CCSXML} + + + 00000000.0000000.0000000 + Do Not Use This Code, Generate the Correct Terms for Your Paper + 500 + + + 00000000.00000000.00000000 + Do Not Use This Code, Generate the Correct Terms for Your Paper + 300 + + + 00000000.00000000.00000000 + Do Not Use This Code, Generate the Correct Terms for Your Paper + 100 + + + 00000000.00000000.00000000 + Do Not Use This Code, Generate the Correct Terms for Your Paper + 100 + + +\end{CCSXML} + +\ccsdesc[500]{Do Not Use This Code~Generate the Correct Terms for Your Paper} +\ccsdesc[300]{Do Not Use This Code~Generate the Correct Terms for Your Paper} +\ccsdesc{Do Not Use This Code~Generate the Correct Terms for Your Paper} +\ccsdesc[100]{Do Not Use This Code~Generate the Correct Terms for Your Paper} + +\keywords{research, paper} + + +\received{20 February 2007} +\received[revised]{12 March 2009} +\received[accepted]{5 June 2009} + +\maketitle + +\section{Introduction} +This paper presents our research work. + +%% +%% The acknowledgments section is defined using the "acks" environment +%% (and NOT an unnumbered section). This ensures the proper +%% identification of the section in the article metadata, and the +%% consistent spelling of the heading. +\begin{acks} +We thank our colleagues and reviewers for their valuable feedback. +\end{acks} + +%% +%% The next two lines define the bibliography style to be used, and +%% the bibliography file. +\bibliographystyle{ACM-Reference-Format} +\bibliography{sample-base} + + +%% +%% If your work has an appendix, this is the place to put it. +\appendix + +\section{Appendix Section} + +Additional research data and detailed analysis. + +\subsection{Data Tables} + +Supplementary tables and figures. + +\section{Additional Resources} + +Further reading and references related to this research. + +\end{document} +\endinput +%% +%% End of file `sample-sigconf-authordraft.tex'. \ No newline at end of file diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 016dff7..396c94e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -38,6 +38,7 @@ import mcpProxyRouter from "./routes/mcpProxyRouter"; import a2aProxyRouter from "./routes/a2aProxyRouter"; import {imageRoutes} from './routes/imageRoutes'; import {initializeMinioService} from './services/minioService'; +import { html2latexRoutes } from "./routes/html2latexRoutes"; // Initialize services and connections setMongoConnection(); @@ -78,6 +79,7 @@ function main(): void { app.use('/api/api-proxy', apiProxyRouter) app.use('/api/mcp-proxy', mcpProxyRouter); app.use('/api/a2a-proxy', a2aProxyRouter); + app.use('/api/html2latex', html2latexRoutes); app.use('/api/images', imageRoutes); app.use('/api/metadata', metadataRoutes); app.use('/api/datanode', createMongoCollectionRouter()); diff --git a/packages/server/src/routes/html2latexRoutes.ts b/packages/server/src/routes/html2latexRoutes.ts new file mode 100644 index 0000000..7af2883 --- /dev/null +++ b/packages/server/src/routes/html2latexRoutes.ts @@ -0,0 +1,387 @@ +import express, { Request, Response } from 'express'; +import fs from 'fs'; +import path from 'path'; + +const router = express.Router(); + +// Path to templates directory +const TEMPLATES_DIR = path.join(__dirname, '../../assets'); + +interface ContentTransformRequest { + template: string; + fields: any; +} + +interface TemplateFieldProcessor { + // Pattern to find the field in the template + findPattern: string | RegExp; + // How to replace it - can be a string template or a function + replacePattern?: string; + // Custom replacement function for complex logic + replaceFunction?: (templateContent: string, fieldValue: any, fieldName: string) => string; + // Default value if field is missing + defaultValue?: any; +} + +interface TemplateField { + name: string; + type: 'string' | 'node' | 'recursive node' | 'list string' | 'list recursive node' | 'list node'; + processor: TemplateFieldProcessor; +} + +interface TemplateInfo { + name: string; + fields: TemplateField[]; +} + +interface TemplateListResponse { + templates: TemplateInfo[]; +} + +interface ContentTransformResponse { + success: boolean; + latex?: string; + error?: string; +} + +interface NodeData { + node_id: string; + title: string; + content: string; + children?: NodeData[]; +} + +// Template definitions with processing patterns +const TEMPLATE_DEFINITIONS: Record = { + template1: { + name: 'template1', + fields: [ + { + name: 'title', + type: 'string', + processor: { + findPattern: /\\title\{[^}]*\}/, + replaceFunction: (template: string, value: string, fieldName: string) => { + const titleValue = value || 'Untitled'; + return template.replace(/\\title\{[^}]*\}/, `\\title{${titleValue}}`); + }, + defaultValue: 'Untitled' + } + }, + { + name: 'authors', + type: 'list string', + processor: { + findPattern: /\\title\{[^}]*\}([\s\S]*?)\\begin\{abstract\}/, + replaceFunction: (template: string, authors: string[], fieldName: string) => { + if (!Array.isArray(authors) || authors.length === 0) { + return template.replace( + /\\title\{[^}]*\}([\s\S]*?)\\begin\{abstract\}/, + (match) => { + const titleMatch = match.match(/\\title\{[^}]*\}/); + const title = titleMatch ? titleMatch[0] : '\\title{Untitled}'; + return `${title}\n\n\\begin{abstract}`; + } + ); + } + + const authorLatex = authors.map((author: string, index: number) => { + return `\\author{${author}} +\\affiliation{% + \\institution{Institution ${index + 1}} + \\city{City} + \\country{Country} +}`; + }).join('\n\n'); + + return template.replace( + /\\title\{[^}]*\}([\s\S]*?)\\begin\{abstract\}/, + (match) => { + const titleMatch = match.match(/\\title\{[^}]*\}/); + const title = titleMatch ? titleMatch[0] : '\\title{Untitled}'; + return `${title}\n\n${authorLatex}\n\n\\begin{abstract}`; + } + ); + }, + defaultValue: ['Author Name'] + } + }, + { + name: 'abstract', + type: 'node', + processor: { + findPattern: /\\begin\{abstract\}[\s\S]*?\\end\{abstract\}/, + replaceFunction: (template: string, nodeData: any, fieldName: string) => { + const content = nodeData?.content ? convertHtmlToLatex(nodeData.content) : 'This paper presents research findings.'; + return template.replace( + /\\begin\{abstract\}[\s\S]*?\\end\{abstract\}/, + `\\begin{abstract}\n${content}\n\\end{abstract}` + ); + }, + defaultValue: { node_id: 'default', title: 'Abstract', content: 'This paper presents research findings.' } + } + }, + { + name: 'sections', + type: 'list recursive node', + processor: { + findPattern: /(\\maketitle\s*)([\s\S]*?)(\\begin\{acks\}|\\bibliographystyle)/, + replaceFunction: (template: string, sections: any[], fieldName: string) => { + if (!Array.isArray(sections) || sections.length === 0) { + return template.replace( + /(\\maketitle\s*)([\s\S]*?)(\\begin\{acks\}|\\bibliographystyle)/, + `$1\n\n\\section{Introduction}\nThis paper presents our research work.\n\n$3` + ); + } + + const sectionsLatex = sections.map((section: NodeData) => + convertRecursiveNodeToLatex(section, 1) + ).join('\n\n'); + + return template.replace( + /(\\maketitle\s*)([\s\S]*?)(\\begin\{acks\}|\\bibliographystyle)/, + `$1\n\n${sectionsLatex}\n\n$3` + ); + }, + defaultValue: [{ node_id: 'default', title: 'Introduction', content: 'This paper presents our research work.', children: [] }] + } + }, + { + name: 'keywords', + type: 'list string', + processor: { + findPattern: /\\keywords\{[^}]*\}/, + replaceFunction: (template: string, keywords: string[], fieldName: string) => { + if (!Array.isArray(keywords) || keywords.length === 0) { + return template.replace(/\\keywords\{[^}]*\}/, '\\keywords{research, paper}'); + } + const keywordString = keywords.join(', '); + return template.replace(/\\keywords\{[^}]*\}/, `\\keywords{${keywordString}}`); + }, + defaultValue: ['research', 'paper'] + } + } + ] + } +}; + +/** + * POST /api/html2latex/transform + * Transform HTML content to LaTeX using specified template + */ +router.post('/transform', async (req: Request, res: Response): Promise => { + try { + const { template, fields } = req.body as ContentTransformRequest; + + if (!template || !fields) { + res.status(400).json({ + success: false, + error: 'Template and fields are required' + } as ContentTransformResponse); + return; + } + + // Check if template exists + const templatePath = path.join(TEMPLATES_DIR, `${template}.tex`); + + if (!fs.existsSync(templatePath)) { + res.status(400).json({ + success: false, + error: `Template '${template}' not found` + } as ContentTransformResponse); + return; + } + + let templateContent = fs.readFileSync(templatePath, 'utf-8'); + + // Process fields based on template + const transformedLatex = processTemplateFields(template, fields, templateContent); + + res.json({ + success: true, + latex: transformedLatex + } as ContentTransformResponse); + + } catch (error: any) { + console.error('HTML to LaTeX transform error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Internal server error' + } as ContentTransformResponse); + } +}); + +/** + * GET /api/html2latex/templates + * Get list of available LaTeX templates + */ +router.get('/templates', (req: Request, res: Response): void => { + try { + // Read templates directory + if (!fs.existsSync(TEMPLATES_DIR)) { + res.status(500).json({ + success: false, + error: 'Templates directory not found' + }); + return; + } + + const files = fs.readdirSync(TEMPLATES_DIR); + const availableTemplateFiles = files + .filter(file => file.endsWith('.tex')) + .map(file => file.replace('.tex', '')); + + // Get template definitions for available files + const templates = availableTemplateFiles + .filter(templateName => TEMPLATE_DEFINITIONS[templateName]) + .map(templateName => TEMPLATE_DEFINITIONS[templateName]); + + res.json({ + success: true, + templates + } as TemplateListResponse & { success: boolean }); + + } catch (error: any) { + console.error('Templates list error:', error); + res.status(500).json({ + success: false, + error: error.message || 'Internal server error' + }); + } +}); + +/** + * Process template fields and generate LaTeX - modular approach + */ +function processTemplateFields(templateName: string, fields: any, templateContent: string): string { + const templateDef = TEMPLATE_DEFINITIONS[templateName]; + if (!templateDef) { + throw new Error(`Unknown template: ${templateName}`); + } + + let result = templateContent; + + // Process each field based on its type + templateDef.fields.forEach(field => { + const fieldValue = fields[field.name]; + if (fieldValue !== undefined && fieldValue !== null) { + result = processTemplateField(result, field, fieldValue); + } else { + // Set default values for missing fields + result = setDefaultFieldValue(result, field); + } + }); + + return result; +} + +/** + * Process a single template field using its processor configuration + */ +function processTemplateField(templateContent: string, field: TemplateField, fieldValue: any): string { + const { processor } = field; + + // Use custom replace function if provided + if (processor.replaceFunction) { + return processor.replaceFunction(templateContent, fieldValue, field.name); + } + + console.warn(`No replacement method defined for field: ${field.name}`); + return templateContent; +} + +/** + * Set default values for missing fields using processor configuration + */ +function setDefaultFieldValue(templateContent: string, field: TemplateField): string { + const defaultValue = field.processor.defaultValue; + if (defaultValue !== undefined) { + return processTemplateField(templateContent, field, defaultValue); + } + return templateContent; +} + + +/** + * Legacy function for template1 - now calls the modular approach + */ +function processTemplate1FieldsForACMTemplate(fields: any, template: string): string { + // Use the new modular approach + return processTemplateFields('template1', fields, template); +} + +/** + * Convert a single node to LaTeX content + */ +function convertNodeToLatex(node: NodeData): string { + return convertHtmlToLatex(node.content); +} + +/** + * Convert a recursive node structure to LaTeX with proper sectioning + */ +function convertRecursiveNodeToLatex(node: NodeData, level: number): string { + let latex = ''; + + // Add section heading based on level + const sectionCommands = ['\\section', '\\subsection', '\\subsubsection', '\\paragraph', '\\subparagraph']; + const sectionCommand = sectionCommands[Math.min(level - 1, sectionCommands.length - 1)]; + + // Use the title from the node data, fallback if not provided + const title = node.title && node.title.trim() !== '' ? node.title : `Section ${level}`; + + // Convert content to LaTeX for body + const bodyContent = convertHtmlToLatex(node.content); + + latex += `${sectionCommand}{${title}}\n\n`; + if (bodyContent) { + latex += bodyContent + '\n\n'; + } + + // Process children recursively + if (node.children && node.children.length > 0) { + latex += '\n'; + latex += node.children.map(child => + convertRecursiveNodeToLatex(child, level + 1) + ).join('\n\n'); + } + + return latex; +} + +/** + * Convert HTML content to LaTeX format + */ +function convertHtmlToLatex(html: string): string { + if (!html) return ''; + + let latex = html; + + // Basic HTML to LaTeX conversions + latex = latex + .replace(/]*>(.*?)<\/h[1-6]>/gi, '$1') + .replace(/]*>(.*?)<\/p>/gi, '$1\n\n') + .replace(/]*>(.*?)<\/strong>/gi, '\\textbf{$1}') + .replace(/]*>(.*?)<\/b>/gi, '\\textbf{$1}') + .replace(/]*>(.*?)<\/em>/gi, '\\textit{$1}') + .replace(/]*>(.*?)<\/i>/gi, '\\textit{$1}') + .replace(/]*>(.*?)<\/u>/gi, '\\underline{$1}') + .replace(/]*>(.*?)<\/code>/gi, '\\texttt{$1}') + .replace(/]*>(.*?)<\/pre>/gi, '\\begin{verbatim}\n$1\n\\end{verbatim}') + .replace(/]*>/gi, '\\begin{itemize}') + .replace(/<\/ul>/gi, '\\end{itemize}') + .replace(/]*>/gi, '\\begin{enumerate}') + .replace(/<\/ol>/gi, '\\end{enumerate}') + .replace(/]*>(.*?)<\/li>/gi, '\\item $1') + .replace(/]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '\\url{$1}') + .replace(/]*>/gi, '\\\\') + .replace(/<[^>]*>/g, '') + .replace(/\n\s*\n\s*\n/g, '\n\n') + .trim(); + + return latex; +} + +// Server only generates LaTeX - client handles rendering with react-latex-next + +export { router as html2latexRoutes }; \ No newline at end of file