@@ -64,6 +64,48 @@ export function escapeLaTeX(text: string): string {
6464 return text . replace ( / [ \\ { } $ & % # _ ~ ^ ] / g, ( char ) => map [ char ] ) ;
6565}
6666
67+ /**
68+ * Escape LaTeX text while preserving inline math ($...$)
69+ */
70+ function escapeLatexPreservingMath ( text : string ) : string {
71+ let result = '' ;
72+ let i = 0 ;
73+ let inMath = false ;
74+ let mathStart = - 1 ;
75+
76+ while ( i < text . length ) {
77+ if ( text [ i ] === '$' && ( i === 0 || text [ i - 1 ] !== '\\' ) ) {
78+ if ( ! inMath ) {
79+ // Start of math
80+ mathStart = i ;
81+ inMath = true ;
82+ result += '$' ; // Keep the $ for math mode
83+ i ++ ;
84+ } else {
85+ // End of math - keep everything in math mode as-is
86+ result += text . substring ( mathStart + 1 , i ) + '$' ;
87+ inMath = false ;
88+ i ++ ;
89+ mathStart = - 1 ;
90+ }
91+ } else if ( ! inMath ) {
92+ // Outside math - escape special characters
93+ result += escapeLaTeX ( text [ i ] ) ;
94+ i ++ ;
95+ } else {
96+ // Inside math - don't process yet, will be added when we hit closing $
97+ i ++ ;
98+ }
99+ }
100+
101+ // Handle unclosed math
102+ if ( inMath ) {
103+ result += escapeLaTeX ( text . substring ( mathStart ) ) ;
104+ }
105+
106+ return result ;
107+ }
108+
67109/**
68110 * Get file extension for export format
69111 */
@@ -486,15 +528,220 @@ function renderInlineMath(text: string): string {
486528 return result ;
487529}
488530
531+ /**
532+ * Strip \htmlClass wrappers from LaTeX and replace with \textcolor
533+ * Converts \htmlClass{term-X}{content} → \textcolor{termX}{content}
534+ */
535+ function stripHtmlClassForLatex ( latex : string ) : string {
536+ let result = '' ;
537+ let i = 0 ;
538+
539+ while ( i < latex . length ) {
540+ // Look for \htmlClass{
541+ if ( latex . substring ( i , i + 11 ) === '\\htmlClass{' ) {
542+ // Find the closing }
543+ const classStart = i + 11 ;
544+ let classEnd = latex . indexOf ( '}' , classStart ) ;
545+
546+ if ( classEnd === - 1 ) {
547+ // Malformed, just copy and continue
548+ result += latex [ i ] ;
549+ i ++ ;
550+ continue ;
551+ }
552+
553+ const fullClassName = latex . substring ( classStart , classEnd ) ;
554+
555+ // Check if it's a term-X class
556+ if ( ! fullClassName . startsWith ( 'term-' ) ) {
557+ // Not a term class, just copy
558+ result += latex . substring ( i , classEnd + 1 ) ;
559+ i = classEnd + 1 ;
560+ continue ;
561+ }
562+
563+ // Extract className from term-X
564+ const className = fullClassName . substring ( 5 ) ; // Remove 'term-' prefix
565+ const latexColorName = `term${ className } ` ;
566+
567+ // Check if there's a { after }
568+ if ( latex [ classEnd + 1 ] !== '{' ) {
569+ // Malformed, just copy and continue
570+ result += latex . substring ( i , classEnd + 1 ) ;
571+ i = classEnd + 1 ;
572+ continue ;
573+ }
574+
575+ // Find the matching closing brace for content
576+ const contentStart = classEnd + 2 ; // After }{
577+ let braceCount = 1 ;
578+ let contentEnd = contentStart ;
579+
580+ while ( contentEnd < latex . length && braceCount > 0 ) {
581+ if ( latex [ contentEnd ] === '{' && latex [ contentEnd - 1 ] !== '\\' ) {
582+ braceCount ++ ;
583+ } else if ( latex [ contentEnd ] === '}' && latex [ contentEnd - 1 ] !== '\\' ) {
584+ braceCount -- ;
585+ }
586+ contentEnd ++ ;
587+ }
588+
589+ if ( braceCount !== 0 ) {
590+ // Unmatched braces, just copy and continue
591+ result += latex . substring ( i , contentStart ) ;
592+ i = contentStart ;
593+ continue ;
594+ }
595+
596+ // Extract content (excluding the final })
597+ const content = latex . substring ( contentStart , contentEnd - 1 ) ;
598+
599+ // Replace with \textcolor{termX}{content}
600+ result += `\\textcolor{${ latexColorName } }{${ content } }` ;
601+
602+ i = contentEnd ;
603+ } else {
604+ result += latex [ i ] ;
605+ i ++ ;
606+ }
607+ }
608+
609+ return result ;
610+ }
611+
612+ /**
613+ * Convert HTML description to LaTeX text
614+ * Strips <span> tags and converts content to LaTeX
615+ */
616+ function convertDescriptionToLatex ( html : string ) : string {
617+ let result = '' ;
618+ let i = 0 ;
619+
620+ while ( i < html . length ) {
621+ // Look for <span class="term-X">
622+ if ( html . substring ( i , i + 18 ) === '<span class="term-' ) {
623+ // Find the closing >
624+ const tagEnd = html . indexOf ( '>' , i ) ;
625+ if ( tagEnd === - 1 ) {
626+ result += escapeLaTeX ( html [ i ] ) ;
627+ i ++ ;
628+ continue ;
629+ }
630+
631+ // Extract class name
632+ const tagContent = html . substring ( i , tagEnd + 1 ) ;
633+ const classMatch = tagContent . match ( / c l a s s = " t e r m - ( [ ^ " ] + ) " / ) ;
634+
635+ if ( ! classMatch ) {
636+ result += escapeLaTeX ( html . substring ( i , tagEnd + 1 ) ) ;
637+ i = tagEnd + 1 ;
638+ continue ;
639+ }
640+
641+ const className = classMatch [ 1 ] ;
642+ const latexColorName = `term${ className } ` ;
643+
644+ // Find the closing </span>
645+ const closeTag = '</span>' ;
646+ const closeIndex = html . indexOf ( closeTag , tagEnd + 1 ) ;
647+
648+ if ( closeIndex === - 1 ) {
649+ result += escapeLaTeX ( html . substring ( i , tagEnd + 1 ) ) ;
650+ i = tagEnd + 1 ;
651+ continue ;
652+ }
653+
654+ // Extract content between tags
655+ const content = html . substring ( tagEnd + 1 , closeIndex ) ;
656+
657+ // Output colored text in LaTeX
658+ result += `\\textcolor{${ latexColorName } }{${ escapeLaTeX ( content ) } }` ;
659+
660+ i = closeIndex + closeTag . length ;
661+ } else if ( html . substring ( i , i + 3 ) === '<p>' ) {
662+ // Skip <p> tags
663+ i += 3 ;
664+ } else if ( html . substring ( i , i + 4 ) === '</p>' ) {
665+ // Replace </p> with paragraph break
666+ result += '\n\n' ;
667+ i += 4 ;
668+ } else {
669+ // Regular text - escape for LaTeX
670+ result += escapeLaTeX ( html [ i ] ) ;
671+ i ++ ;
672+ }
673+ }
674+
675+ return result . trim ( ) ;
676+ }
677+
489678/**
490679 * Export to LaTeX (complete document with xcolor)
491- * Implementation: Commit 3
680+ * Non-interactive: colors defined in preamble, applied with \textcolor
492681 */
493682export function exportToLaTeX (
494- _content : ParsedContent ,
495- _colorScheme : ColorScheme
683+ content : ParsedContent ,
684+ colorScheme : ColorScheme
496685) : string {
497- throw new Error ( 'LaTeX export not yet implemented' ) ;
686+ // Generate color definitions for preamble
687+ const colorDefinitions = content . termOrder
688+ . map ( ( className ) => {
689+ const color = getTermColor ( className , content . termOrder , colorScheme ) ;
690+ const latexColorName = `term${ className } ` ;
691+ // Convert #RRGGBB to uppercase hex for LaTeX
692+ const hexColor = color . replace ( '#' , '' ) . toUpperCase ( ) ;
693+ return `\\definecolor{${ latexColorName } }{HTML}{${ hexColor } }` ;
694+ } )
695+ . join ( '\n' ) ;
696+
697+ // Convert LaTeX: replace \htmlClass{term-X}{content} with \textcolor{termX}{content}
698+ const coloredLatex = stripHtmlClassForLatex ( content . latex ) ;
699+
700+ // Convert description: strip HTML tags, convert to LaTeX
701+ const descriptionLatex = convertDescriptionToLatex ( content . description ) ;
702+
703+ // Convert definitions
704+ const definitionsLatex = Array . from ( content . definitions . entries ( ) )
705+ . map ( ( [ className , definition ] ) => {
706+ const latexColorName = `term${ className } ` ;
707+ // Definitions are plain text, but may contain inline math $...$ which should be kept as-is
708+ // Escape text outside of math mode, keep math mode untouched
709+ const definitionLatex = escapeLatexPreservingMath ( definition ) ;
710+ return `\\subsection*{\\textcolor{${ latexColorName } }{${ escapeLaTeX ( className ) } }}\n${ definitionLatex } ` ;
711+ } )
712+ . join ( '\n\n' ) ;
713+
714+ return `\\documentclass{article}
715+
716+ % Minimal preamble
717+ \\usepackage[T1]{fontenc}
718+ \\usepackage{amsmath}
719+ \\usepackage{xcolor}
720+
721+ % Define colors from scheme
722+ ${ colorDefinitions }
723+
724+ \\title{${ escapeLaTeX ( content . title || 'Mathematical Equation' ) } }
725+ \\date{}
726+
727+ \\begin{document}
728+ \\maketitle
729+
730+ \\section*{Equation}
731+
732+ \\begin{equation}
733+ ${ coloredLatex }
734+ \\end{equation}
735+
736+ \\section*{Description}
737+
738+ ${ descriptionLatex }
739+
740+ \\section*{Definitions}
741+
742+ ${ definitionsLatex }
743+
744+ \\end{document}` ;
498745}
499746
500747/**
0 commit comments