Actualización del sistema de gestión de ecuaciones en el panel LaTeX, implementando un nuevo manejador de MathJax para mejorar la carga de ecuaciones. Se añade un menú para seleccionar el tamaño de fuente y se optimiza la visibilidad del panel LaTeX con un nuevo diseño de splitter. Se mejora la gestión de la configuración de tamaño de fuente y se implementa la funcionalidad para guardar el HTML del panel LaTeX para diagnóstico.

This commit is contained in:
Miguel 2025-06-12 13:31:44 +02:00
parent 01651fc454
commit 8c84eb4195
11 changed files with 1171 additions and 106 deletions

View File

@ -1,11 +1,16 @@
salario=horas*tarifa
salario=800
tarifa=36
#salario=8000
#tarifa=36
horas=?
plot(sin(x),(x,0,1))

2
.gitignore vendored
View File

@ -8,6 +8,8 @@ __pycache__/
*.json
.mathjax/
# Distribution / packaging
.Python
build/

View File

@ -331,4 +331,11 @@ class EvaluationManager:
def schedule_evaluation(self):
"""Programa una evaluación con debounce"""
self._debounce_timer.stop()
self._debounce_timer.start(300) # 300ms de debounce
self._debounce_timer.start(300) # 300ms de debounce
def handle_output_link_click(self, link_id: str, result_object):
"""Maneja clicks en links del output delegando al interactive manager"""
if self.main_window.interactive_manager:
self.main_window.interactive_manager.handle_interactive_click(result_object)
else:
self.logger.warning("Interactive manager no disponible para manejar click")

View File

@ -10,6 +10,7 @@ from PySide6.QtWebEngineWidgets import QWebEngineView
import logging
from typing import List, Dict, Optional
from .evaluation import EvaluationResult
from .mathjax_manager import MathJaxManager
import sympy
@ -24,6 +25,12 @@ class LatexPanel(QWidget):
self._pending_equations = []
self._parent_calculator = parent
self._webview_initialized = False
self._mathjax_timeout_count = 0
self._max_mathjax_timeout = 20 # 10 segundos (20 * 500ms)
# Manager de MathJax local
self.mathjax_manager = MathJaxManager()
self._setup_ui()
# Timer para verificar si MathJax está listo (inicializado bajo demanda)
@ -107,93 +114,131 @@ class LatexPanel(QWidget):
""")
def _generate_mathjax_html(self):
"""Genera HTML base con MathJax configurado para SVG"""
return """
<!DOCTYPE html>
"""Genera HTML base con MathJax configurado para SVG (local o CDN)"""
# Obtener URL de MathJax (local si está disponible, CDN como fallback)
mathjax_url = self.mathjax_manager.get_mathjax_url()
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
// Intentar cargar MathJax local primero, luego CDN como fallback
window.mathjaxLoadAttempts = 0;
window.mathJaxReady = false;
function loadMathJax(url, isFallback = false) {{
return new Promise((resolve, reject) => {{
const script = document.createElement('script');
script.id = 'MathJax-script';
script.async = true;
script.src = url;
script.onload = () => {{
console.log('MathJax cargado desde:', url);
resolve();
}};
script.onerror = () => {{
console.error('Error cargando MathJax desde:', url);
reject(new Error('MathJax load failed'));
}};
document.head.appendChild(script);
}});
}}
// Configuración de MathJax
window.MathJax = {{
tex: {{
inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
processEscapes: true
},
svg: {
}},
svg: {{
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
},
startup: {
ready: function () {
}},
startup: {{
ready: function () {{
MathJax.startup.defaultReady();
// Notificar que MathJax está listo
window.mathJaxReady = true;
console.log('MathJax SVG completamente cargado');
}
}
};
console.log('MathJax SVG completamente listo');
}}
}}
}};
// Intentar cargar MathJax local, luego CDN
loadMathJax('{mathjax_url}').catch(() => {{
console.log('Fallback a CDN de MathJax...');
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
}}).catch(() => {{
console.error('No se pudo cargar MathJax desde ninguna fuente');
// Marcar como listo para mostrar contenido sin renderizado
setTimeout(() => {{
window.mathJaxReady = true;
console.log('Modo fallback activado - sin renderizado LaTeX');
}}, 2000);
}});
</script>
<style>
body {
body {{
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation-block {
}}
.equation-block {{
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}
.equation-block:hover {
}}
.equation-block:hover {{
background: rgba(45, 45, 48, 0.9);
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
}}
.comment {{ border-left-color: #6a9955; }}
.assignment {{ border-left-color: #dcdcaa; }}
.equation {{ border-left-color: #c586c0; }}
.symbolic {{ border-left-color: #9cdcfe; }}
.math-content {
.math-content {{
margin: 2px 0;
font-size: 14px;
}
}}
.comment-text {
.comment-text {{
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}
}}
.info-message {
.info-message {{
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
}}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {
mjx-container[jax="SVG"] {{
margin: 2px 0 !important;
}
}}
mjx-container[jax="SVG"] svg {
mjx-container[jax="SVG"] svg {{
vertical-align: middle;
}
}}
/* Mejorar contraste del SVG */
mjx-container[jax="SVG"] svg text {
mjx-container[jax="SVG"] svg text {{
fill: #d4d4d4 !important;
}
}}
</style>
</head>
<body>
@ -204,53 +249,90 @@ class LatexPanel(QWidget):
</div>
<script>
window.mathJaxReady = false;
function addEquation(type, content) {{
try {{
var container = document.getElementById('equations-container');
if (!container) {{
console.error('Container no encontrado');
return;
}}
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {{
mathContent.className = 'comment-text';
mathContent.textContent = content;
console.log('Comentario agregado:', content);
}} else {{
mathContent.innerHTML = '$$' + content + '$$';
console.log('Ecuación agregada:', content);
}}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {{
console.log('Renderizando con MathJax...');
MathJax.typesetPromise([equation]).then(() => {{
console.log('Ecuación renderizada exitosamente');
}}).catch(function(err) {{
console.error('Error renderizando con MathJax:', err);
// Mostrar como texto plano si falla
mathContent.innerHTML = content;
}});
}} else if (type !== 'comment') {{
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
// Mostrar como texto plano si MathJax no está listo
mathContent.innerHTML = content;
}}
}} catch (e) {{
console.error('Error en addEquation:', e);
}}
}}
function addEquation(type, content) {
var container = document.getElementById('equations-container');
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
} else {
mathContent.innerHTML = '$$' + content + '$$';
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
MathJax.typesetPromise([equation]).catch(function(err) {
console.error('MathJax SVG error:', err);
});
}
}
function clearEquations() {
var container = document.getElementById('equations-container');
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
}
function clearEquations() {{
try {{
var container = document.getElementById('equations-container');
if (container) {{
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
console.log('Ecuaciones limpiadas');
}}
}} catch (e) {{
console.error('Error en clearEquations:', e);
}}
}}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {
if (window.mathJaxReady) {
// Notificar a Python que MathJax está listo
console.log('MathJax SVG listo para renderizado inicial');
return true;
}
return false;
}
function triggerInitialRender() {{
try {{
console.log('Verificando estado MathJax:', window.mathJaxReady);
if (window.mathJaxReady) {{
console.log('MathJax SVG listo para renderizado inicial');
return true;
}}
return false;
}} catch (e) {{
console.error('Error en triggerInitialRender:', e);
return false;
}}
}}
// Monitor de estado para debug
setInterval(() => {{
if (window.mathJaxReady) {{
console.log('🎯 MathJax listo detectado');
}}
}}, 3000);
</script>
</body>
</html>"""
@ -281,27 +363,67 @@ class LatexPanel(QWidget):
if not self._webview_available:
return
# Verificar si MathJax está listo
self.webview.page().runJavaScript(
"window.mathJaxReady || false;",
self._on_mathjax_ready_check
)
# Verificar múltiples condiciones de MathJax
js_check = """
(function() {
try {
// Verificar si window.mathJaxReady está definido
if (window.mathJaxReady === true) return true;
// Verificar si MathJax está disponible y listo
if (window.MathJax && window.MathJax.startup && window.MathJax.startup.document && window.MathJax.startup.document.state() >= 8) {
window.mathJaxReady = true;
return true;
}
// Verificar si existen las funciones necesarias
if (typeof addEquation === 'function' && typeof clearEquations === 'function') {
window.mathJaxReady = true;
return true;
}
return false;
} catch(e) {
console.error('Error verificando MathJax:', e);
return false;
}
})();
"""
self.webview.page().runJavaScript(js_check, self._on_mathjax_ready_check)
def _on_mathjax_ready_check(self, ready):
"""Callback cuando se verifica el estado de MathJax"""
logging.debug(f"🔍 MathJax ready check: {ready}")
if ready and not self._mathjax_ready:
self._mathjax_ready = True
self._mathjax_check_timer.stop()
logging.debug("✅ MathJax listo, procesando ecuaciones pendientes")
logging.debug(f"✅ MathJax listo, procesando {len(self._pending_equations)} ecuaciones pendientes")
# Renderizar ecuaciones pendientes
for eq in self._pending_equations:
self._add_equation_to_webview(eq['type'], eq['content'])
logging.debug(f"📊 Ecuación agregada al WebView: {eq['type']}")
self._pending_equations.clear()
# Trigger initial render si hay un calculador padre
if self._parent_calculator and hasattr(self._parent_calculator, '_trigger_initial_latex_render'):
self._parent_calculator._trigger_initial_latex_render()
elif not ready:
self._mathjax_timeout_count += 1
if self._mathjax_timeout_count >= self._max_mathjax_timeout:
# Timeout alcanzado, forzar como listo para mostrar contenido en texto plano
logging.warning("⚠️ MathJax timeout - cambiando a modo fallback")
self._mathjax_ready = True
self._mathjax_check_timer.stop()
# Procesar ecuaciones pendientes como texto plano
for eq in self._pending_equations:
self._add_equation_as_text(eq['type'], eq['content'])
self._pending_equations.clear()
else:
logging.debug(f"⏳ MathJax aún no está listo ({self._mathjax_timeout_count}/{self._max_mathjax_timeout}), continuando verificación...")
def _add_equation_to_webview(self, eq_type: str, content: str):
"""Añade una ecuación directamente al webview"""
@ -310,6 +432,22 @@ class LatexPanel(QWidget):
js_code = f"addEquation('{eq_type}', '{escaped_content}');"
self.webview.page().runJavaScript(js_code)
def _add_equation_as_text(self, eq_type: str, content: str):
"""Añade una ecuación como texto plano cuando MathJax falla"""
if self._webview_available:
escaped_content = content.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"')
# Agregar ecuación como texto plano sin MathJax
js_code = f"""
var container = document.getElementById('equations-container');
if (container) {{
var equation = document.createElement('div');
equation.className = 'equation-block {eq_type}';
equation.innerHTML = '<div class="math-content" style="font-family: monospace;">{escaped_content}</div>';
container.appendChild(equation);
}}
"""
self.webview.page().runJavaScript(js_code)
def add_equation(self, eq_type: str, content: str):
"""Añade una ecuación al panel"""
self.equations.append({'type': eq_type, 'content': content})

View File

@ -124,26 +124,40 @@ class HybridCalculatorPySide6(QMainWindow):
# Sincronizar scroll
self._setup_scroll_sync()
# Añadir splitter al layout
main_layout.addWidget(self.main_splitter)
# Crear splitter secundario para LaTeX
self.secondary_splitter = QSplitter(Qt.Horizontal)
self.secondary_splitter.addWidget(self.main_splitter)
# Botón expandible para LaTeX
self.latex_button = ExpandableLatexButton()
self.latex_button.clicked.connect(self._toggle_latex_panel)
main_layout.addWidget(self.latex_button)
# Panel LaTeX (inicialmente oculto)
# Panel LaTeX con splitter
self.latex_panel = LatexPanel(self)
self.latex_panel.setMinimumWidth(300)
self.latex_panel.setMaximumWidth(500)
self.latex_panel.setMinimumWidth(200)
if self.latex_panel_visible:
main_layout.addWidget(self.latex_panel)
self.secondary_splitter.addWidget(self.latex_panel)
self.latex_button.setChecked(True)
# Configurar tamaños: 70% para main_splitter, 30% para latex_panel
latex_width = self.settings.get("latex_panel_width", 300)
total_width = 1000 # Ancho por defecto de la ventana
main_width = total_width - latex_width
self.secondary_splitter.setSizes([main_width, latex_width])
else:
# Asegurar que el panel está oculto por defecto
self.latex_panel.hide()
self.latex_button.setChecked(False)
self.secondary_splitter.setSizes([1000])
# Layout principal con splitter secundario y botón
splitter_layout = QHBoxLayout()
splitter_layout.setContentsMargins(0, 0, 0, 0)
splitter_layout.setSpacing(0)
splitter_layout.addWidget(self.secondary_splitter)
splitter_layout.addWidget(self.latex_button)
main_layout.addLayout(splitter_layout)
# Los formatos de salida se configuran automáticamente en EvaluationManager
@ -154,6 +168,9 @@ class HybridCalculatorPySide6(QMainWindow):
# Aplicar tema oscuro
self._apply_dark_theme()
# Aplicar tamaño de fuente guardado
self._apply_saved_font_size()
def _setup_scroll_sync(self):
"""Sincroniza el scroll entre entrada y salida"""
@ -242,6 +259,22 @@ class HybridCalculatorPySide6(QMainWindow):
"""
self.setStyleSheet(dark_style)
def _apply_saved_font_size(self):
"""Aplica el tamaño de fuente guardado"""
is_large = self.settings_manager.get_setting("font_size_large", False)
font_size = 14 if is_large else 11
# Aplicar al estilo actual
current_style = self.styleSheet()
import re
# Reemplazar font-size en QPlainTextEdit y QTextEdit
pattern = r'(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})'
replacement = rf'\1font-size: {font_size}px;\2'
new_style = re.sub(pattern, replacement, current_style)
self.setStyleSheet(new_style)
def _on_input_changed(self):
"""Maneja cambios en el texto de entrada"""
# Delegar al autocomplete manager
@ -265,21 +298,28 @@ class HybridCalculatorPySide6(QMainWindow):
def _toggle_latex_panel(self):
"""Togglea la visibilidad del panel LaTeX"""
main_layout = self.centralWidget().layout()
if self.latex_panel_visible:
# Ocultar panel
main_layout.removeWidget(self.latex_panel)
self.secondary_splitter.widget(1).hide() if self.secondary_splitter.count() > 1 else None
self.latex_panel.hide()
self.latex_button.setChecked(False)
self.latex_panel_visible = False
self.secondary_splitter.setSizes([1000])
else:
# Mostrar panel
main_layout.addWidget(self.latex_panel)
if self.secondary_splitter.count() == 1:
self.secondary_splitter.addWidget(self.latex_panel)
self.latex_panel.show()
self.latex_button.setChecked(True)
self.latex_panel_visible = True
# Configurar tamaños
latex_width = self.settings.get("latex_panel_width", 300)
total_width = self.width()
main_width = total_width - latex_width - 50 # 50 para el botón
self.secondary_splitter.setSizes([main_width, latex_width])
# Actualizar panel con ecuaciones pendientes
self._sync_latex_equations_on_show()
@ -305,6 +345,13 @@ class HybridCalculatorPySide6(QMainWindow):
def closeEvent(self, event):
"""Maneja el cierre de la aplicación"""
# Guardar ancho del panel LaTeX si está visible
if self.latex_panel_visible and self.secondary_splitter.count() > 1:
sizes = self.secondary_splitter.sizes()
if len(sizes) > 1:
latex_width = sizes[1]
self.settings_manager.set_setting("latex_panel_width", latex_width)
# Delegar al settings manager
self.settings_manager.save_all()
event.accept()

View File

@ -76,6 +76,31 @@ class MenuManager:
view_menu.addSeparator()
# Submenu de fuentes
font_menu = view_menu.addMenu("Tamaño de fuente")
font_small_action = QAction("Pequeña (11px)", self.main_window)
font_small_action.setCheckable(True)
font_small_action.triggered.connect(lambda: self.set_font_size(False))
font_menu.addAction(font_small_action)
font_large_action = QAction("Grande (14px)", self.main_window)
font_large_action.setCheckable(True)
font_large_action.triggered.connect(lambda: self.set_font_size(True))
font_menu.addAction(font_large_action)
# Marcar el tamaño actual
is_large = self.main_window.settings_manager.get_setting("font_size_large", False)
if is_large:
font_large_action.setChecked(True)
else:
font_small_action.setChecked(True)
self.font_small_action = font_small_action
self.font_large_action = font_large_action
view_menu.addSeparator()
system_info_action = QAction("Información del sistema", self.main_window)
system_info_action.triggered.connect(self.show_types_info)
view_menu.addAction(system_info_action)
@ -100,6 +125,10 @@ class MenuManager:
latex_status_action.triggered.connect(self._show_latex_panel_status)
diag_menu.addAction(latex_status_action)
save_html_action = QAction("💾 Guardar HTML del Panel LaTeX", self.main_window)
save_html_action.triggered.connect(self._save_latex_html)
diag_menu.addAction(save_html_action)
diag_menu.addSeparator()
copy_debug_action = QAction("📋 Copiar Debug al Portapapeles", self.main_window)
@ -212,6 +241,47 @@ class MenuManager:
except Exception as e:
QMessageBox.critical(self.main_window, "Error", f"No se pudo limpiar el historial:\n{e}")
def set_font_size(self, is_large: bool):
"""Cambia el tamaño de fuente de los paneles"""
# Guardar configuración
self.main_window.settings_manager.set_setting("font_size_large", is_large)
# Actualizar checkboxes
self.font_small_action.setChecked(not is_large)
self.font_large_action.setChecked(is_large)
# Aplicar nuevo tamaño
font_size = 14 if is_large else 11
self._apply_font_size_to_panels(font_size)
status_text = f"🔤 Fuente cambiada a {'grande' if is_large else 'pequeña'} ({font_size}px)"
self.main_window._update_status(status_text)
def _apply_font_size_to_panels(self, font_size: int):
"""Aplica el tamaño de fuente a los paneles de texto"""
style_update = f"""
QPlainTextEdit, QTextEdit {{
font-size: {font_size}px;
}}
"""
# Actualizar estilo de los paneles principales
current_style = self.main_window.styleSheet()
# Reemplazar solo la parte de font-size en QPlainTextEdit y QTextEdit
import re
# Patrón para encontrar y reemplazar font-size en QPlainTextEdit y QTextEdit
pattern = r'(QPlainTextEdit, QTextEdit\s*\{[^}]*?)font-size:\s*\d+px;([^}]*\})'
replacement = rf'\1font-size: {font_size}px;\2'
new_style = re.sub(pattern, replacement, current_style)
# Si no se encontró el patrón, agregar el estilo
if new_style == current_style:
new_style += style_update
self.main_window.setStyleSheet(new_style)
# ========== FUNCIONES DE MENÚ HERRAMIENTAS ==========
def reload_types(self):
@ -314,6 +384,70 @@ PARA SOLUCIONAR:
self._show_info_dialog("Estado Panel LaTeX", status_message)
def _save_latex_html(self):
"""Guarda el HTML del panel LaTeX para diagnóstico"""
try:
# Obtener HTML del panel LaTeX
latex_panel = self.main_window.latex_panel
if hasattr(latex_panel, '_webview_available') and latex_panel._webview_available:
# Panel con WebEngine
if hasattr(latex_panel, 'webview') and latex_panel._webview_initialized:
# Obtener HTML actual del WebView
def on_html_received(html_content):
self._save_html_to_file(html_content, "webview_current")
latex_panel.webview.page().toHtml(on_html_received)
# También guardar el HTML base generado
base_html = latex_panel._generate_mathjax_html()
self._save_html_to_file(base_html, "webview_base")
else:
# WebEngine no inicializado, solo HTML base
base_html = latex_panel._generate_mathjax_html()
self._save_html_to_file(base_html, "webview_not_initialized")
else:
# Panel con fallback text browser
if hasattr(latex_panel, 'text_browser'):
fallback_html = latex_panel.text_browser.toHtml()
self._save_html_to_file(fallback_html, "fallback_textbrowser")
else:
self._save_html_to_file("No hay contenido HTML disponible", "no_content")
QMessageBox.information(
self.main_window,
"HTML Guardado",
"El HTML del panel LaTeX ha sido guardado en ./debug_html/ para diagnóstico."
)
except Exception as e:
self.logger.error(f"Error guardando HTML del panel LaTeX: {e}")
QMessageBox.critical(
self.main_window,
"Error",
f"Error guardando HTML: {e}"
)
def _save_html_to_file(self, html_content, file_suffix):
"""Guarda contenido HTML a un archivo"""
# Crear directorio de debug si no existe
debug_dir = Path("./debug_html")
debug_dir.mkdir(exist_ok=True)
# Generar nombre de archivo con timestamp
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"latex_panel_{file_suffix}_{timestamp}.html"
filepath = debug_dir / filename
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(html_content)
self.logger.info(f"HTML guardado en: {filepath}")
except Exception as e:
self.logger.error(f"Error escribiendo archivo HTML: {e}")
def _copy_debug_to_clipboard(self):
"""Copia información de debug completa al portapapeles"""
try:

View File

@ -31,7 +31,9 @@ class SettingsManager:
"window_geometry": None,
"splitter_sizes": None,
"debug_mode": False,
"latex_panel_visible": False
"latex_panel_visible": False,
"latex_panel_width": 300,
"font_size_large": False # False = pequeña (11px), True = grande (14px)
}
def _save_settings(self):

View File

@ -48,9 +48,13 @@ class OutputTextEdit(QTextEdit):
cursor = self.cursorForPosition(event.pos())
pos = cursor.position()
logging.debug(f"🖱️ Click en posición {pos}, links disponibles: {len(self.clickable_links)}")
# Buscar si el click fue en un link
for (start, end), (link_id, obj) in self.clickable_links.items():
logging.debug(f"🔍 Verificando link {start}-{end}: {link_id}")
if start <= pos <= end:
logging.debug(f"✅ Click en link detectado: {link_id}")
self.link_clicked.emit(link_id, obj)
return

120
app/mathjax_manager.py Normal file
View File

@ -0,0 +1,120 @@
"""
Sistema de gestión de MathJax local con descarga automática
"""
import os
import zipfile
import logging
from pathlib import Path
from urllib.request import urlretrieve
from urllib.error import URLError
class MathJaxManager:
"""Gestor de MathJax local con descarga automática"""
MATHJAX_VERSION = "3.2.2"
MATHJAX_URL = f"https://github.com/mathjax/MathJax/archive/{MATHJAX_VERSION}.zip"
LOCAL_DIR = Path(".mathjax")
def __init__(self):
self.logger = logging.getLogger(__name__)
self.mathjax_dir = self.LOCAL_DIR / f"MathJax-{self.MATHJAX_VERSION}"
self.es5_dir = self.mathjax_dir / "es5"
def get_mathjax_url(self) -> str:
"""
Obtiene la URL de MathJax (local si está disponible, CDN si no)
"""
if self.is_local_available():
# Construir URL local relativa para file://
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
self.logger.debug(f"Usando MathJax local: {local_url}")
return local_url
else:
# Descargar automáticamente si no existe
if self.download_mathjax():
local_url = f"file:///{self.es5_dir.absolute().as_posix()}/tex-svg.js"
self.logger.info(f"MathJax descargado y configurado localmente: {local_url}")
return local_url
else:
# Fallback a CDN
cdn_url = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"
self.logger.warning(f"Fallback a CDN: {cdn_url}")
return cdn_url
def is_local_available(self) -> bool:
"""Verifica si MathJax está disponible localmente"""
tex_svg_file = self.es5_dir / "tex-svg.js"
return tex_svg_file.exists() and tex_svg_file.is_file()
def download_mathjax(self) -> bool:
"""
Descarga MathJax automáticamente si no está disponible
Returns:
True si la descarga fue exitosa, False si falló
"""
try:
self.logger.info(f"Descargando MathJax {self.MATHJAX_VERSION}...")
# Crear directorio si no existe
self.LOCAL_DIR.mkdir(exist_ok=True)
# Archivo temporal para la descarga
zip_path = self.LOCAL_DIR / f"mathjax-{self.MATHJAX_VERSION}.zip"
# Descargar archivo ZIP
self.logger.debug(f"Descargando desde: {self.MATHJAX_URL}")
urlretrieve(self.MATHJAX_URL, zip_path)
# Extraer archivo
self.logger.debug(f"Extrayendo en: {self.LOCAL_DIR}")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(self.LOCAL_DIR)
# Limpiar archivo ZIP
zip_path.unlink()
# Verificar que se extrajo correctamente
if self.is_local_available():
self.logger.info(f"✅ MathJax {self.MATHJAX_VERSION} descargado correctamente")
return True
else:
self.logger.error("❌ Error: MathJax no se extrajo correctamente")
return False
except URLError as e:
self.logger.error(f"❌ Error de red descargando MathJax: {e}")
return False
except zipfile.BadZipFile as e:
self.logger.error(f"❌ Error: archivo ZIP corrupto: {e}")
# Limpiar archivo corrupto
if zip_path.exists():
zip_path.unlink()
return False
except Exception as e:
self.logger.error(f"❌ Error inesperado descargando MathJax: {e}")
return False
def clean_local_mathjax(self):
"""
Limpia la instalación local de MathJax
(Para forzar redownload en la siguiente inicialización)
"""
try:
if self.LOCAL_DIR.exists():
import shutil
shutil.rmtree(self.LOCAL_DIR)
self.logger.info("🗑️ MathJax local limpiado")
except Exception as e:
self.logger.error(f"Error limpiando MathJax local: {e}")
def get_status_info(self) -> dict:
"""Obtiene información del estado de MathJax"""
return {
"local_available": self.is_local_available(),
"local_dir": str(self.LOCAL_DIR.absolute()),
"mathjax_dir": str(self.mathjax_dir.absolute()) if self.mathjax_dir.exists() else None,
"version": self.MATHJAX_VERSION,
"tex_svg_file": str(self.es5_dir / "tex-svg.js") if self.is_local_available() else None
}

View File

@ -0,0 +1,218 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script>
// Intentar cargar MathJax local primero, luego CDN como fallback
window.mathjaxLoadAttempts = 0;
window.mathJaxReady = false;
function loadMathJax(url, isFallback = false) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = 'MathJax-script';
script.async = true;
script.src = url;
script.onload = () => {
console.log('MathJax cargado desde:', url);
resolve();
};
script.onerror = () => {
console.error('Error cargando MathJax desde:', url);
reject(new Error('MathJax load failed'));
};
document.head.appendChild(script);
});
}
// Configuración de MathJax
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
svg: {
scale: 0.9,
minScale: 0.5,
fontCache: 'global'
},
startup: {
ready: function () {
MathJax.startup.defaultReady();
window.mathJaxReady = true;
console.log('MathJax SVG completamente listo');
}
}
};
// Intentar cargar MathJax local, luego CDN
loadMathJax('file:///D:/Proyectos/Scripts/Calcv2/.mathjax/MathJax-3.2.2/es5/tex-svg.js').catch(() => {
console.log('Fallback a CDN de MathJax...');
return loadMathJax('https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-svg.js', true);
}).catch(() => {
console.error('No se pudo cargar MathJax desde ninguna fuente');
// Marcar como listo para mostrar contenido sin renderizado
setTimeout(() => {
window.mathJaxReady = true;
console.log('Modo fallback activado - sin renderizado LaTeX');
}, 2000);
});
</script>
<style>
body {
background-color: #1a1a1a;
color: #d4d4d4;
font-family: 'Segoe UI', Arial, sans-serif;
margin: 0;
padding: 8px;
line-height: 1.2;
}
.equation-block {
background: rgba(45, 45, 48, 0.8);
border-left: 3px solid;
margin: 4px 0;
padding: 6px 12px;
border-radius: 4px;
transition: all 0.1s ease;
}
.equation-block:hover {
background: rgba(45, 45, 48, 0.9);
}
.comment { border-left-color: #6a9955; }
.assignment { border-left-color: #dcdcaa; }
.equation { border-left-color: #c586c0; }
.symbolic { border-left-color: #9cdcfe; }
.math-content {
margin: 2px 0;
font-size: 14px;
}
.comment-text {
font-style: italic;
color: #6a9955;
font-size: 12px;
margin: 0;
}
.info-message {
text-align: center;
padding: 20px;
color: #666;
font-size: 12px;
}
/* Optimizaciones para SVG rendering */
mjx-container[jax="SVG"] {
margin: 2px 0 !important;
}
mjx-container[jax="SVG"] svg {
vertical-align: middle;
}
mjx-container[jax="SVG"] svg text {
fill: #d4d4d4 !important;
}
</style>
</head>
<body>
<div id="equations-container">
<div class="info-message">
Panel de Ecuaciones LaTeX (SVG)
</div>
</div>
<script>
function addEquation(type, content) {
try {
var container = document.getElementById('equations-container');
if (!container) {
console.error('Container no encontrado');
return;
}
// Limpiar mensaje inicial si existe
var infoMsg = container.querySelector('.info-message');
if (infoMsg) infoMsg.remove();
var equation = document.createElement('div');
equation.className = 'equation-block ' + type;
var mathContent = document.createElement('div');
mathContent.className = 'math-content';
if (type === 'comment') {
mathContent.className = 'comment-text';
mathContent.textContent = content;
console.log('Comentario agregado:', content);
} else {
mathContent.innerHTML = '$$' + content + '$$';
console.log('Ecuación agregada:', content);
}
equation.appendChild(mathContent);
container.appendChild(equation);
// Re-renderizar MathJax solo si está listo
if (window.MathJax && window.mathJaxReady && type !== 'comment') {
console.log('Renderizando con MathJax...');
MathJax.typesetPromise([equation]).then(() => {
console.log('Ecuación renderizada exitosamente');
}).catch(function(err) {
console.error('Error renderizando con MathJax:', err);
// Mostrar como texto plano si falla
mathContent.innerHTML = content;
});
} else if (type !== 'comment') {
console.log('MathJax no listo, mostrando como texto:', window.mathJaxReady);
// Mostrar como texto plano si MathJax no está listo
mathContent.innerHTML = content;
}
} catch (e) {
console.error('Error en addEquation:', e);
}
}
function clearEquations() {
try {
var container = document.getElementById('equations-container');
if (container) {
container.innerHTML = '<div class="info-message">Panel de Ecuaciones LaTeX (SVG)</div>';
console.log('Ecuaciones limpiadas');
}
} catch (e) {
console.error('Error en clearEquations:', e);
}
}
// Función para notificar que está listo para renderizar
function triggerInitialRender() {
try {
console.log('Verificando estado MathJax:', window.mathJaxReady);
if (window.mathJaxReady) {
console.log('MathJax SVG listo para renderizado inicial');
return true;
}
return false;
} catch (e) {
console.error('Error en triggerInitialRender:', e);
return false;
}
}
// Monitor de estado para debug
setInterval(() => {
if (window.mathJaxReady) {
console.log('🎯 MathJax listo detectado');
}
}, 3000);
</script>
</body>
</html>

File diff suppressed because one or more lines are too long