From 649c86afa5e980d4e036760f9db302ad2d8c3213 Mon Sep 17 00:00:00 2001 From: Yusuke Watanabe Date: Mon, 27 Apr 2026 04:41:00 +0900 Subject: [PATCH] refactor(tex): extract scitex.tex into standalone scitex-tex package LaTeX helpers (export to .tex, compile via pdflatex, render preview images, to_vec sig-fig formatting) now live in the standalone scitex-tex package (https://github.com/ywatanabe1989/scitex-tex). scitex.tex/__init__.py becomes a sys.modules alias. The [tex] extra is updated to depend on scitex-tex>=0.1.0. Zero scitex.* deps in the new package; only matplotlib (for preview). 128/128 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 5 +- src/scitex/tex/__init__.py | 34 +- src/scitex/tex/_export.py | 912 --------------- src/scitex/tex/_preview.py | 117 -- src/scitex/tex/_skills/SKILL.md | 56 - src/scitex/tex/_skills/compile.md | 118 -- src/scitex/tex/_skills/export.md | 143 --- src/scitex/tex/_skills/preview.md | 69 -- src/scitex/tex/_skills/to_vec.md | 102 -- src/scitex/tex/_to_vec.py | 118 -- tests/scitex/tex/__init__.py | 3 - tests/scitex/tex/test__export.py | 1813 ----------------------------- tests/scitex/tex/test__preview.py | 627 ---------- tests/scitex/tex/test__to_vec.py | 401 ------- 14 files changed, 22 insertions(+), 4496 deletions(-) delete mode 100755 src/scitex/tex/_export.py delete mode 100755 src/scitex/tex/_preview.py delete mode 100644 src/scitex/tex/_skills/SKILL.md delete mode 100644 src/scitex/tex/_skills/compile.md delete mode 100644 src/scitex/tex/_skills/export.md delete mode 100644 src/scitex/tex/_skills/preview.md delete mode 100644 src/scitex/tex/_skills/to_vec.md delete mode 100755 src/scitex/tex/_to_vec.py delete mode 100644 tests/scitex/tex/__init__.py delete mode 100644 tests/scitex/tex/test__export.py delete mode 100644 tests/scitex/tex/test__preview.py delete mode 100644 tests/scitex/tex/test__to_vec.py diff --git a/pyproject.toml b/pyproject.toml index bdfecd610..fce7782a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -682,9 +682,8 @@ template = [ # Tex Module - LaTeX utilities # Use: pip install scitex[tex] -tex = [ - "matplotlib", -] +# Real implementation lives in the standalone scitex-tex package. +tex = ["scitex-tex>=0.1.0"] # Tunnel Module - SSH reverse tunnel for NAT traversal # Use: pip install scitex[tunnel] diff --git a/src/scitex/tex/__init__.py b/src/scitex/tex/__init__.py index 3275cc141..a94956731 100755 --- a/src/scitex/tex/__init__.py +++ b/src/scitex/tex/__init__.py @@ -1,14 +1,20 @@ -#!/usr/bin/env python3 -"""LaTeX utilities module for scitex.""" - -from ._export import CompileResult, compile_tex, export_tex -from ._preview import preview -from ._to_vec import to_vec - -__all__ = [ - "export_tex", - "compile_tex", - "CompileResult", - "preview", - "to_vec", -] +"""SciTeX tex — thin compatibility shim for scitex-tex. + +Aliases ``scitex.tex`` to the standalone ``scitex_tex`` package via ``sys.modules``. +``scitex.tex is scitex_tex``. + +Install: ``pip install scitex[tex]`` (or ``pip install scitex-tex``). +See: https://github.com/ywatanabe1989/scitex-tex +""" + +import sys as _sys + +try: + import scitex_tex as _real +except ImportError as _e: # pragma: no cover + raise ImportError( + "scitex.tex requires the 'scitex-tex' package. " + "Install with: pip install scitex[tex] (or: pip install scitex-tex)" + ) from _e + +_sys.modules[__name__] = _real diff --git a/src/scitex/tex/_export.py b/src/scitex/tex/_export.py deleted file mode 100755 index 1e4f4316c..000000000 --- a/src/scitex/tex/_export.py +++ /dev/null @@ -1,912 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Timestamp: 2025-12-11 16:00:00 -# File: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_export.py - -""" -Export SciTeX writer documents to LaTeX format. - -This module converts the intermediate document format (from scitex.msword -or scitex.writer) into LaTeX source files. -""" - -from __future__ import annotations - -import os -import re -import shutil -import subprocess -from dataclasses import dataclass -from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple - -# Journal-specific document class configurations -JOURNAL_PRESETS = { - "article": { - "document_class": "article", - "class_options": [], - "required_packages": [], - }, - "ieee": { - "document_class": "IEEEtran", - "class_options": ["conference"], - "required_packages": ["cite", "amsmath", "algorithmic"], - }, - "elsevier": { - "document_class": "elsarticle", - "class_options": ["preprint", "12pt"], - "required_packages": ["lineno", "hyperref"], - }, - "springer": { - "document_class": "svjour3", - "class_options": ["smallextended"], - "required_packages": [], - }, - "aps": { - "document_class": "revtex4-2", - "class_options": ["aps", "prl", "preprint"], - "required_packages": [], - }, - "mdpi": { - "document_class": "article", - "class_options": [], - "required_packages": ["mdpi"], - }, - "acm": { - "document_class": "acmart", - "class_options": ["sigconf"], - "required_packages": [], - }, -} - - -def export_tex( - writer_doc: Dict[str, Any], - output_path: str | Path, - document_class: str = "article", - packages: Optional[List[str]] = None, - preamble: Optional[str] = None, - image_dir: Optional[str | Path] = None, - export_images: bool = True, - journal_preset: Optional[str] = None, - class_options: Optional[List[str]] = None, - use_bibtex: bool = False, -) -> Path: - """ - Export a SciTeX writer document to LaTeX format. - - Parameters - ---------- - writer_doc : dict - SciTeX writer document structure containing: - - blocks: List of document blocks (headings, paragraphs, captions, etc.) - - metadata: Document metadata (title, author, etc.) - - images: Image references with binary data - - references: Bibliography entries - output_path : str | Path - Output path for the .tex file. - document_class : str - LaTeX document class (article, report, book, etc.). - Overridden if journal_preset is specified. - packages : list[str] | None - Additional LaTeX packages to include. - preamble : str | None - Additional preamble content. - image_dir : str | Path | None - Directory to save extracted images. If None, uses - "{output_stem}_figures/" next to the output .tex file. - Set export_images=False to skip image export. - export_images : bool - Whether to export images to files. Default True. - journal_preset : str | None - Use a journal-specific preset: "ieee", "elsevier", "springer", - "aps", "mdpi", "acm". Sets document_class and required packages. - class_options : list[str] | None - Document class options (e.g., ["12pt", "twocolumn"]). - use_bibtex : bool - If True, generate \\bibliography{} instead of thebibliography. - Creates a .bib file alongside the .tex file. - - Returns - ------- - Path - The path to the written .tex file. - - Examples - -------- - >>> from scitex.msword import load_docx - >>> from scitex.tex import export_tex - >>> doc = load_docx("manuscript.docx") - >>> export_tex(doc, "manuscript.tex") - PosixPath('manuscript.tex') - - >>> # Export for IEEE conference - >>> export_tex(doc, "manuscript.tex", journal_preset="ieee") - - >>> # Export with custom image directory - >>> export_tex(doc, "manuscript.tex", image_dir="./figures") - """ - output_path = Path(output_path) - - # Apply journal preset if specified - effective_class = document_class - effective_options = class_options or [] - extra_packages = [] - - if journal_preset and journal_preset in JOURNAL_PRESETS: - preset = JOURNAL_PRESETS[journal_preset] - effective_class = preset["document_class"] - effective_options = preset["class_options"] + (class_options or []) - extra_packages = preset["required_packages"] - - # Extract components from writer_doc - blocks = writer_doc.get("blocks", []) - metadata = writer_doc.get("metadata", {}) - references = writer_doc.get("references", []) - images = writer_doc.get("images", []) - - # Handle image export - image_map: Dict[str, str] = {} # hash -> relative path - if export_images and images: - if image_dir is None: - image_dir = output_path.parent / f"{output_path.stem}_figures" - else: - image_dir = Path(image_dir) - - image_dir.mkdir(parents=True, exist_ok=True) - image_map = _write_images_to_dir(images, image_dir, output_path.parent) - - # Combine packages - all_packages = extra_packages + (packages or []) - - # Build LaTeX content - latex_content = _build_latex_document( - blocks=blocks, - metadata=metadata, - references=references, - document_class=effective_class, - class_options=effective_options, - packages=all_packages if all_packages else None, - preamble=preamble, - image_map=image_map, - use_bibtex=use_bibtex, - output_stem=output_path.stem, - ) - - # Write to file - output_path.write_text(latex_content, encoding="utf-8") - - # Generate .bib file if using bibtex - if use_bibtex and references: - bib_path = output_path.with_suffix(".bib") - bib_content = _generate_bibtex(references) - bib_path.write_text(bib_content, encoding="utf-8") - - return output_path - - -def _generate_bibtex(references: List[Dict[str, Any]]) -> str: - """Generate BibTeX content from references.""" - entries = [] - for ref in references: - num = ref.get("number", len(entries) + 1) - text = ref.get("text", ref.get("raw", "")) - - # Basic entry - in practice, would parse author/title/year - entry = f"""@misc{{ref{num}, - note = {{{text}}} -}}""" - entries.append(entry) - - return "\n\n".join(entries) - - -def _write_images_to_dir( - images: List[Dict[str, Any]], - image_dir: Path, - tex_parent: Path, -) -> Dict[str, str]: - """ - Write images to directory and return hash->relative_path mapping. - - Parameters - ---------- - images : list - List of image dicts with 'hash', 'extension', 'data' keys. - image_dir : Path - Directory to write images to. - tex_parent : Path - Parent directory of the .tex file (for relative paths). - - Returns - ------- - dict - Mapping from image hash to relative path for LaTeX. - """ - image_map = {} - fig_counter = 0 - - for img in images: - img_hash = img.get("hash") - ext = img.get("extension", ".png") - data = img.get("data") - - if data is None or img_hash is None: - continue - - # Skip duplicates (same hash = same image content) - if img_hash in image_map: - continue - - fig_counter += 1 - filename = f"fig_{fig_counter}{ext}" - filepath = image_dir / filename - - # Write image data - filepath.write_bytes(data) - - # Store relative path from tex file location - try: - rel_path = filepath.relative_to(tex_parent) - except ValueError: - rel_path = filepath - - image_map[img_hash] = str(rel_path) - - return image_map - - -def _build_latex_document( - blocks: List[Dict[str, Any]], - metadata: Dict[str, Any], - references: List[Dict[str, Any]], - document_class: str, - class_options: Optional[List[str]] = None, - packages: Optional[List[str]] = None, - preamble: Optional[str] = None, - image_map: Optional[Dict[str, str]] = None, - use_bibtex: bool = False, - output_stem: str = "document", -) -> str: - """Build complete LaTeX document content.""" - if image_map is None: - image_map = {} - lines = [] - - # Document class with options - if class_options: - opts = ",".join(class_options) - lines.append(f"\\documentclass[{opts}]{{{document_class}}}") - else: - lines.append(f"\\documentclass{{{document_class}}}") - lines.append("") - - # Default packages - default_packages = [ - "inputenc", - "fontenc", - "amsmath", - "amssymb", - "graphicx", - "hyperref", - ] - - # Package options - package_options = { - "inputenc": "utf8", - "fontenc": "T1", - } - - for pkg in default_packages: - opt = package_options.get(pkg) - if opt: - lines.append(f"\\usepackage[{opt}]{{{pkg}}}") - else: - lines.append(f"\\usepackage{{{pkg}}}") - - # Additional packages - if packages: - for pkg in packages: - if pkg not in default_packages: - lines.append(f"\\usepackage{{{pkg}}}") - - lines.append("") - - # Metadata - if metadata.get("title"): - title = _escape_latex(metadata["title"]) - lines.append(f"\\title{{{title}}}") - if metadata.get("author"): - author = _escape_latex(metadata["author"]) - lines.append(f"\\author{{{author}}}") - - lines.append("") - - # Additional preamble - if preamble: - lines.append(preamble) - lines.append("") - - # Begin document - lines.append("\\begin{document}") - lines.append("") - - # Title - if metadata.get("title"): - lines.append("\\maketitle") - lines.append("") - - # Track list state for proper itemize/enumerate environments - in_list = False - list_type = None - - # Process blocks - for i, block in enumerate(blocks): - btype = block.get("type") - - # Handle list transitions - if btype == "list-item": - item_list_type = block.get("list_type", "unordered") - if not in_list: - env = "enumerate" if item_list_type == "ordered" else "itemize" - lines.append(f"\\begin{{{env}}}") - in_list = True - list_type = item_list_type - elif in_list: - # Close list environment - env = "enumerate" if list_type == "ordered" else "itemize" - lines.append(f"\\end{{{env}}}") - lines.append("") - in_list = False - list_type = None - - block_latex = _convert_block_to_latex(block, image_map) - if block_latex: - lines.append(block_latex) - - # Close any open list - if in_list: - env = "enumerate" if list_type == "ordered" else "itemize" - lines.append(f"\\end{{{env}}}") - lines.append("") - - # References section - if references: - lines.append("") - if use_bibtex: - lines.append(f"\\bibliographystyle{{plain}}") - lines.append(f"\\bibliography{{{output_stem}}}") - else: - lines.append("\\begin{thebibliography}{99}") - for ref in references: - ref_latex = _convert_reference_to_latex(ref) - if ref_latex: - lines.append(ref_latex) - lines.append("\\end{thebibliography}") - - # End document - lines.append("") - lines.append("\\end{document}") - - return "\n".join(lines) - - -def _convert_block_to_latex( - block: Dict[str, Any], - image_map: Optional[Dict[str, str]] = None, -) -> Optional[str]: - """Convert a single block to LaTeX.""" - if image_map is None: - image_map = {} - - btype = block.get("type", "paragraph") - text = block.get("text", "") - - if not text and btype not in ("table", "image", "caption", "equation"): - return None - - if btype == "heading": - return _convert_heading(block) - elif btype == "paragraph": - return _convert_paragraph(block) - elif btype == "caption": - return _convert_caption(block, image_map) - elif btype == "table": - return _convert_table(block) - elif btype == "image": - return _convert_image(block, image_map) - elif btype == "list-item": - return _convert_list_item(block) - elif btype == "equation": - return _convert_equation(block) - elif btype == "reference-paragraph": - # Skip - handled separately in references section - return None - else: - # Default: treat as paragraph - return _escape_latex(text) + "\n" - - -def _convert_equation(block: Dict[str, Any]) -> str: - """Convert an equation block to LaTeX.""" - latex = block.get("latex", "") - text = block.get("text", "") - - if latex: - # Use the converted LaTeX from OMML - return f"\\begin{{equation}}\n{latex}\n\\end{{equation}}\n" - elif text: - # Fallback: wrap text in equation environment - return f"\\begin{{equation}}\n{_escape_latex(text)}\n\\end{{equation}}\n" - return "" - - -def _convert_heading(block: Dict[str, Any]) -> str: - """Convert a heading block to LaTeX.""" - level = block.get("level", 1) - text = _escape_latex(block.get("text", "")) - - # Map heading levels to LaTeX commands - level_commands = { - 1: "section", - 2: "subsection", - 3: "subsubsection", - 4: "paragraph", - 5: "subparagraph", - } - - command = level_commands.get(level, "paragraph") - return f"\\{command}{{{text}}}\n" - - -def _convert_paragraph(block: Dict[str, Any]) -> str: - """Convert a paragraph block to LaTeX.""" - runs = block.get("runs", []) - - if runs: - # Build paragraph from formatted runs - parts = [] - for run in runs: - run_text = _escape_latex(run.get("text", "")) - if run.get("bold"): - run_text = f"\\textbf{{{run_text}}}" - if run.get("italic"): - run_text = f"\\textit{{{run_text}}}" - if run.get("underline"): - run_text = f"\\underline{{{run_text}}}" - parts.append(run_text) - return "".join(parts) + "\n" - else: - return _escape_latex(block.get("text", "")) + "\n" - - -def _convert_caption( - block: Dict[str, Any], - image_map: Optional[Dict[str, str]] = None, -) -> str: - """Convert a caption block to LaTeX figure/table environment.""" - if image_map is None: - image_map = {} - - caption_type = block.get("caption_type", "") - number = block.get("number", "") - caption_text = _escape_latex(block.get("caption_text", block.get("text", ""))) - image_hash = block.get("image_hash") - - if caption_type == "figure": - # Check if we have an associated image - image_path = None - if image_hash and image_hash in image_map: - image_path = image_map[image_hash] - - lines = [ - "\\begin{figure}[htbp]", - "\\centering", - ] - - if image_path: - # Remove extension for includegraphics - image_path_no_ext = ( - image_path.rsplit(".", 1)[0] if "." in image_path else image_path - ) - lines.append( - f"\\includegraphics[width=0.8\\textwidth]{{{image_path_no_ext}}}" - ) - else: - lines.append(f"% Image placeholder for Figure {number}") - - lines.extend( - [ - f"\\caption{{{caption_text}}}", - f"\\label{{fig:{number}}}", - "\\end{figure}", - "", - ] - ) - return "\n".join(lines) - - elif caption_type == "table": - # Table captions - typically above the table - return f"% Table {number}: {caption_text}\n" - - else: - return f"% Caption: {caption_text}\n" - - -def _convert_image( - block: Dict[str, Any], - image_map: Optional[Dict[str, str]] = None, -) -> str: - """Convert an image block to LaTeX includegraphics.""" - if image_map is None: - image_map = {} - - image_hash = block.get("image_hash") or block.get("hash") - width = block.get("width", "0.8\\textwidth") - - if image_hash and image_hash in image_map: - image_path = image_map[image_hash] - # Remove extension for includegraphics - image_path_no_ext = ( - image_path.rsplit(".", 1)[0] if "." in image_path else image_path - ) - - lines = [ - "\\begin{figure}[htbp]", - "\\centering", - f"\\includegraphics[width={width}]{{{image_path_no_ext}}}", - "\\end{figure}", - "", - ] - return "\n".join(lines) - - return "% Image placeholder\n" - - -def _convert_table(block: Dict[str, Any]) -> str: - """Convert a table block to LaTeX.""" - rows = block.get("rows", []) - if not rows: - return "" - - num_cols = len(rows[0]) if rows else 0 - col_spec = "|" + "c|" * num_cols - - lines = [ - "\\begin{table}[htbp]", - "\\centering", - f"\\begin{{tabular}}{{{col_spec}}}", - "\\hline", - ] - - for i, row in enumerate(rows): - escaped_cells = [_escape_latex(str(cell)) for cell in row] - lines.append(" & ".join(escaped_cells) + " \\\\") - lines.append("\\hline") - - lines.extend( - [ - "\\end{tabular}", - "\\end{table}", - "", - ] - ) - - return "\n".join(lines) - - -def _convert_list_item(block: Dict[str, Any]) -> str: - """Convert a list item to LaTeX.""" - text = _escape_latex(block.get("text", "")) - return f"\\item {text}\n" - - -def _convert_reference_to_latex(ref: Dict[str, Any]) -> str: - """Convert a reference entry to LaTeX bibitem.""" - number = ref.get("number") - text = _escape_latex(ref.get("text", ref.get("raw", ""))) - - if number: - return f"\\bibitem{{ref{number}}} {text}" - else: - return f"\\bibitem{{}} {text}" - - -def _escape_latex(text: str) -> str: - """Escape special LaTeX characters.""" - if not text: - return "" - - # Characters that need escaping in LaTeX - replacements = [ - ("\\", "\\textbackslash{}"), - ("&", "\\&"), - ("%", "\\%"), - ("$", "\\$"), - ("#", "\\#"), - ("_", "\\_"), - ("{", "\\{"), - ("}", "\\}"), - ("~", "\\textasciitilde{}"), - ("^", "\\textasciicircum{}"), - ] - - # Apply replacements (order matters - backslash first) - result = text - for old, new in replacements: - # Skip if already escaped - if old == "\\": - # Don't escape existing LaTeX commands - result = re.sub(r"(? CompileResult: - """ - Compile a LaTeX file to PDF. - - Parameters - ---------- - tex_path : str | Path - Path to the .tex file. - output_dir : str | Path | None - Output directory for PDF. If None, uses same directory as tex file. - compiler : str - LaTeX compiler to use: "pdflatex", "xelatex", "lualatex", or "latexmk". - Default is "pdflatex". - runs : int - Number of compilation passes (for references/ToC). Default is 2. - Ignored if compiler is "latexmk". - clean : bool - Remove auxiliary files (.aux, .log, .out, etc.) after compilation. - Default is True. - timeout : int - Timeout in seconds for each compilation pass. Default is 120. - - Returns - ------- - CompileResult - Compilation result with success status, PDF path, and logs. - - Examples - -------- - >>> from scitex.tex import compile_tex - >>> result = compile_tex("manuscript.tex") - >>> if result.success: - ... print(f"PDF created: {result.pdf_path}") - ... else: - ... print(f"Errors: {result.errors}") - - >>> # Use latexmk for automatic multi-pass compilation - >>> result = compile_tex("manuscript.tex", compiler="latexmk") - - Notes - ----- - Requires LaTeX to be installed on the system (texlive, miktex, etc.). - """ - tex_path = Path(tex_path).absolute() - - if not tex_path.exists(): - return CompileResult( - success=False, - pdf_path=None, - exit_code=1, - stdout="", - stderr=f"File not found: {tex_path}", - errors=[f"File not found: {tex_path}"], - ) - - # Determine output directory - if output_dir is None: - output_dir = tex_path.parent - else: - output_dir = Path(output_dir).absolute() - output_dir.mkdir(parents=True, exist_ok=True) - - # Check if compiler is available - compiler_cmd = shutil.which(compiler) - if compiler_cmd is None: - return CompileResult( - success=False, - pdf_path=None, - exit_code=127, - stdout="", - stderr=f"Compiler not found: {compiler}", - errors=[f"Compiler not found: {compiler}. Install texlive or miktex."], - ) - - # Build command - if compiler == "latexmk": - cmd = [ - compiler, - "-pdf", - "-interaction=nonstopmode", - f"-output-directory={output_dir}", - str(tex_path), - ] - runs = 1 # latexmk handles multi-pass - else: - cmd = [ - compiler, - "-interaction=nonstopmode", - "-halt-on-error", - f"-output-directory={output_dir}", - str(tex_path), - ] - - # Run compilation - stdout_all = [] - stderr_all = [] - exit_code = 0 - - for run_num in range(runs): - try: - result = subprocess.run( - cmd, - cwd=tex_path.parent, - capture_output=True, - text=True, - timeout=timeout, - ) - stdout_all.append(f"=== Pass {run_num + 1} ===\n{result.stdout}") - stderr_all.append(result.stderr) - exit_code = result.returncode - - # If compilation failed, don't continue - if exit_code != 0: - break - - except subprocess.TimeoutExpired: - return CompileResult( - success=False, - pdf_path=None, - exit_code=124, - stdout="\n".join(stdout_all), - stderr=f"Compilation timed out after {timeout} seconds", - errors=[f"Compilation timed out after {timeout} seconds"], - ) - except Exception as e: - return CompileResult( - success=False, - pdf_path=None, - exit_code=1, - stdout="\n".join(stdout_all), - stderr=str(e), - errors=[str(e)], - ) - - # Check for output PDF - pdf_name = tex_path.stem + ".pdf" - pdf_path = output_dir / pdf_name - - # Read log file for detailed errors/warnings - log_path = output_dir / (tex_path.stem + ".log") - log_content = "" - errors = [] - warnings = [] - - if log_path.exists(): - try: - log_content = log_path.read_text(encoding="utf-8", errors="replace") - errors, warnings = _parse_latex_log(log_content) - except Exception: - pass - - # Clean auxiliary files - if clean: - aux_extensions = [ - ".aux", - ".log", - ".out", - ".toc", - ".lof", - ".lot", - ".bbl", - ".blg", - ".fls", - ".fdb_latexmk", - ".synctex.gz", - ] - for ext in aux_extensions: - aux_file = output_dir / (tex_path.stem + ext) - if aux_file.exists(): - try: - aux_file.unlink() - except Exception: - pass - - success = exit_code == 0 and pdf_path.exists() - - return CompileResult( - success=success, - pdf_path=pdf_path if pdf_path.exists() else None, - exit_code=exit_code, - stdout="\n".join(stdout_all), - stderr="\n".join(stderr_all), - log_content=log_content, - errors=errors, - warnings=warnings, - ) - - -def _parse_latex_log(log_content: str) -> Tuple[List[str], List[str]]: - """Parse LaTeX log file for errors and warnings.""" - errors = [] - warnings = [] - - lines = log_content.split("\n") - - for i, line in enumerate(lines): - # Error patterns - if line.startswith("!"): - # Collect multi-line error message - error_lines = [line] - for j in range(i + 1, min(i + 5, len(lines))): - if lines[j].startswith("l.") or lines[j].strip() == "": - break - error_lines.append(lines[j]) - errors.append(" ".join(error_lines)) - - elif "Error:" in line or "Fatal error" in line: - errors.append(line.strip()) - - # Warning patterns - elif "Warning:" in line: - warnings.append(line.strip()) - elif "Underfull" in line or "Overfull" in line: - warnings.append(line.strip()) - - return errors, warnings - - -__all__ = ["export_tex", "compile_tex", "CompileResult"] diff --git a/src/scitex/tex/_preview.py b/src/scitex/tex/_preview.py deleted file mode 100755 index abb7ae8e4..000000000 --- a/src/scitex/tex/_preview.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -# Time-stamp: "2025-06-05 12:00:00 (ywatanabe)" -# File: ./src/scitex/tex/_preview.py - -""" -LaTeX preview functionality with fallback mechanisms. - -Functionality: - - Generate previews of LaTeX strings with automatic fallback - - Handle LaTeX rendering failures gracefully -Input: - List of LaTeX strings -Output: - Matplotlib figure with previews -Prerequisites: - matplotlib, numpy, scitex.plt, scitex.str._latex_fallback -""" - -import numpy as np - -try: - from scitex.str import latex_fallback_decorator, safe_latex_render - - FALLBACK_AVAILABLE = True -except ImportError: - FALLBACK_AVAILABLE = False - - def latex_fallback_decorator(fallback_strategy="auto", preserve_math=True): - def decorator(func): - return func - - return decorator - - def safe_latex_render(text, fallback_strategy="auto", preserve_math=True): - return text - - -@latex_fallback_decorator(fallback_strategy="auto", preserve_math=True) -def preview(tex_str_list, enable_fallback=True): - r""" - Generate a preview of LaTeX strings with automatic fallback. - - Parameters - ---------- - tex_str_list : list of str - List of LaTeX strings to preview - enable_fallback : bool, optional - Whether to enable LaTeX fallback mechanisms, by default True - - Returns - ------- - matplotlib.figure.Figure - Figure containing the previews - - Examples - -------- - >>> tex_strings = ["x^2", r"\sum_{i=1}^n i", r"\alpha + \beta"] - >>> fig = preview(tex_strings) - >>> scitex.plt.show() - - Notes - ----- - If LaTeX rendering fails, this function automatically falls back to - mathtext or unicode alternatives while preserving the preview layout. - """ - from scitex.plt import subplots - - if not isinstance(tex_str_list, (list, tuple)): - tex_str_list = [tex_str_list] - - fig, axes = subplots( - nrows=len(tex_str_list), ncols=1, figsize=(10, 3 * len(tex_str_list)) - ) - axes = np.atleast_1d(axes) - - for ax, tex_string in zip(axes, tex_str_list): - try: - # Original LaTeX string (raw) - if enable_fallback and FALLBACK_AVAILABLE: - safe_raw = safe_latex_render(tex_string, "unicode", preserve_math=False) - ax.text(0.5, 0.7, safe_raw, size=20, ha="center", va="center") - else: - ax.text(0.5, 0.7, tex_string, size=20, ha="center", va="center") - - # LaTeX-formatted string - latex_formatted = ( - f"${tex_string}$" - if not (tex_string.startswith("$") and tex_string.endswith("$")) - else tex_string - ) - - if enable_fallback and FALLBACK_AVAILABLE: - safe_latex = safe_latex_render(latex_formatted, preserve_math=True) - ax.text(0.5, 0.3, safe_latex, size=20, ha="center", va="center") - else: - ax.text(0.5, 0.3, latex_formatted, size=20, ha="center", va="center") - - except Exception as e: - # Fallback for individual preview failures - ax.text(0.5, 0.7, f"Raw: {tex_string}", size=16, ha="center", va="center") - ax.text( - 0.5, - 0.3, - f"Error: {str(e)[:50]}...", - size=12, - ha="center", - va="center", - color="red", - ) - - ax.hide_spines() - - fig.tight_layout() - return fig - - -# EOF diff --git a/src/scitex/tex/_skills/SKILL.md b/src/scitex/tex/_skills/SKILL.md deleted file mode 100644 index 0868e3ff4..000000000 --- a/src/scitex/tex/_skills/SKILL.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: stx.tex -description: LaTeX utilities — export writer documents to .tex, compile to PDF, preview LaTeX strings as figures, and convert strings to vector notation. Use when working with LaTeX in the SciTeX ecosystem. -user-invocable: false ---- - -# stx.tex — LaTeX Utilities - -Utility functions for LaTeX authoring workflows. Accessed via `import scitex as stx` then `stx.tex.`. - -## Sub-skills - -### Document Export -- [export.md](export.md) — `export_tex`: convert a SciTeX writer document dict to a `.tex` file with journal presets (IEEE, Elsevier, Springer, APS, MDPI, ACM), image extraction, and optional BibTeX output - -### Compilation -- [compile.md](compile.md) — `compile_tex`, `CompileResult`: invoke pdflatex / xelatex / lualatex / latexmk and get back a structured result with success flag, PDF path, and parsed errors/warnings - -### String Preview -- [preview.md](preview.md) — `preview`: render a list of LaTeX strings as a matplotlib figure with automatic fallback to mathtext or unicode when a system LaTeX engine is absent - -### Vector Notation -- [to_vec.md](to_vec.md) — `to_vec`, `safe_to_vec`: format a string as `\overrightarrow{\mathrm{...}}` with configurable fallback (auto, mathtext, unicode, plain) - -## Quick Reference - -```python -import scitex as stx - -# Export writer doc to .tex (journal preset) -stx.tex.export_tex(writer_doc, "paper.tex", journal_preset="ieee") - -# Compile to PDF -result = stx.tex.compile_tex("paper.tex", compiler="latexmk") -if result.success: - print(result.pdf_path) -else: - print(result.errors) - -# Preview LaTeX expressions as a figure -fig = stx.tex.preview([r"\alpha + \beta", r"\sum_{i=1}^n i"]) -stx.plt.show() - -# Vector notation for axis labels -ax.set_xlabel(stx.tex.to_vec("r")) # -> $\overrightarrow{\mathrm{r}}$ -``` - -## Exports - -| Name | Kind | Source | -|------|------|--------| -| `export_tex` | function | `_export.py` | -| `compile_tex` | function | `_export.py` | -| `CompileResult` | dataclass | `_export.py` | -| `preview` | function | `_preview.py` | -| `to_vec` | function | `_to_vec.py` | diff --git a/src/scitex/tex/_skills/compile.md b/src/scitex/tex/_skills/compile.md deleted file mode 100644 index f7543c8bc..000000000 --- a/src/scitex/tex/_skills/compile.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -description: Compile a .tex file to PDF using pdflatex, xelatex, lualatex, or latexmk. Returns a structured CompileResult with success flag, PDF path, and parsed errors/warnings from the log. ---- - -# compile_tex / CompileResult - -Invoke a system LaTeX compiler on a `.tex` file and return a structured result. - -## compile_tex - -```python -compile_tex( - tex_path: str | Path, - output_dir: str | Path | None = None, - compiler: str = "pdflatex", - runs: int = 2, - clean: bool = True, - timeout: int = 120, -) -> CompileResult -``` - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `tex_path` | `str \| Path` | required | Path to the `.tex` file | -| `output_dir` | `str \| Path \| None` | `None` | Destination for the PDF; defaults to the same directory as the `.tex` file | -| `compiler` | `str` | `"pdflatex"` | Compiler binary: `"pdflatex"`, `"xelatex"`, `"lualatex"`, or `"latexmk"` | -| `runs` | `int` | `2` | Number of passes (needed for cross-references and ToC); ignored when `compiler="latexmk"` | -| `clean` | `bool` | `True` | Remove auxiliary files (`.aux`, `.log`, `.out`, `.toc`, `.lof`, `.lot`, `.bbl`, `.blg`, `.fls`, `.fdb_latexmk`, `.synctex.gz`) after compilation | -| `timeout` | `int` | `120` | Per-pass timeout in seconds | - -**Returns** `CompileResult` - -**Compiler flags used internally** - -- `pdflatex` / `xelatex` / `lualatex`: `-interaction=nonstopmode -halt-on-error -output-directory=` -- `latexmk`: `-pdf -interaction=nonstopmode -output-directory=` (single pass, handles multi-pass internally) - -**Error cases that return a failed CompileResult without raising** - -- `tex_path` does not exist -- `compiler` binary not on `PATH` (exit code 127) -- Compilation times out (exit code 124) -- Any unexpected exception during `subprocess.run` - ---- - -## CompileResult - -Dataclass returned by `compile_tex`. - -```python -@dataclass -class CompileResult: - success: bool # True if exit_code == 0 AND PDF file exists - pdf_path: Path | None # Absolute path to generated PDF, or None - exit_code: int # Shell exit code of the last compiler run - stdout: str # Combined stdout from all passes (labelled "=== Pass N ===") - stderr: str # Combined stderr from all passes - log_content: str # Raw content of the .log file (empty if clean=True or not found) - errors: list[str] # Lines starting with "!" or containing "Error:" / "Fatal error" - warnings: list[str] # Lines containing "Warning:", "Underfull", "Overfull" -``` - -Note: when `clean=True` (default) the `.log` file is deleted before it can be read, so `log_content` will be empty. Set `clean=False` to retain logs. - ---- - -## Examples - -```python -from scitex.tex import compile_tex - -# Basic compilation — pdflatex, 2 passes, clean auxiliary files -result = compile_tex("manuscript.tex") -if result.success: - print(f"PDF at: {result.pdf_path}") -else: - print("Compilation failed") - for err in result.errors: - print(" ", err) - -# latexmk (handles all passes, bibliography, etc.) -result = compile_tex("manuscript.tex", compiler="latexmk") - -# XeLaTeX, custom output directory, keep log for inspection -result = compile_tex( - "manuscript.tex", - compiler="xelatex", - output_dir="./build", - clean=False, -) -if not result.success: - print(result.log_content) # Full .log available because clean=False - -# Three passes for complex cross-references -result = compile_tex("thesis.tex", runs=3, clean=False) -print(result.warnings) - -# Access raw output -result = compile_tex("draft.tex") -print(result.stdout) # "=== Pass 1 ===\n...\n=== Pass 2 ===\n..." -print(result.stderr) -print(result.exit_code) # 0 on success -``` - -## Requirements - -A LaTeX distribution must be installed (TeX Live, MiKTeX, MacTeX, etc.) and the chosen compiler binary must be on `PATH`. - -Check availability before calling: - -```python -import shutil -if shutil.which("pdflatex") is None: - print("pdflatex not found — install texlive-latex-base") -``` diff --git a/src/scitex/tex/_skills/export.md b/src/scitex/tex/_skills/export.md deleted file mode 100644 index 0e1e68283..000000000 --- a/src/scitex/tex/_skills/export.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -description: Export a SciTeX writer document dict to a .tex file. Handles document class, journal presets, package injection, image extraction, and BibTeX generation. ---- - -# export_tex - -Convert a SciTeX writer document (dict) to a `.tex` file on disk. - -```python -export_tex( - writer_doc: dict, - output_path: str | Path, - document_class: str = "article", - packages: list[str] | None = None, - preamble: str | None = None, - image_dir: str | Path | None = None, - export_images: bool = True, - journal_preset: str | None = None, - class_options: list[str] | None = None, - use_bibtex: bool = False, -) -> Path -``` - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `writer_doc` | `dict` | required | SciTeX writer document with keys `"blocks"`, `"metadata"`, `"images"`, `"references"` | -| `output_path` | `str \| Path` | required | Destination `.tex` file path | -| `document_class` | `str` | `"article"` | LaTeX document class; overridden by `journal_preset` | -| `packages` | `list[str]` | `None` | Additional `\usepackage` entries (appended after defaults) | -| `preamble` | `str` | `None` | Raw string inserted into the preamble after `\usepackage` lines | -| `image_dir` | `str \| Path` | `None` | Directory to extract embedded images into; defaults to `{stem}_figures/` next to the `.tex` file | -| `export_images` | `bool` | `True` | Set `False` to skip writing image files | -| `journal_preset` | `str` | `None` | One of `"ieee"`, `"elsevier"`, `"springer"`, `"aps"`, `"mdpi"`, `"acm"` | -| `class_options` | `list[str]` | `None` | Document class options, e.g. `["12pt", "twocolumn"]` | -| `use_bibtex` | `bool` | `False` | Generate `\bibliography{}` + a `.bib` file instead of inline `thebibliography` | - -**Returns** `Path` — the written `.tex` file path. - -**Default packages always included** - -`inputenc` (utf8), `fontenc` (T1), `amsmath`, `amssymb`, `graphicx`, `hyperref` - -**Journal presets** - -| Preset | Document class | Extra packages | -|--------|---------------|----------------| -| `"ieee"` | `IEEEtran` (`conference`) | `cite`, `amsmath`, `algorithmic` | -| `"elsevier"` | `elsarticle` (`preprint,12pt`) | `lineno`, `hyperref` | -| `"springer"` | `svjour3` (`smallextended`) | — | -| `"aps"` | `revtex4-2` (`aps,prl,preprint`) | — | -| `"mdpi"` | `article` | `mdpi` | -| `"acm"` | `acmart` (`sigconf`) | — | - -**writer_doc structure** - -```python -writer_doc = { - "metadata": {"title": "My Paper", "author": "A. Author"}, - "blocks": [ - {"type": "heading", "level": 1, "text": "Introduction"}, - {"type": "paragraph", "text": "This study..."}, - {"type": "paragraph", "runs": [ - {"text": "Bold term", "bold": True}, - {"text": " followed by ", "bold": False}, - {"text": "italic", "italic": True}, - ]}, - {"type": "list-item", "list_type": "unordered", "text": "First point"}, - {"type": "list-item", "list_type": "unordered", "text": "Second point"}, - {"type": "equation", "latex": "E = mc^2"}, - {"type": "table", "rows": [["A", "B"], [1, 2]]}, - {"type": "image", "image_hash": "abc123", "width": "0.6\\textwidth"}, - {"type": "caption", "caption_type": "figure", "number": "1", - "caption_text": "Results", "image_hash": "abc123"}, - ], - "images": [ - {"hash": "abc123", "extension": ".png", "data": b""} - ], - "references": [ - {"number": 1, "text": "Author et al., Journal, 2024"} - ], -} -``` - -**Block types** - -| `type` | Required keys | Notes | -|--------|--------------|-------| -| `heading` | `level` (1–5), `text` | Maps to `\section` … `\subparagraph` | -| `paragraph` | `text` or `runs` | `runs` list supports `bold`, `italic`, `underline` per run | -| `list-item` | `text`, `list_type` (`"ordered"`/`"unordered"`) | Consecutive items of the same type are wrapped in one environment | -| `equation` | `latex` (preferred) or `text` | `latex` is emitted verbatim inside `equation` | -| `table` | `rows` (list of lists) | Centered, all columns `c`, full `\hline` borders | -| `image` | `image_hash`, optional `width` | `\includegraphics` without extension | -| `caption` | `caption_type` (`"figure"`/`"table"`), `number`, `caption_text`, optional `image_hash` | Figures get full `figure` environment | -| `reference-paragraph` | any | Silently skipped (handled in `references` section) | - -**Examples** - -```python -from scitex.msword import load_docx -from scitex.tex import export_tex - -# Basic export from a DOCX -doc = load_docx("manuscript.docx") -tex_path = export_tex(doc, "manuscript.tex") -# -> PosixPath('manuscript.tex') -# -> PosixPath('manuscript_figures/') created if images present - -# IEEE conference format -export_tex(doc, "ieee_paper.tex", journal_preset="ieee") - -# Elsevier with explicit image directory -export_tex( - doc, - "elsevier_paper.tex", - journal_preset="elsevier", - image_dir="./figures", - export_images=True, -) - -# Custom preamble and extra packages -export_tex( - doc, - "custom.tex", - document_class="report", - class_options=["12pt", "twoside"], - packages=["booktabs", "siunitx"], - preamble="\\setlength{\\parindent}{0pt}\n\\setlength{\\parskip}{6pt}", -) - -# Use BibTeX references -export_tex(doc, "paper.tex", use_bibtex=True) -# -> writes paper.tex AND paper.bib -``` - -**LaTeX escaping** - -`_escape_latex()` is applied to all text content. Special characters handled: -`\`, `&`, `%`, `$`, `#`, `_`, `{`, `}`, `~`, `^` - -Existing LaTeX commands (e.g. `\alpha`) are not double-escaped. diff --git a/src/scitex/tex/_skills/preview.md b/src/scitex/tex/_skills/preview.md deleted file mode 100644 index e1a3037dc..000000000 --- a/src/scitex/tex/_skills/preview.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -description: Render a list of LaTeX strings as a matplotlib figure. Each string is shown twice — raw and math-formatted. Automatically falls back to mathtext or unicode when a full LaTeX engine is unavailable. ---- - -# preview - -Render LaTeX strings visually in a matplotlib figure with automatic fallback. - -```python -preview( - tex_str_list: str | list[str], - enable_fallback: bool = True, -) -> matplotlib.figure.Figure -``` - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `tex_str_list` | `str \| list[str]` | required | One or more LaTeX strings to preview. A bare string is wrapped in a one-element list. | -| `enable_fallback` | `bool` | `True` | When `True`, runs strings through `safe_latex_render` before passing to matplotlib, avoiding crashes if a system LaTeX installation is absent. | - -**Returns** `matplotlib.figure.Figure` - -Each input string gets its own subplot (height 3 inches per string, width 10 inches). - -For each subplot, the string is displayed in two positions: -- Top row (y=0.7): raw text, run through `safe_latex_render(..., "unicode", preserve_math=False)` if fallback is enabled. -- Bottom row (y=0.3): math-formatted string wrapped in `$...$` (unless already wrapped), run through `safe_latex_render(..., preserve_math=True)` if fallback is enabled. - -If rendering of an individual string fails, the subplot shows the raw string and a red error message instead of raising. - -**Fallback behaviour** - -`preview` is decorated with `@latex_fallback_decorator(fallback_strategy="auto", preserve_math=True)` from `scitex.str._latex_fallback`. When that module is unavailable (ImportError), the decorator is a no-op and strings are passed directly to matplotlib. - -**Examples** - -```python -import scitex as stx - -# Preview a single expression -fig = stx.tex.preview(r"\alpha + \beta = \gamma") -stx.plt.show() - -# Preview multiple expressions -expressions = [ - r"x^2 + y^2 = r^2", - r"\sum_{i=1}^{n} i = \frac{n(n+1)}{2}", - r"\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}", - r"\nabla \cdot \mathbf{E} = \frac{\rho}{\varepsilon_0}", -] -fig = stx.tex.preview(expressions) -stx.io.save(fig, "latex_preview.png") - -# Disable fallback (will crash if LaTeX not installed) -fig = stx.tex.preview([r"\frac{1}{2}"], enable_fallback=False) - -# Check figure layout -fig = stx.tex.preview(["a", "b", "c"]) -print(fig.get_size_inches()) # (10, 9) — 3 strings × 3 inches each -``` - -**Notes** - -- The function uses `scitex.plt.subplots` (not bare `matplotlib.pyplot.subplots`). -- Axes spines are hidden via `ax.hide_spines()`. -- `fig.tight_layout()` is called before returning. -- This function previews LaTeX *string notation* (for figures, axis labels, etc.) not full `.tex` documents. For full document preview use `compile_tex` + a system PDF viewer. diff --git a/src/scitex/tex/_skills/to_vec.md b/src/scitex/tex/_skills/to_vec.md deleted file mode 100644 index 0f0432cfa..000000000 --- a/src/scitex/tex/_skills/to_vec.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -description: Convert a string to LaTeX vector notation (\overrightarrow{\mathrm{...}}). Supports automatic fallback to mathtext or unicode when a system LaTeX engine is unavailable. ---- - -# to_vec / safe_to_vec - -Convert a string to LaTeX vector notation with configurable fallback. - -## to_vec - -```python -to_vec( - v_str: str, - enable_fallback: bool = True, - fallback_strategy: str = "auto", -) -> str -``` - -**Parameters** - -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `v_str` | `str` | required | String to format as a vector (e.g. `"AB"`, `"v"`) | -| `enable_fallback` | `bool` | `True` | When `True`, applies fallback rendering if LaTeX is unavailable | -| `fallback_strategy` | `str` | `"auto"` | How to handle rendering failure (see table below) | - -**Returns** `str` — LaTeX string, or fallback representation if LaTeX is unavailable. - -**Fallback strategies** - -| Strategy | Behaviour | Example output | -|----------|-----------|---------------| -| `"auto"` | Try mathtext; if that fails, use unicode | `$\overrightarrow{\mathrm{AB}}$` or `AB⃗` | -| `"mathtext"` | Wrap in `$...$` for matplotlib mathtext | `$\overrightarrow{\mathrm{AB}}$` | -| `"unicode"` | Unicode combining right arrow above (U+20D7) | `AB⃗` | -| `"plain"` | Plain-text wrapper | `vec(AB)` | - -When `enable_fallback=False`, the function returns the raw LaTeX string regardless of system capabilities: -`\overrightarrow{\mathrm{AB}}` - -**Function is decorated** with `@latex_fallback_decorator(fallback_strategy="auto", preserve_math=True)` from `scitex.str._latex_fallback`. When that module is unavailable the decorator is a no-op. - ---- - -## safe_to_vec - -Convenience wrapper with explicit fallback control and `enable_fallback` always `True`. - -```python -safe_to_vec( - v_str: str, - fallback_strategy: str = "auto", -) -> str -``` - -Equivalent to `to_vec(v_str, enable_fallback=True, fallback_strategy=fallback_strategy)`. - ---- - -## Aliases - -`vector_notation` is a module-level alias for `to_vec` (backward compatibility). - ---- - -## Examples - -```python -import scitex as stx - -# Default — auto fallback -v = stx.tex.to_vec("AB") -# Returns: "$\overrightarrow{\mathrm{AB}}$" -# or "AB⃗" if mathtext fails - -# Force unicode output -v = stx.tex.to_vec("v", fallback_strategy="unicode") -# Returns: "v⃗" - -# Force plain text (no symbols) -v = stx.tex.to_vec("F", fallback_strategy="plain") -# Returns: "vec(F)" - -# Raw LaTeX (no fallback) -v = stx.tex.to_vec("E", enable_fallback=False) -# Returns: "\overrightarrow{\mathrm{E}}" - -# Use as axis label in a figure -fig, ax = stx.plt.subplots() -ax.set_xlabel(stx.tex.to_vec("r")) -ax.set_ylabel(stx.tex.to_vec("F")) - -# safe_to_vec convenience wrapper -v = stx.tex.safe_to_vec("AB", fallback_strategy="unicode") -# Returns: "AB⃗" - -# Backward compat alias -from scitex.tex._to_vec import vector_notation -v = vector_notation("k") # same as to_vec("k") -``` - -**Edge case**: empty string returns `""` without further processing. diff --git a/src/scitex/tex/_to_vec.py b/src/scitex/tex/_to_vec.py deleted file mode 100755 index 899c01a0e..000000000 --- a/src/scitex/tex/_to_vec.py +++ /dev/null @@ -1,118 +0,0 @@ -#!/usr/bin/env python3 -# Time-stamp: "2025-06-05 12:00:00 (ywatanabe)" -# File: ./src/scitex/tex/_to_vec.py - -""" -LaTeX vector notation with fallback mechanisms. - -Functionality: - - Convert strings to LaTeX vector notation with automatic fallback - - Handle LaTeX rendering failures gracefully -Input: - String representation of vector -Output: - LaTeX vector notation with fallback support -Prerequisites: - scitex.str._latex_fallback -""" - -try: - from scitex.str import latex_fallback_decorator, safe_latex_render - - FALLBACK_AVAILABLE = True -except ImportError: - FALLBACK_AVAILABLE = False - - def latex_fallback_decorator(fallback_strategy="auto", preserve_math=True): - def decorator(func): - return func - - return decorator - - def safe_latex_render(text, fallback_strategy="auto", preserve_math=True): - return text - - -@latex_fallback_decorator(fallback_strategy="auto", preserve_math=True) -def to_vec(v_str, enable_fallback=True, fallback_strategy="auto"): - r""" - Convert a string to LaTeX vector notation with automatic fallback. - - Parameters - ---------- - v_str : str - String representation of the vector - enable_fallback : bool, optional - Whether to enable LaTeX fallback mechanisms, by default True - fallback_strategy : str, optional - Fallback strategy: "auto", "mathtext", "unicode", "plain", by default "auto" - - Returns - ------- - str - LaTeX representation of the vector with automatic fallback - - Examples - -------- - >>> vector = to_vec("AB") - >>> print(vector) # LaTeX: \overrightarrow{\mathrm{AB}} - - >>> vector = to_vec("AB") # Falls back to unicode if LaTeX fails - >>> print(vector) # Unicode: A⃗B or AB⃗ - - Notes - ----- - If LaTeX rendering fails, this function automatically falls back to: - - mathtext: Uses matplotlib's built-in math rendering - - unicode: Uses Unicode vector symbols (⃗) - - plain: Returns plain text with "vec()" notation - """ - if not v_str: - return "" - - # Create LaTeX vector notation - latex_vector = f"\\overrightarrow{{\\mathrm{{{v_str}}}}}" - - if enable_fallback and FALLBACK_AVAILABLE: - # Custom fallback handling for vectors - if fallback_strategy == "auto": - # Try mathtext first, then unicode - try: - mathtext_result = safe_latex_render(f"${latex_vector}$", "mathtext") - return mathtext_result - except Exception: - # Fall back to unicode vector notation - return f"{v_str}⃗" # Unicode combining right arrow above - elif fallback_strategy == "unicode": - return f"{v_str}⃗" # Unicode combining right arrow above - elif fallback_strategy == "plain": - return f"vec({v_str})" - else: - return safe_latex_render(f"${latex_vector}$", fallback_strategy) - else: - return latex_vector - - -def safe_to_vec(v_str, fallback_strategy="auto"): - """ - Safe version of to_vec with explicit fallback control. - - Parameters - ---------- - v_str : str - String representation of the vector - fallback_strategy : str, optional - Explicit fallback strategy: "auto", "mathtext", "unicode", "plain" - - Returns - ------- - str - Vector notation with specified fallback behavior - """ - return to_vec(v_str, enable_fallback=True, fallback_strategy=fallback_strategy) - - -# Backward compatibility -vector_notation = to_vec - -# EOF diff --git a/tests/scitex/tex/__init__.py b/tests/scitex/tex/__init__.py deleted file mode 100644 index 617bc7fc2..000000000 --- a/tests/scitex/tex/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -"""Tests for scitex.tex module.""" diff --git a/tests/scitex/tex/test__export.py b/tests/scitex/tex/test__export.py deleted file mode 100644 index c0e700077..000000000 --- a/tests/scitex/tex/test__export.py +++ /dev/null @@ -1,1813 +0,0 @@ -#!/usr/bin/env python3 -# Time-stamp: "2026-01-05 14:00:00 (ywatanabe)" -# File: ./tests/scitex/tex/test__export.py - -"""Comprehensive tests for tex._export module.""" - -import shutil -import tempfile -from pathlib import Path -from unittest.mock import MagicMock, Mock, patch - -import pytest - -from scitex.tex._export import ( - JOURNAL_PRESETS, - CompileResult, - _build_latex_document, - _convert_caption, - _convert_equation, - _convert_heading, - _convert_image, - _convert_list_item, - _convert_paragraph, - _convert_reference_to_latex, - _convert_table, - _escape_latex, - _generate_bibtex, - _parse_latex_log, - _write_images_to_dir, - compile_tex, - export_tex, -) - - -class TestEscapeLatex: - """Tests for _escape_latex helper function.""" - - def test_empty_string(self): - """Test with empty string.""" - assert _escape_latex("") == "" - - def test_plain_text(self): - """Test with plain text (no special chars).""" - assert _escape_latex("Hello World") == "Hello World" - - def test_ampersand(self): - """Test escaping ampersand.""" - assert _escape_latex("A & B") == r"A \& B" - - def test_percent(self): - """Test escaping percent sign.""" - assert _escape_latex("100%") == r"100\%" - - def test_dollar(self): - """Test escaping dollar sign.""" - assert _escape_latex("$100") == r"\$100" - - def test_hash(self): - """Test escaping hash/pound sign.""" - assert _escape_latex("Item #1") == r"Item \#1" - - def test_underscore(self): - """Test escaping underscore.""" - assert _escape_latex("var_name") == r"var\_name" - - def test_braces(self): - """Test escaping curly braces.""" - assert _escape_latex("{text}") == r"\{text\}" - - def test_tilde(self): - """Test escaping tilde.""" - result = _escape_latex("~") - assert "textasciitilde" in result - - def test_caret(self): - """Test escaping caret.""" - result = _escape_latex("^") - assert "textasciicircum" in result - - def test_multiple_special_chars(self): - """Test escaping multiple special characters.""" - result = _escape_latex("$100 & 50%") - assert r"\$" in result - assert r"\&" in result - assert r"\%" in result - - def test_none_input(self): - """Test with None-like falsy input.""" - # Function handles empty string - assert _escape_latex("") == "" - - -class TestConvertHeading: - """Tests for _convert_heading helper function.""" - - def test_level_1_heading(self): - """Test level 1 heading (section).""" - block = {"type": "heading", "level": 1, "text": "Introduction"} - result = _convert_heading(block) - assert r"\section{Introduction}" in result - - def test_level_2_heading(self): - """Test level 2 heading (subsection).""" - block = {"type": "heading", "level": 2, "text": "Methods"} - result = _convert_heading(block) - assert r"\subsection{Methods}" in result - - def test_level_3_heading(self): - """Test level 3 heading (subsubsection).""" - block = {"type": "heading", "level": 3, "text": "Data Collection"} - result = _convert_heading(block) - assert r"\subsubsection{Data Collection}" in result - - def test_level_4_heading(self): - """Test level 4 heading (paragraph).""" - block = {"type": "heading", "level": 4, "text": "Details"} - result = _convert_heading(block) - assert r"\paragraph{Details}" in result - - def test_level_5_heading(self): - """Test level 5 heading (subparagraph).""" - block = {"type": "heading", "level": 5, "text": "Note"} - result = _convert_heading(block) - assert r"\subparagraph{Note}" in result - - def test_default_level(self): - """Test heading with missing level defaults to section.""" - block = {"type": "heading", "text": "Title"} - result = _convert_heading(block) - assert r"\section{Title}" in result - - def test_heading_with_special_chars(self): - """Test heading with special LaTeX characters.""" - block = {"type": "heading", "level": 1, "text": "Results & Discussion"} - result = _convert_heading(block) - assert r"\&" in result - - -class TestConvertParagraph: - """Tests for _convert_paragraph helper function.""" - - def test_simple_paragraph(self): - """Test simple paragraph without formatting.""" - block = {"type": "paragraph", "text": "This is a paragraph."} - result = _convert_paragraph(block) - assert "This is a paragraph." in result - - def test_paragraph_with_runs(self): - """Test paragraph with formatted runs.""" - block = { - "type": "paragraph", - "runs": [ - {"text": "Normal "}, - {"text": "bold", "bold": True}, - {"text": " text"}, - ], - } - result = _convert_paragraph(block) - assert r"\textbf{bold}" in result - assert "Normal" in result - - def test_paragraph_with_italic(self): - """Test paragraph with italic text.""" - block = { - "type": "paragraph", - "runs": [{"text": "emphasis", "italic": True}], - } - result = _convert_paragraph(block) - assert r"\textit{emphasis}" in result - - def test_paragraph_with_underline(self): - """Test paragraph with underlined text.""" - block = { - "type": "paragraph", - "runs": [{"text": "underlined", "underline": True}], - } - result = _convert_paragraph(block) - assert r"\underline{underlined}" in result - - def test_paragraph_with_combined_formatting(self): - """Test paragraph with bold, italic, and underline.""" - block = { - "type": "paragraph", - "runs": [ - {"text": "styled", "bold": True, "italic": True, "underline": True} - ], - } - result = _convert_paragraph(block) - assert r"\textbf" in result - assert r"\textit" in result - assert r"\underline" in result - - -class TestConvertTable: - """Tests for _convert_table helper function.""" - - def test_simple_table(self): - """Test simple 2x2 table.""" - block = { - "type": "table", - "rows": [["A", "B"], ["C", "D"]], - } - result = _convert_table(block) - assert r"\begin{table}" in result - assert r"\begin{tabular}" in result - assert r"\end{tabular}" in result - assert r"\end{table}" in result - assert "A & B" in result - assert "C & D" in result - - def test_empty_table(self): - """Test empty table.""" - block = {"type": "table", "rows": []} - result = _convert_table(block) - assert result == "" - - def test_table_with_special_chars(self): - """Test table with special LaTeX characters.""" - block = { - "type": "table", - "rows": [["100%", "$50"]], - } - result = _convert_table(block) - assert r"\%" in result - assert r"\$" in result - - def test_table_column_spec(self): - """Test table column specification.""" - block = { - "type": "table", - "rows": [["A", "B", "C"]], - } - result = _convert_table(block) - # Should have |c|c|c| for 3 columns - assert "|c|c|c|" in result - - -class TestConvertCaption: - """Tests for _convert_caption helper function.""" - - def test_figure_caption(self): - """Test figure caption.""" - block = { - "type": "caption", - "caption_type": "figure", - "number": "1", - "caption_text": "A sample figure", - } - result = _convert_caption(block) - assert r"\begin{figure}" in result - assert r"\caption{A sample figure}" in result - assert r"\label{fig:1}" in result - assert r"\end{figure}" in result - - def test_figure_caption_with_image(self): - """Test figure caption with associated image.""" - block = { - "type": "caption", - "caption_type": "figure", - "number": "2", - "caption_text": "With image", - "image_hash": "abc123", - } - image_map = {"abc123": "figures/fig_1.png"} - result = _convert_caption(block, image_map) - assert r"\includegraphics" in result - assert "figures/fig_1" in result - - def test_table_caption(self): - """Test table caption.""" - block = { - "type": "caption", - "caption_type": "table", - "number": "1", - "caption_text": "Sample table", - } - result = _convert_caption(block) - assert "Table 1" in result - assert "Sample table" in result - - def test_generic_caption(self): - """Test generic caption without type.""" - block = { - "type": "caption", - "caption_text": "Some caption", - } - result = _convert_caption(block) - assert "Caption:" in result - - -class TestConvertImage: - """Tests for _convert_image helper function.""" - - def test_image_with_hash(self): - """Test image conversion with valid hash.""" - block = {"type": "image", "image_hash": "hash123"} - image_map = {"hash123": "figures/fig_1.png"} - result = _convert_image(block, image_map) - assert r"\includegraphics" in result - assert "figures/fig_1" in result - - def test_image_placeholder(self): - """Test image placeholder when no hash match.""" - block = {"type": "image", "image_hash": "unknown"} - result = _convert_image(block, {}) - assert "placeholder" in result.lower() - - def test_image_custom_width(self): - """Test image with custom width.""" - block = {"type": "image", "image_hash": "hash123", "width": "0.5\\textwidth"} - image_map = {"hash123": "figures/fig_1.png"} - result = _convert_image(block, image_map) - assert "width=0.5" in result - - -class TestConvertListItem: - """Tests for _convert_list_item helper function.""" - - def test_simple_list_item(self): - """Test simple list item.""" - block = {"type": "list-item", "text": "First item"} - result = _convert_list_item(block) - assert r"\item First item" in result - - def test_list_item_with_special_chars(self): - """Test list item with special characters.""" - block = {"type": "list-item", "text": "Item with $100"} - result = _convert_list_item(block) - assert r"\$" in result - - -class TestConvertEquation: - """Tests for _convert_equation helper function.""" - - def test_equation_with_latex(self): - """Test equation with LaTeX content.""" - block = {"type": "equation", "latex": "E = mc^2"} - result = _convert_equation(block) - assert r"\begin{equation}" in result - assert "E = mc^2" in result - assert r"\end{equation}" in result - - def test_equation_with_text_fallback(self): - """Test equation with text fallback.""" - block = {"type": "equation", "text": "x + y = z"} - result = _convert_equation(block) - assert r"\begin{equation}" in result - - def test_empty_equation(self): - """Test equation with no content.""" - block = {"type": "equation"} - result = _convert_equation(block) - assert result == "" - - -class TestConvertReference: - """Tests for _convert_reference_to_latex helper function.""" - - def test_numbered_reference(self): - """Test numbered reference.""" - ref = {"number": 1, "text": "Author, Title, Year"} - result = _convert_reference_to_latex(ref) - assert r"\bibitem{ref1}" in result - assert "Author" in result - - def test_unnumbered_reference(self): - """Test unnumbered reference.""" - ref = {"text": "Anonymous reference"} - result = _convert_reference_to_latex(ref) - assert r"\bibitem{}" in result - - -class TestGenerateBibtex: - """Tests for _generate_bibtex helper function.""" - - def test_single_reference(self): - """Test generating bibtex for single reference.""" - refs = [{"number": 1, "text": "Smith, J. Paper Title. 2020"}] - result = _generate_bibtex(refs) - assert "@misc{ref1" in result - assert "note" in result - - def test_multiple_references(self): - """Test generating bibtex for multiple references.""" - refs = [ - {"number": 1, "text": "First ref"}, - {"number": 2, "text": "Second ref"}, - ] - result = _generate_bibtex(refs) - assert "@misc{ref1" in result - assert "@misc{ref2" in result - - -class TestParseLatexLog: - """Tests for _parse_latex_log helper function.""" - - def test_parse_error(self): - """Test parsing LaTeX error from log.""" - log = """ -! Undefined control sequence. -l.15 \\badcommand -""" - errors, warnings = _parse_latex_log(log) - assert len(errors) >= 1 - assert "Undefined control sequence" in errors[0] - - def test_parse_warning(self): - """Test parsing LaTeX warning from log.""" - log = """ -LaTeX Warning: Reference `fig:1' on page 1 undefined -""" - errors, warnings = _parse_latex_log(log) - assert len(warnings) >= 1 - - def test_parse_overfull(self): - """Test parsing overfull hbox warning.""" - log = """ -Overfull \\hbox (10.0pt too wide) in paragraph -""" - errors, warnings = _parse_latex_log(log) - assert any("Overfull" in w for w in warnings) - - def test_empty_log(self): - """Test parsing empty log.""" - errors, warnings = _parse_latex_log("") - assert errors == [] - assert warnings == [] - - -class TestJournalPresets: - """Tests for JOURNAL_PRESETS configuration.""" - - def test_presets_exist(self): - """Test that journal presets are defined.""" - assert "article" in JOURNAL_PRESETS - assert "ieee" in JOURNAL_PRESETS - assert "elsevier" in JOURNAL_PRESETS - assert "springer" in JOURNAL_PRESETS - assert "aps" in JOURNAL_PRESETS - assert "mdpi" in JOURNAL_PRESETS - assert "acm" in JOURNAL_PRESETS - - def test_preset_structure(self): - """Test that presets have required keys.""" - for name, preset in JOURNAL_PRESETS.items(): - assert "document_class" in preset - assert "class_options" in preset - assert "required_packages" in preset - - def test_ieee_preset(self): - """Test IEEE preset configuration.""" - preset = JOURNAL_PRESETS["ieee"] - assert preset["document_class"] == "IEEEtran" - assert "conference" in preset["class_options"] - - -class TestCompileResult: - """Tests for CompileResult dataclass.""" - - def test_create_success_result(self): - """Test creating successful compile result.""" - result = CompileResult( - success=True, - pdf_path=Path("/tmp/test.pdf"), - exit_code=0, - stdout="Output", - stderr="", - ) - assert result.success is True - assert result.exit_code == 0 - assert result.errors == [] - assert result.warnings == [] - - def test_create_failure_result(self): - """Test creating failed compile result.""" - result = CompileResult( - success=False, - pdf_path=None, - exit_code=1, - stdout="", - stderr="Error", - errors=["Compile error"], - ) - assert result.success is False - assert result.pdf_path is None - assert "Compile error" in result.errors - - def test_default_lists(self): - """Test that errors and warnings default to empty lists.""" - result = CompileResult( - success=True, - pdf_path=None, - exit_code=0, - stdout="", - stderr="", - ) - assert result.errors == [] - assert result.warnings == [] - - -class TestExportTex: - """Tests for export_tex function.""" - - def test_export_minimal_document(self): - """Test exporting minimal document.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - assert result == output_path - assert output_path.exists() - content = output_path.read_text() - assert r"\documentclass{article}" in content - assert r"\begin{document}" in content - assert r"\end{document}" in content - - def test_export_with_metadata(self): - """Test exporting document with metadata.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {"title": "Test Title", "author": "Test Author"}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert r"\title{Test Title}" in content - assert r"\author{Test Author}" in content - assert r"\maketitle" in content - - def test_export_with_heading(self): - """Test exporting document with heading.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [{"type": "heading", "level": 1, "text": "Introduction"}], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert r"\section{Introduction}" in content - - def test_export_with_paragraphs(self): - """Test exporting document with paragraphs.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [ - {"type": "paragraph", "text": "First paragraph."}, - {"type": "paragraph", "text": "Second paragraph."}, - ], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert "First paragraph." in content - assert "Second paragraph." in content - - def test_export_with_references(self): - """Test exporting document with references.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [{"number": 1, "text": "Smith, 2020"}], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert r"\begin{thebibliography}" in content - assert r"\bibitem{ref1}" in content - assert r"\end{thebibliography}" in content - - def test_export_with_bibtex(self): - """Test exporting document with bibtex.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [{"number": 1, "text": "Reference text"}], - "images": [], - } - result = export_tex(doc, output_path, use_bibtex=True) - - content = output_path.read_text() - assert r"\bibliography{test}" in content - - bib_path = output_path.with_suffix(".bib") - assert bib_path.exists() - bib_content = bib_path.read_text() - assert "@misc" in bib_content - - def test_export_with_journal_preset(self): - """Test exporting with journal preset.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path, journal_preset="ieee") - - content = output_path.read_text() - assert r"\documentclass[conference]{IEEEtran}" in content - - def test_export_with_class_options(self): - """Test exporting with class options.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path, class_options=["12pt", "twocolumn"]) - - content = output_path.read_text() - assert "12pt" in content - assert "twocolumn" in content - - def test_export_with_additional_packages(self): - """Test exporting with additional packages.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path, packages=["booktabs", "siunitx"]) - - content = output_path.read_text() - assert r"\usepackage{booktabs}" in content - assert r"\usepackage{siunitx}" in content - - def test_export_with_preamble(self): - """Test exporting with additional preamble.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [], - "metadata": {}, - "references": [], - "images": [], - } - preamble = r"\newcommand{\mycommand}{test}" - result = export_tex(doc, output_path, preamble=preamble) - - content = output_path.read_text() - assert preamble in content - - def test_export_with_list_items(self): - """Test exporting document with list items.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [ - { - "type": "list-item", - "text": "First item", - "list_type": "unordered", - }, - { - "type": "list-item", - "text": "Second item", - "list_type": "unordered", - }, - {"type": "paragraph", "text": "After list."}, - ], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert r"\begin{itemize}" in content - assert r"\item First item" in content - assert r"\item Second item" in content - assert r"\end{itemize}" in content - - def test_export_with_ordered_list(self): - """Test exporting document with ordered list.""" - with tempfile.TemporaryDirectory() as tmpdir: - output_path = Path(tmpdir) / "test.tex" - doc = { - "blocks": [ - {"type": "list-item", "text": "First", "list_type": "ordered"}, - {"type": "list-item", "text": "Second", "list_type": "ordered"}, - ], - "metadata": {}, - "references": [], - "images": [], - } - result = export_tex(doc, output_path) - - content = output_path.read_text() - assert r"\begin{enumerate}" in content - assert r"\end{enumerate}" in content - - -class TestCompileTex: - """Tests for compile_tex function.""" - - def test_compile_nonexistent_file(self): - """Test compiling nonexistent file.""" - result = compile_tex("/nonexistent/path/file.tex") - assert result.success is False - assert ( - "not found" in result.stderr.lower() - or "not found" in str(result.errors).lower() - ) - - def test_compile_missing_compiler(self): - """Test compile with missing compiler.""" - with tempfile.TemporaryDirectory() as tmpdir: - tex_path = Path(tmpdir) / "test.tex" - tex_path.write_text( - r"\documentclass{article}\begin{document}Test\end{document}" - ) - - result = compile_tex(tex_path, compiler="nonexistent_compiler") - assert result.success is False - assert result.exit_code == 127 - - @pytest.mark.skipif( - shutil.which("pdflatex") is None, - reason="pdflatex not installed", - ) - def test_compile_simple_document(self): - """Test compiling a simple document.""" - with tempfile.TemporaryDirectory() as tmpdir: - tex_path = Path(tmpdir) / "test.tex" - tex_content = r""" -\documentclass{article} -\begin{document} -Hello, World! -\end{document} -""" - tex_path.write_text(tex_content) - - result = compile_tex(tex_path, clean=True) - assert result.success is True - assert result.pdf_path is not None - assert result.pdf_path.exists() - - @pytest.mark.skipif( - shutil.which("pdflatex") is None, - reason="pdflatex not installed", - ) - def test_compile_with_error(self): - """Test compiling document with LaTeX error.""" - with tempfile.TemporaryDirectory() as tmpdir: - tex_path = Path(tmpdir) / "test.tex" - tex_content = r""" -\documentclass{article} -\begin{document} -\badcommand -\end{document} -""" - tex_path.write_text(tex_content) - - result = compile_tex(tex_path, clean=False) - assert result.success is False - assert len(result.errors) > 0 - - def test_compile_timeout(self): - """Test compile timeout handling.""" - with tempfile.TemporaryDirectory() as tmpdir: - tex_path = Path(tmpdir) / "test.tex" - tex_path.write_text( - r"\documentclass{article}\begin{document}Test\end{document}" - ) - - # Mock subprocess.run to raise TimeoutExpired - with patch("subprocess.run") as mock_run: - import subprocess - - mock_run.side_effect = subprocess.TimeoutExpired( - cmd="pdflatex", timeout=1 - ) - - # Also mock shutil.which to return a path - with patch("shutil.which", return_value="/usr/bin/pdflatex"): - result = compile_tex(tex_path, timeout=1) - assert result.success is False - assert result.exit_code == 124 - assert "timed out" in result.stderr.lower() - - -class TestWriteImagesToDir: - """Tests for _write_images_to_dir helper function.""" - - def test_write_single_image(self): - """Test writing single image.""" - with tempfile.TemporaryDirectory() as tmpdir: - image_dir = Path(tmpdir) / "figures" - image_dir.mkdir() - tex_parent = Path(tmpdir) - - images = [ - {"hash": "abc123", "extension": ".png", "data": b"\x89PNG\r\n\x1a\n"}, - ] - - result = _write_images_to_dir(images, image_dir, tex_parent) - - assert "abc123" in result - assert "figures" in result["abc123"] - - def test_skip_duplicate_images(self): - """Test skipping duplicate images.""" - with tempfile.TemporaryDirectory() as tmpdir: - image_dir = Path(tmpdir) / "figures" - image_dir.mkdir() - tex_parent = Path(tmpdir) - - images = [ - {"hash": "abc123", "extension": ".png", "data": b"data1"}, - {"hash": "abc123", "extension": ".png", "data": b"data2"}, - ] - - result = _write_images_to_dir(images, image_dir, tex_parent) - - # Should only have one entry - assert len(result) == 1 - - def test_skip_invalid_images(self): - """Test skipping images without data or hash.""" - with tempfile.TemporaryDirectory() as tmpdir: - image_dir = Path(tmpdir) / "figures" - image_dir.mkdir() - tex_parent = Path(tmpdir) - - images = [ - {"hash": None, "extension": ".png", "data": b"data"}, - {"hash": "abc", "extension": ".png", "data": None}, - ] - - result = _write_images_to_dir(images, image_dir, tex_parent) - - assert len(result) == 0 - - -class TestBuildLatexDocument: - """Tests for _build_latex_document helper function.""" - - def test_build_minimal_document(self): - """Test building minimal document.""" - result = _build_latex_document( - blocks=[], - metadata={}, - references=[], - document_class="article", - ) - - assert r"\documentclass{article}" in result - assert r"\begin{document}" in result - assert r"\end{document}" in result - - def test_build_with_options(self): - """Test building with class options.""" - result = _build_latex_document( - blocks=[], - metadata={}, - references=[], - document_class="article", - class_options=["12pt", "a4paper"], - ) - - assert r"\documentclass[12pt,a4paper]{article}" in result - - def test_default_packages(self): - """Test that default packages are included.""" - result = _build_latex_document( - blocks=[], - metadata={}, - references=[], - document_class="article", - ) - - assert r"\usepackage[utf8]{inputenc}" in result - assert r"\usepackage[T1]{fontenc}" in result - assert r"\usepackage{amsmath}" in result - assert r"\usepackage{graphicx}" in result - assert r"\usepackage{hyperref}" in result - - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_export.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # Timestamp: 2025-12-11 16:00:00 -# # File: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_export.py -# -# """ -# Export SciTeX writer documents to LaTeX format. -# -# This module converts the intermediate document format (from scitex.msword -# or scitex.writer) into LaTeX source files. -# """ -# -# from __future__ import annotations -# -# import os -# import re -# import shutil -# import subprocess -# from dataclasses import dataclass -# from pathlib import Path -# from typing import Any, Dict, List, Optional, Tuple -# -# # Journal-specific document class configurations -# JOURNAL_PRESETS = { -# "article": { -# "document_class": "article", -# "class_options": [], -# "required_packages": [], -# }, -# "ieee": { -# "document_class": "IEEEtran", -# "class_options": ["conference"], -# "required_packages": ["cite", "amsmath", "algorithmic"], -# }, -# "elsevier": { -# "document_class": "elsarticle", -# "class_options": ["preprint", "12pt"], -# "required_packages": ["lineno", "hyperref"], -# }, -# "springer": { -# "document_class": "svjour3", -# "class_options": ["smallextended"], -# "required_packages": [], -# }, -# "aps": { -# "document_class": "revtex4-2", -# "class_options": ["aps", "prl", "preprint"], -# "required_packages": [], -# }, -# "mdpi": { -# "document_class": "article", -# "class_options": [], -# "required_packages": ["mdpi"], -# }, -# "acm": { -# "document_class": "acmart", -# "class_options": ["sigconf"], -# "required_packages": [], -# }, -# } -# -# -# def export_tex( -# writer_doc: Dict[str, Any], -# output_path: str | Path, -# document_class: str = "article", -# packages: Optional[List[str]] = None, -# preamble: Optional[str] = None, -# image_dir: Optional[str | Path] = None, -# export_images: bool = True, -# journal_preset: Optional[str] = None, -# class_options: Optional[List[str]] = None, -# use_bibtex: bool = False, -# ) -> Path: -# """ -# Export a SciTeX writer document to LaTeX format. -# -# Parameters -# ---------- -# writer_doc : dict -# SciTeX writer document structure containing: -# - blocks: List of document blocks (headings, paragraphs, captions, etc.) -# - metadata: Document metadata (title, author, etc.) -# - images: Image references with binary data -# - references: Bibliography entries -# output_path : str | Path -# Output path for the .tex file. -# document_class : str -# LaTeX document class (article, report, book, etc.). -# Overridden if journal_preset is specified. -# packages : list[str] | None -# Additional LaTeX packages to include. -# preamble : str | None -# Additional preamble content. -# image_dir : str | Path | None -# Directory to save extracted images. If None, uses -# "{output_stem}_figures/" next to the output .tex file. -# Set export_images=False to skip image export. -# export_images : bool -# Whether to export images to files. Default True. -# journal_preset : str | None -# Use a journal-specific preset: "ieee", "elsevier", "springer", -# "aps", "mdpi", "acm". Sets document_class and required packages. -# class_options : list[str] | None -# Document class options (e.g., ["12pt", "twocolumn"]). -# use_bibtex : bool -# If True, generate \\bibliography{} instead of thebibliography. -# Creates a .bib file alongside the .tex file. -# -# Returns -# ------- -# Path -# The path to the written .tex file. -# -# Examples -# -------- -# >>> from scitex.msword import load_docx -# >>> from scitex.tex import export_tex -# >>> doc = load_docx("manuscript.docx") -# >>> export_tex(doc, "manuscript.tex") -# PosixPath('manuscript.tex') -# -# >>> # Export for IEEE conference -# >>> export_tex(doc, "manuscript.tex", journal_preset="ieee") -# -# >>> # Export with custom image directory -# >>> export_tex(doc, "manuscript.tex", image_dir="./figures") -# """ -# output_path = Path(output_path) -# -# # Apply journal preset if specified -# effective_class = document_class -# effective_options = class_options or [] -# extra_packages = [] -# -# if journal_preset and journal_preset in JOURNAL_PRESETS: -# preset = JOURNAL_PRESETS[journal_preset] -# effective_class = preset["document_class"] -# effective_options = preset["class_options"] + (class_options or []) -# extra_packages = preset["required_packages"] -# -# # Extract components from writer_doc -# blocks = writer_doc.get("blocks", []) -# metadata = writer_doc.get("metadata", {}) -# references = writer_doc.get("references", []) -# images = writer_doc.get("images", []) -# -# # Handle image export -# image_map: Dict[str, str] = {} # hash -> relative path -# if export_images and images: -# if image_dir is None: -# image_dir = output_path.parent / f"{output_path.stem}_figures" -# else: -# image_dir = Path(image_dir) -# -# image_dir.mkdir(parents=True, exist_ok=True) -# image_map = _write_images_to_dir(images, image_dir, output_path.parent) -# -# # Combine packages -# all_packages = extra_packages + (packages or []) -# -# # Build LaTeX content -# latex_content = _build_latex_document( -# blocks=blocks, -# metadata=metadata, -# references=references, -# document_class=effective_class, -# class_options=effective_options, -# packages=all_packages if all_packages else None, -# preamble=preamble, -# image_map=image_map, -# use_bibtex=use_bibtex, -# output_stem=output_path.stem, -# ) -# -# # Write to file -# output_path.write_text(latex_content, encoding="utf-8") -# -# # Generate .bib file if using bibtex -# if use_bibtex and references: -# bib_path = output_path.with_suffix(".bib") -# bib_content = _generate_bibtex(references) -# bib_path.write_text(bib_content, encoding="utf-8") -# -# return output_path -# -# -# def _generate_bibtex(references: List[Dict[str, Any]]) -> str: -# """Generate BibTeX content from references.""" -# entries = [] -# for ref in references: -# num = ref.get("number", len(entries) + 1) -# text = ref.get("text", ref.get("raw", "")) -# -# # Basic entry - in practice, would parse author/title/year -# entry = f"""@misc{{ref{num}, -# note = {{{text}}} -# }}""" -# entries.append(entry) -# -# return "\n\n".join(entries) -# -# -# def _write_images_to_dir( -# images: List[Dict[str, Any]], -# image_dir: Path, -# tex_parent: Path, -# ) -> Dict[str, str]: -# """ -# Write images to directory and return hash->relative_path mapping. -# -# Parameters -# ---------- -# images : list -# List of image dicts with 'hash', 'extension', 'data' keys. -# image_dir : Path -# Directory to write images to. -# tex_parent : Path -# Parent directory of the .tex file (for relative paths). -# -# Returns -# ------- -# dict -# Mapping from image hash to relative path for LaTeX. -# """ -# image_map = {} -# fig_counter = 0 -# -# for img in images: -# img_hash = img.get("hash") -# ext = img.get("extension", ".png") -# data = img.get("data") -# -# if data is None or img_hash is None: -# continue -# -# # Skip duplicates (same hash = same image content) -# if img_hash in image_map: -# continue -# -# fig_counter += 1 -# filename = f"fig_{fig_counter}{ext}" -# filepath = image_dir / filename -# -# # Write image data -# filepath.write_bytes(data) -# -# # Store relative path from tex file location -# try: -# rel_path = filepath.relative_to(tex_parent) -# except ValueError: -# rel_path = filepath -# -# image_map[img_hash] = str(rel_path) -# -# return image_map -# -# -# def _build_latex_document( -# blocks: List[Dict[str, Any]], -# metadata: Dict[str, Any], -# references: List[Dict[str, Any]], -# document_class: str, -# class_options: Optional[List[str]] = None, -# packages: Optional[List[str]] = None, -# preamble: Optional[str] = None, -# image_map: Optional[Dict[str, str]] = None, -# use_bibtex: bool = False, -# output_stem: str = "document", -# ) -> str: -# """Build complete LaTeX document content.""" -# if image_map is None: -# image_map = {} -# lines = [] -# -# # Document class with options -# if class_options: -# opts = ",".join(class_options) -# lines.append(f"\\documentclass[{opts}]{{{document_class}}}") -# else: -# lines.append(f"\\documentclass{{{document_class}}}") -# lines.append("") -# -# # Default packages -# default_packages = [ -# "inputenc", -# "fontenc", -# "amsmath", -# "amssymb", -# "graphicx", -# "hyperref", -# ] -# -# # Package options -# package_options = { -# "inputenc": "utf8", -# "fontenc": "T1", -# } -# -# for pkg in default_packages: -# opt = package_options.get(pkg) -# if opt: -# lines.append(f"\\usepackage[{opt}]{{{pkg}}}") -# else: -# lines.append(f"\\usepackage{{{pkg}}}") -# -# # Additional packages -# if packages: -# for pkg in packages: -# if pkg not in default_packages: -# lines.append(f"\\usepackage{{{pkg}}}") -# -# lines.append("") -# -# # Metadata -# if metadata.get("title"): -# title = _escape_latex(metadata["title"]) -# lines.append(f"\\title{{{title}}}") -# if metadata.get("author"): -# author = _escape_latex(metadata["author"]) -# lines.append(f"\\author{{{author}}}") -# -# lines.append("") -# -# # Additional preamble -# if preamble: -# lines.append(preamble) -# lines.append("") -# -# # Begin document -# lines.append("\\begin{document}") -# lines.append("") -# -# # Title -# if metadata.get("title"): -# lines.append("\\maketitle") -# lines.append("") -# -# # Track list state for proper itemize/enumerate environments -# in_list = False -# list_type = None -# -# # Process blocks -# for i, block in enumerate(blocks): -# btype = block.get("type") -# -# # Handle list transitions -# if btype == "list-item": -# item_list_type = block.get("list_type", "unordered") -# if not in_list: -# env = "enumerate" if item_list_type == "ordered" else "itemize" -# lines.append(f"\\begin{{{env}}}") -# in_list = True -# list_type = item_list_type -# elif in_list: -# # Close list environment -# env = "enumerate" if list_type == "ordered" else "itemize" -# lines.append(f"\\end{{{env}}}") -# lines.append("") -# in_list = False -# list_type = None -# -# block_latex = _convert_block_to_latex(block, image_map) -# if block_latex: -# lines.append(block_latex) -# -# # Close any open list -# if in_list: -# env = "enumerate" if list_type == "ordered" else "itemize" -# lines.append(f"\\end{{{env}}}") -# lines.append("") -# -# # References section -# if references: -# lines.append("") -# if use_bibtex: -# lines.append(f"\\bibliographystyle{{plain}}") -# lines.append(f"\\bibliography{{{output_stem}}}") -# else: -# lines.append("\\begin{thebibliography}{99}") -# for ref in references: -# ref_latex = _convert_reference_to_latex(ref) -# if ref_latex: -# lines.append(ref_latex) -# lines.append("\\end{thebibliography}") -# -# # End document -# lines.append("") -# lines.append("\\end{document}") -# -# return "\n".join(lines) -# -# -# def _convert_block_to_latex( -# block: Dict[str, Any], -# image_map: Optional[Dict[str, str]] = None, -# ) -> Optional[str]: -# """Convert a single block to LaTeX.""" -# if image_map is None: -# image_map = {} -# -# btype = block.get("type", "paragraph") -# text = block.get("text", "") -# -# if not text and btype not in ("table", "image", "caption", "equation"): -# return None -# -# if btype == "heading": -# return _convert_heading(block) -# elif btype == "paragraph": -# return _convert_paragraph(block) -# elif btype == "caption": -# return _convert_caption(block, image_map) -# elif btype == "table": -# return _convert_table(block) -# elif btype == "image": -# return _convert_image(block, image_map) -# elif btype == "list-item": -# return _convert_list_item(block) -# elif btype == "equation": -# return _convert_equation(block) -# elif btype == "reference-paragraph": -# # Skip - handled separately in references section -# return None -# else: -# # Default: treat as paragraph -# return _escape_latex(text) + "\n" -# -# -# def _convert_equation(block: Dict[str, Any]) -> str: -# """Convert an equation block to LaTeX.""" -# latex = block.get("latex", "") -# text = block.get("text", "") -# -# if latex: -# # Use the converted LaTeX from OMML -# return f"\\begin{{equation}}\n{latex}\n\\end{{equation}}\n" -# elif text: -# # Fallback: wrap text in equation environment -# return f"\\begin{{equation}}\n{_escape_latex(text)}\n\\end{{equation}}\n" -# return "" -# -# -# def _convert_heading(block: Dict[str, Any]) -> str: -# """Convert a heading block to LaTeX.""" -# level = block.get("level", 1) -# text = _escape_latex(block.get("text", "")) -# -# # Map heading levels to LaTeX commands -# level_commands = { -# 1: "section", -# 2: "subsection", -# 3: "subsubsection", -# 4: "paragraph", -# 5: "subparagraph", -# } -# -# command = level_commands.get(level, "paragraph") -# return f"\\{command}{{{text}}}\n" -# -# -# def _convert_paragraph(block: Dict[str, Any]) -> str: -# """Convert a paragraph block to LaTeX.""" -# runs = block.get("runs", []) -# -# if runs: -# # Build paragraph from formatted runs -# parts = [] -# for run in runs: -# run_text = _escape_latex(run.get("text", "")) -# if run.get("bold"): -# run_text = f"\\textbf{{{run_text}}}" -# if run.get("italic"): -# run_text = f"\\textit{{{run_text}}}" -# if run.get("underline"): -# run_text = f"\\underline{{{run_text}}}" -# parts.append(run_text) -# return "".join(parts) + "\n" -# else: -# return _escape_latex(block.get("text", "")) + "\n" -# -# -# def _convert_caption( -# block: Dict[str, Any], -# image_map: Optional[Dict[str, str]] = None, -# ) -> str: -# """Convert a caption block to LaTeX figure/table environment.""" -# if image_map is None: -# image_map = {} -# -# caption_type = block.get("caption_type", "") -# number = block.get("number", "") -# caption_text = _escape_latex(block.get("caption_text", block.get("text", ""))) -# image_hash = block.get("image_hash") -# -# if caption_type == "figure": -# # Check if we have an associated image -# image_path = None -# if image_hash and image_hash in image_map: -# image_path = image_map[image_hash] -# -# lines = [ -# "\\begin{figure}[htbp]", -# "\\centering", -# ] -# -# if image_path: -# # Remove extension for includegraphics -# image_path_no_ext = image_path.rsplit(".", 1)[0] if "." in image_path else image_path -# lines.append(f"\\includegraphics[width=0.8\\textwidth]{{{image_path_no_ext}}}") -# else: -# lines.append(f"% Image placeholder for Figure {number}") -# -# lines.extend([ -# f"\\caption{{{caption_text}}}", -# f"\\label{{fig:{number}}}", -# "\\end{figure}", -# "", -# ]) -# return "\n".join(lines) -# -# elif caption_type == "table": -# # Table captions - typically above the table -# return f"% Table {number}: {caption_text}\n" -# -# else: -# return f"% Caption: {caption_text}\n" -# -# -# def _convert_image( -# block: Dict[str, Any], -# image_map: Optional[Dict[str, str]] = None, -# ) -> str: -# """Convert an image block to LaTeX includegraphics.""" -# if image_map is None: -# image_map = {} -# -# image_hash = block.get("image_hash") or block.get("hash") -# width = block.get("width", "0.8\\textwidth") -# -# if image_hash and image_hash in image_map: -# image_path = image_map[image_hash] -# # Remove extension for includegraphics -# image_path_no_ext = image_path.rsplit(".", 1)[0] if "." in image_path else image_path -# -# lines = [ -# "\\begin{figure}[htbp]", -# "\\centering", -# f"\\includegraphics[width={width}]{{{image_path_no_ext}}}", -# "\\end{figure}", -# "", -# ] -# return "\n".join(lines) -# -# return "% Image placeholder\n" -# -# -# def _convert_table(block: Dict[str, Any]) -> str: -# """Convert a table block to LaTeX.""" -# rows = block.get("rows", []) -# if not rows: -# return "" -# -# num_cols = len(rows[0]) if rows else 0 -# col_spec = "|" + "c|" * num_cols -# -# lines = [ -# "\\begin{table}[htbp]", -# "\\centering", -# f"\\begin{{tabular}}{{{col_spec}}}", -# "\\hline", -# ] -# -# for i, row in enumerate(rows): -# escaped_cells = [_escape_latex(str(cell)) for cell in row] -# lines.append(" & ".join(escaped_cells) + " \\\\") -# lines.append("\\hline") -# -# lines.extend([ -# "\\end{tabular}", -# "\\end{table}", -# "", -# ]) -# -# return "\n".join(lines) -# -# -# def _convert_list_item(block: Dict[str, Any]) -> str: -# """Convert a list item to LaTeX.""" -# text = _escape_latex(block.get("text", "")) -# return f"\\item {text}\n" -# -# -# def _convert_reference_to_latex(ref: Dict[str, Any]) -> str: -# """Convert a reference entry to LaTeX bibitem.""" -# number = ref.get("number") -# text = _escape_latex(ref.get("text", ref.get("raw", ""))) -# -# if number: -# return f"\\bibitem{{ref{number}}} {text}" -# else: -# return f"\\bibitem{{}} {text}" -# -# -# def _escape_latex(text: str) -> str: -# """Escape special LaTeX characters.""" -# if not text: -# return "" -# -# # Characters that need escaping in LaTeX -# replacements = [ -# ("\\", "\\textbackslash{}"), -# ("&", "\\&"), -# ("%", "\\%"), -# ("$", "\\$"), -# ("#", "\\#"), -# ("_", "\\_"), -# ("{", "\\{"), -# ("}", "\\}"), -# ("~", "\\textasciitilde{}"), -# ("^", "\\textasciicircum{}"), -# ] -# -# # Apply replacements (order matters - backslash first) -# result = text -# for old, new in replacements: -# # Skip if already escaped -# if old == "\\": -# # Don't escape existing LaTeX commands -# result = re.sub(r'(? CompileResult: -# """ -# Compile a LaTeX file to PDF. -# -# Parameters -# ---------- -# tex_path : str | Path -# Path to the .tex file. -# output_dir : str | Path | None -# Output directory for PDF. If None, uses same directory as tex file. -# compiler : str -# LaTeX compiler to use: "pdflatex", "xelatex", "lualatex", or "latexmk". -# Default is "pdflatex". -# runs : int -# Number of compilation passes (for references/ToC). Default is 2. -# Ignored if compiler is "latexmk". -# clean : bool -# Remove auxiliary files (.aux, .log, .out, etc.) after compilation. -# Default is True. -# timeout : int -# Timeout in seconds for each compilation pass. Default is 120. -# -# Returns -# ------- -# CompileResult -# Compilation result with success status, PDF path, and logs. -# -# Examples -# -------- -# >>> from scitex.tex import compile_tex -# >>> result = compile_tex("manuscript.tex") -# >>> if result.success: -# ... print(f"PDF created: {result.pdf_path}") -# ... else: -# ... print(f"Errors: {result.errors}") -# -# >>> # Use latexmk for automatic multi-pass compilation -# >>> result = compile_tex("manuscript.tex", compiler="latexmk") -# -# Notes -# ----- -# Requires LaTeX to be installed on the system (texlive, miktex, etc.). -# """ -# tex_path = Path(tex_path).absolute() -# -# if not tex_path.exists(): -# return CompileResult( -# success=False, -# pdf_path=None, -# exit_code=1, -# stdout="", -# stderr=f"File not found: {tex_path}", -# errors=[f"File not found: {tex_path}"], -# ) -# -# # Determine output directory -# if output_dir is None: -# output_dir = tex_path.parent -# else: -# output_dir = Path(output_dir).absolute() -# output_dir.mkdir(parents=True, exist_ok=True) -# -# # Check if compiler is available -# compiler_cmd = shutil.which(compiler) -# if compiler_cmd is None: -# return CompileResult( -# success=False, -# pdf_path=None, -# exit_code=127, -# stdout="", -# stderr=f"Compiler not found: {compiler}", -# errors=[f"Compiler not found: {compiler}. Install texlive or miktex."], -# ) -# -# # Build command -# if compiler == "latexmk": -# cmd = [ -# compiler, -# "-pdf", -# "-interaction=nonstopmode", -# f"-output-directory={output_dir}", -# str(tex_path), -# ] -# runs = 1 # latexmk handles multi-pass -# else: -# cmd = [ -# compiler, -# "-interaction=nonstopmode", -# "-halt-on-error", -# f"-output-directory={output_dir}", -# str(tex_path), -# ] -# -# # Run compilation -# stdout_all = [] -# stderr_all = [] -# exit_code = 0 -# -# for run_num in range(runs): -# try: -# result = subprocess.run( -# cmd, -# cwd=tex_path.parent, -# capture_output=True, -# text=True, -# timeout=timeout, -# ) -# stdout_all.append(f"=== Pass {run_num + 1} ===\n{result.stdout}") -# stderr_all.append(result.stderr) -# exit_code = result.returncode -# -# # If compilation failed, don't continue -# if exit_code != 0: -# break -# -# except subprocess.TimeoutExpired: -# return CompileResult( -# success=False, -# pdf_path=None, -# exit_code=124, -# stdout="\n".join(stdout_all), -# stderr=f"Compilation timed out after {timeout} seconds", -# errors=[f"Compilation timed out after {timeout} seconds"], -# ) -# except Exception as e: -# return CompileResult( -# success=False, -# pdf_path=None, -# exit_code=1, -# stdout="\n".join(stdout_all), -# stderr=str(e), -# errors=[str(e)], -# ) -# -# # Check for output PDF -# pdf_name = tex_path.stem + ".pdf" -# pdf_path = output_dir / pdf_name -# -# # Read log file for detailed errors/warnings -# log_path = output_dir / (tex_path.stem + ".log") -# log_content = "" -# errors = [] -# warnings = [] -# -# if log_path.exists(): -# try: -# log_content = log_path.read_text(encoding="utf-8", errors="replace") -# errors, warnings = _parse_latex_log(log_content) -# except Exception: -# pass -# -# # Clean auxiliary files -# if clean: -# aux_extensions = [".aux", ".log", ".out", ".toc", ".lof", ".lot", -# ".bbl", ".blg", ".fls", ".fdb_latexmk", ".synctex.gz"] -# for ext in aux_extensions: -# aux_file = output_dir / (tex_path.stem + ext) -# if aux_file.exists(): -# try: -# aux_file.unlink() -# except Exception: -# pass -# -# success = exit_code == 0 and pdf_path.exists() -# -# return CompileResult( -# success=success, -# pdf_path=pdf_path if pdf_path.exists() else None, -# exit_code=exit_code, -# stdout="\n".join(stdout_all), -# stderr="\n".join(stderr_all), -# log_content=log_content, -# errors=errors, -# warnings=warnings, -# ) -# -# -# def _parse_latex_log(log_content: str) -> Tuple[List[str], List[str]]: -# """Parse LaTeX log file for errors and warnings.""" -# errors = [] -# warnings = [] -# -# lines = log_content.split("\n") -# -# for i, line in enumerate(lines): -# # Error patterns -# if line.startswith("!"): -# # Collect multi-line error message -# error_lines = [line] -# for j in range(i + 1, min(i + 5, len(lines))): -# if lines[j].startswith("l.") or lines[j].strip() == "": -# break -# error_lines.append(lines[j]) -# errors.append(" ".join(error_lines)) -# -# elif "Error:" in line or "Fatal error" in line: -# errors.append(line.strip()) -# -# # Warning patterns -# elif "Warning:" in line: -# warnings.append(line.strip()) -# elif "Underfull" in line or "Overfull" in line: -# warnings.append(line.strip()) -# -# return errors, warnings -# -# -# __all__ = ["export_tex", "compile_tex", "CompileResult"] - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_export.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/tex/test__preview.py b/tests/scitex/tex/test__preview.py deleted file mode 100644 index b73aa260b..000000000 --- a/tests/scitex/tex/test__preview.py +++ /dev/null @@ -1,627 +0,0 @@ -#!/usr/bin/env python3 -# Time-stamp: "2026-01-05 14:00:00 (ywatanabe)" -# File: ./tests/scitex/tex/test__preview.py - -"""Comprehensive tests for tex._preview module""" - -from unittest.mock import MagicMock, Mock, patch - -import numpy as np -import pytest - -# Required for scitex.tex module -matplotlib = pytest.importorskip("matplotlib") -matplotlib.use("Agg") # Use non-interactive backend for testing -from matplotlib.axes import Axes -from matplotlib.figure import Figure - -from scitex.tex._preview import FALLBACK_AVAILABLE - - -class TestPreviewWithoutFallback: - """Tests for preview function with enable_fallback=False.""" - - def test_preview_single_tex_string(self): - """Test preview with single LaTeX string.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - result = preview(["x^2"], enable_fallback=False) - - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=1, ncols=1, figsize=(10, 3)) - assert mock_ax.text.call_count == 2 - mock_ax.text.assert_any_call( - 0.5, 0.7, "x^2", size=20, ha="center", va="center" - ) - mock_ax.text.assert_any_call( - 0.5, 0.3, "$x^2$", size=20, ha="center", va="center" - ) - mock_ax.hide_spines.assert_called_once() - mock_fig.tight_layout.assert_called_once() - - def test_preview_multiple_tex_strings(self): - """Test preview with multiple LaTeX strings.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_axes = [Mock(spec=Axes) for _ in range(3)] - for ax in mock_axes: - ax.text = Mock() - ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_axes) - - tex_strings = ["x^2", r"\sum_{i=1}^n i", r"\frac{a}{b}"] - result = preview(tex_strings, enable_fallback=False) - - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=3, ncols=1, figsize=(10, 9)) - - for ax, tex_str in zip(mock_axes, tex_strings): - assert ax.text.call_count == 2 - ax.text.assert_any_call( - 0.5, 0.7, tex_str, size=20, ha="center", va="center" - ) - ax.text.assert_any_call( - 0.5, 0.3, f"${tex_str}$", size=20, ha="center", va="center" - ) - ax.hide_spines.assert_called_once() - - mock_fig.tight_layout.assert_called_once() - - def test_preview_empty_list(self): - """Test preview with empty list.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_subplots.return_value = (mock_fig, np.array([])) - mock_fig.tight_layout = Mock() - - result = preview([], enable_fallback=False) - - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=0, ncols=1, figsize=(10, 0)) - mock_fig.tight_layout.assert_called_once() - - def test_preview_complex_latex(self): - """Test preview with complex LaTeX expressions.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - complex_tex = r"\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}" - result = preview([complex_tex], enable_fallback=False) - - assert result == mock_fig - mock_ax.text.assert_any_call( - 0.5, 0.7, complex_tex, size=20, ha="center", va="center" - ) - mock_ax.text.assert_any_call( - 0.5, 0.3, f"${complex_tex}$", size=20, ha="center", va="center" - ) - - def test_preview_special_characters(self): - """Test preview with special LaTeX characters.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - special_tex = r"\alpha \beta \gamma \delta \epsilon" - result = preview([special_tex], enable_fallback=False) - - assert result == mock_fig - mock_ax.text.assert_any_call( - 0.5, 0.7, special_tex, size=20, ha="center", va="center" - ) - mock_ax.text.assert_any_call( - 0.5, 0.3, f"${special_tex}$", size=20, ha="center", va="center" - ) - - def test_preview_text_positioning(self): - """Test that text is positioned correctly.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - result = preview(["E=mc^2"], enable_fallback=False) - - calls = mock_ax.text.call_args_list - assert len(calls) == 2 - # First call: raw string at y=0.7 - assert calls[0][0] == (0.5, 0.7, "E=mc^2") - assert calls[0][1] == {"size": 20, "ha": "center", "va": "center"} - # Second call: LaTeX string at y=0.3 - assert calls[1][0] == (0.5, 0.3, "$E=mc^2$") - assert calls[1][1] == {"size": 20, "ha": "center", "va": "center"} - - def test_preview_figure_size_scaling(self): - """Test that figure size scales with number of strings.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_axes = [Mock(spec=Axes) for _ in range(5)] - for ax in mock_axes: - ax.text = Mock() - ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_axes) - - tex_strings = ["a", "b", "c", "d", "e"] - result = preview(tex_strings, enable_fallback=False) - - mock_subplots.assert_called_once_with(nrows=5, ncols=1, figsize=(10, 15)) - - def test_preview_matrix_latex(self): - """Test preview with matrix LaTeX notation.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - matrix_tex = r"\begin{pmatrix} a & b \\ c & d \end{pmatrix}" - result = preview([matrix_tex], enable_fallback=False) - - assert result == mock_fig - mock_ax.text.assert_any_call( - 0.5, 0.7, matrix_tex, size=20, ha="center", va="center" - ) - mock_ax.text.assert_any_call( - 0.5, 0.3, f"${matrix_tex}$", size=20, ha="center", va="center" - ) - - -class TestPreviewWithFallback: - """Tests for preview function with enable_fallback=True (default).""" - - def test_preview_with_fallback_enabled(self): - """Test preview with fallback enabled (default).""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - result = preview(["x^2"]) # default enable_fallback=True - - assert result == mock_fig - assert mock_ax.text.call_count == 2 - # When fallback is available, text may be converted - if FALLBACK_AVAILABLE: - # Verify text was called twice (raw and latex formatted) - calls = mock_ax.text.call_args_list - # First call is raw at y=0.7 - assert calls[0][0][0] == 0.5 - assert calls[0][0][1] == 0.7 - # Second call is latex at y=0.3 - assert calls[1][0][0] == 0.5 - assert calls[1][0][1] == 0.3 - - def test_preview_fallback_converts_text(self): - """Test that fallback converts superscripts to unicode.""" - from scitex.tex import preview - - if not FALLBACK_AVAILABLE: - pytest.skip("Fallback module not available") - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - result = preview(["x^2"]) - - calls = mock_ax.text.call_args_list - # First call (raw at y=0.7) should have unicode conversion - first_call_text = calls[0][0][2] - # Should contain unicode superscript 2 - assert "x" in first_call_text - # The second character should be superscript 2 (²) - assert "²" in first_call_text or "^" in first_call_text - - -class TestPreviewEdgeCases: - """Tests for edge cases in preview function.""" - - def test_preview_single_string_input(self): - """Test preview converts single string to list.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - # Single string should be converted to list - result = preview("x^2", enable_fallback=False) - - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=1, ncols=1, figsize=(10, 3)) - - def test_preview_with_numpy_array_axes(self): - """Test preview handles numpy array of axes correctly.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, np.array(mock_ax)) - - result = preview(["test"], enable_fallback=False) - - assert result == mock_fig - mock_ax.text.assert_any_call( - 0.5, 0.7, "test", size=20, ha="center", va="center" - ) - - def test_preview_unicode_strings(self): - """Test preview with Unicode strings.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - unicode_tex = "∑ᵢ₌₁ⁿ xᵢ²" - result = preview([unicode_tex], enable_fallback=False) - - assert result == mock_fig - mock_ax.text.assert_any_call( - 0.5, 0.7, unicode_tex, size=20, ha="center", va="center" - ) - mock_ax.text.assert_any_call( - 0.5, 0.3, f"${unicode_tex}$", size=20, ha="center", va="center" - ) - - def test_preview_already_wrapped_in_dollars(self): - """Test preview handles strings already wrapped in $ correctly.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - # Already wrapped in $ should not double wrap - result = preview(["$x^2$"], enable_fallback=False) - - assert result == mock_fig - calls = mock_ax.text.call_args_list - # Second call should use the string as-is (no double wrapping) - assert calls[1][0][2] == "$x^2$" - - -class TestPreviewErrorHandling: - """Tests for error handling in preview function.""" - - def test_preview_error_recovery(self): - """Test preview handles errors gracefully.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock(side_effect=Exception("Layout error")) - mock_subplots.return_value = (mock_fig, mock_ax) - - with pytest.raises(Exception, match="Layout error"): - preview(["test"], enable_fallback=False) - - def test_preview_none_input(self): - """Test preview with None input - wraps in list and proceeds.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - # None gets wrapped in list: [None], nrows=1 - result = preview(None, enable_fallback=False) - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=1, ncols=1, figsize=(10, 3)) - - def test_preview_int_input(self): - """Test preview with int input - wraps in list and proceeds.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_ax = Mock(spec=Axes) - mock_ax.text = Mock() - mock_ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_ax) - - # int gets wrapped in list: [123], nrows=1 - result = preview(123, enable_fallback=False) - assert result == mock_fig - mock_subplots.assert_called_once_with(nrows=1, ncols=1, figsize=(10, 3)) - - -class TestPreviewPerformance: - """Tests for preview performance.""" - - def test_preview_long_list_performance(self): - """Test preview performance with many LaTeX strings.""" - import time - - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_axes = [Mock(spec=Axes) for _ in range(100)] - for ax in mock_axes: - ax.text = Mock() - ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_axes) - - tex_strings = [f"x^{{{i}}}" for i in range(100)] - - start_time = time.time() - result = preview(tex_strings, enable_fallback=False) - elapsed = time.time() - start_time - - assert elapsed < 1.0 - assert result == mock_fig - - -class TestPreviewMixedContent: - """Tests for preview with mixed content types.""" - - def test_preview_mixed_content(self): - """Test preview with mixed LaTeX and plain text.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_axes = [Mock(spec=Axes) for _ in range(3)] - for ax in mock_axes: - ax.text = Mock() - ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_axes) - - mixed_content = ["Plain text", r"\LaTeX", "x + y = z"] - result = preview(mixed_content, enable_fallback=False) - - assert result == mock_fig - for ax, content in zip(mock_axes, mixed_content): - ax.text.assert_any_call( - 0.5, 0.7, content, size=20, ha="center", va="center" - ) - ax.text.assert_any_call( - 0.5, 0.3, f"${content}$", size=20, ha="center", va="center" - ) - - -class TestPreviewDocstrings: - """Tests for docstring examples.""" - - def test_preview_docstring_example(self): - """Test the example from the docstring works.""" - from scitex.tex import preview - - with patch("scitex.plt.subplots") as mock_subplots: - mock_fig = Mock(spec=Figure) - mock_axes = [Mock(spec=Axes) for _ in range(3)] - for ax in mock_axes: - ax.text = Mock() - ax.hide_spines = Mock() - mock_fig.tight_layout = Mock() - mock_subplots.return_value = (mock_fig, mock_axes) - - # Example from docstring - tex_strings = ["x^2", r"\sum_{i=1}^n i", r"\alpha + \beta"] - fig = preview(tex_strings, enable_fallback=False) - - assert fig == mock_fig - - # Verify strings were rendered - mock_axes[0].text.assert_any_call( - 0.5, 0.7, "x^2", size=20, ha="center", va="center" - ) - mock_axes[0].text.assert_any_call( - 0.5, 0.3, "$x^2$", size=20, ha="center", va="center" - ) - - -class TestFallbackAvailability: - """Tests for fallback availability.""" - - def test_fallback_available_is_bool(self): - """Test FALLBACK_AVAILABLE is a boolean.""" - assert isinstance(FALLBACK_AVAILABLE, bool) - - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_preview.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # Time-stamp: "2025-06-05 12:00:00 (ywatanabe)" -# # File: ./src/scitex/tex/_preview.py -# -# """ -# LaTeX preview functionality with fallback mechanisms. -# -# Functionality: -# - Generate previews of LaTeX strings with automatic fallback -# - Handle LaTeX rendering failures gracefully -# Input: -# List of LaTeX strings -# Output: -# Matplotlib figure with previews -# Prerequisites: -# matplotlib, numpy, scitex.plt, scitex.str._latex_fallback -# """ -# -# import numpy as np -# -# try: -# from scitex.str._latex_fallback import safe_latex_render, latex_fallback_decorator -# -# FALLBACK_AVAILABLE = True -# except ImportError: -# FALLBACK_AVAILABLE = False -# -# def latex_fallback_decorator(fallback_strategy="auto", preserve_math=True): -# def decorator(func): -# return func -# -# return decorator -# -# def safe_latex_render(text, fallback_strategy="auto", preserve_math=True): -# return text -# -# -# @latex_fallback_decorator(fallback_strategy="auto", preserve_math=True) -# def preview(tex_str_list, enable_fallback=True): -# r""" -# Generate a preview of LaTeX strings with automatic fallback. -# -# Parameters -# ---------- -# tex_str_list : list of str -# List of LaTeX strings to preview -# enable_fallback : bool, optional -# Whether to enable LaTeX fallback mechanisms, by default True -# -# Returns -# ------- -# matplotlib.figure.Figure -# Figure containing the previews -# -# Examples -# -------- -# >>> tex_strings = ["x^2", r"\sum_{i=1}^n i", r"\alpha + \beta"] -# >>> fig = preview(tex_strings) -# >>> scitex.plt.show() -# -# Notes -# ----- -# If LaTeX rendering fails, this function automatically falls back to -# mathtext or unicode alternatives while preserving the preview layout. -# """ -# from scitex.plt import subplots -# -# if not isinstance(tex_str_list, (list, tuple)): -# tex_str_list = [tex_str_list] -# -# fig, axes = subplots( -# nrows=len(tex_str_list), ncols=1, figsize=(10, 3 * len(tex_str_list)) -# ) -# axes = np.atleast_1d(axes) -# -# for ax, tex_string in zip(axes, tex_str_list): -# try: -# # Original LaTeX string (raw) -# if enable_fallback and FALLBACK_AVAILABLE: -# safe_raw = safe_latex_render(tex_string, "unicode", preserve_math=False) -# ax.text(0.5, 0.7, safe_raw, size=20, ha="center", va="center") -# else: -# ax.text(0.5, 0.7, tex_string, size=20, ha="center", va="center") -# -# # LaTeX-formatted string -# latex_formatted = ( -# f"${tex_string}$" -# if not (tex_string.startswith("$") and tex_string.endswith("$")) -# else tex_string -# ) -# -# if enable_fallback and FALLBACK_AVAILABLE: -# safe_latex = safe_latex_render(latex_formatted, preserve_math=True) -# ax.text(0.5, 0.3, safe_latex, size=20, ha="center", va="center") -# else: -# ax.text(0.5, 0.3, latex_formatted, size=20, ha="center", va="center") -# -# except Exception as e: -# # Fallback for individual preview failures -# ax.text(0.5, 0.7, f"Raw: {tex_string}", size=16, ha="center", va="center") -# ax.text( -# 0.5, -# 0.3, -# f"Error: {str(e)[:50]}...", -# size=12, -# ha="center", -# va="center", -# color="red", -# ) -# -# ax.hide_spines() -# -# fig.tight_layout() -# return fig -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_preview.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/tex/test__to_vec.py b/tests/scitex/tex/test__to_vec.py deleted file mode 100644 index 365cf8668..000000000 --- a/tests/scitex/tex/test__to_vec.py +++ /dev/null @@ -1,401 +0,0 @@ -#!/usr/bin/env python3 -# Time-stamp: "2026-01-05 14:00:00 (ywatanabe)" -# File: ./tests/scitex/tex/test__to_vec.py - -"""Tests for to_vec function that converts strings to LaTeX vector notation.""" - -import pytest - -from scitex.tex import to_vec -from scitex.tex._to_vec import FALLBACK_AVAILABLE, safe_to_vec, vector_notation - - -class TestToVecWithoutFallback: - """Tests for to_vec with enable_fallback=False (raw LaTeX output).""" - - def test_basic_vector(self): - """Test basic vector conversion returns raw LaTeX.""" - result = to_vec("AB", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_single_character(self): - """Test with single character vector.""" - result = to_vec("v", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{v}}" - - result = to_vec("x", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{x}}" - - def test_numeric_string(self): - """Test with numeric strings.""" - result = to_vec("12", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{12}}" - - result = to_vec("0", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{0}}" - - def test_empty_string(self): - """Test with empty string returns empty string.""" - result = to_vec("", enable_fallback=False) - assert result == "" - - def test_special_characters(self): - """Test with special characters that are valid in LaTeX.""" - result = to_vec("v_1", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{v_1}}" - - result = to_vec("PQ", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{PQ}}" - - result = to_vec("A'", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{A'}}" - - def test_long_string(self): - """Test with longer vector names.""" - result = to_vec("velocity", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{velocity}}" - - result = to_vec("F_net", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{F_net}}" - - def test_unicode(self): - """Test with unicode characters.""" - result = to_vec("αβ", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{αβ}}" - - result = to_vec("∇φ", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{∇φ}}" - - def test_spaces(self): - """Test with strings containing spaces.""" - result = to_vec("A B", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{A B}}" - - result = to_vec(" CD ", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{ CD }}" - - def test_latex_special_chars(self): - """Test with characters that have special meaning in LaTeX.""" - result = to_vec("$x$", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{$x$}}" - - result = to_vec("a&b", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{a&b}}" - - result = to_vec("x^2", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{x^2}}" - - def test_braces(self): - """Test with braces in input.""" - result = to_vec("{AB}", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{{AB}}}" - - result = to_vec("a{b}c", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{a{b}c}}" - - def test_escape_sequences(self): - """Test with escape sequences.""" - result = to_vec(r"\vec", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{\vec}}" - - result = to_vec("A\nB", enable_fallback=False) - assert result == "\\overrightarrow{\\mathrm{A\nB}}" - - def test_mathematical_notation(self): - """Test with common mathematical vector notations.""" - result = to_vec("r", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{r}}" - - result = to_vec("F", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{F}}" - - result = to_vec("r_0", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{r_0}}" - - def test_raw_string_output(self): - """Test that output is valid raw string for LaTeX.""" - result = to_vec("PQ", enable_fallback=False) - assert r"\overrightarrow" in result - assert r"\mathrm" in result - assert "{PQ}" in result - - def test_edge_cases(self): - """Test edge cases and boundary conditions.""" - long_str = "A" * 100 - result = to_vec(long_str, enable_fallback=False) - assert result == rf"\overrightarrow{{\mathrm{{{long_str}}}}}" - - result = to_vec("_-_-_", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{_-_-_}}" - - result = to_vec("123456", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{123456}}" - - -class TestToVecWithFallback: - """Tests for to_vec with enable_fallback=True (default behavior).""" - - def test_basic_vector_fallback(self): - """Test basic vector with fallback enabled returns unicode.""" - result = to_vec("AB") # default enable_fallback=True - # When fallback is available, returns unicode combining arrow - if FALLBACK_AVAILABLE: - # Could be mathtext processed or unicode fallback - assert "AB" in result or "⃗" in result - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_empty_string_fallback(self): - """Test empty string returns empty with fallback enabled.""" - result = to_vec("") - assert result == "" - - def test_fallback_preserves_content(self): - """Test that the vector content is preserved in fallback output.""" - result = to_vec("XYZ") - assert "X" in result or "XYZ" in result - - def test_unicode_fallback_strategy(self): - """Test explicit unicode fallback strategy.""" - result = to_vec("AB", fallback_strategy="unicode") - if FALLBACK_AVAILABLE: - assert result == "AB⃗" - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_plain_fallback_strategy(self): - """Test explicit plain fallback strategy.""" - result = to_vec("AB", fallback_strategy="plain") - if FALLBACK_AVAILABLE: - assert result == "vec(AB)" - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - -class TestToVecConsistency: - """Tests for consistent behavior of to_vec.""" - - def test_repeated_calls_consistent(self): - """Test that repeated calls produce consistent results.""" - input_str = "XY" - result1 = to_vec(input_str, enable_fallback=False) - result2 = to_vec(input_str, enable_fallback=False) - assert result1 == result2 == r"\overrightarrow{\mathrm{XY}}" - - def test_type_preservation(self): - """Test that function returns string type.""" - result = to_vec("AB", enable_fallback=False) - assert isinstance(result, str) - - result = to_vec("") - assert isinstance(result, str) - - result = to_vec("test") - assert isinstance(result, str) - - def test_practical_usage(self): - """Test practical usage in LaTeX documents.""" - vec_ab = to_vec("AB", enable_fallback=False) - vec_bc = to_vec("BC", enable_fallback=False) - - assert vec_ab.startswith(r"\overrightarrow") - assert vec_bc.startswith(r"\overrightarrow") - - latex_expr = f"{vec_ab} + {vec_bc}" - assert r"\overrightarrow{\mathrm{AB}}" in latex_expr - assert r"\overrightarrow{\mathrm{BC}}" in latex_expr - - -class TestSafeToVec: - """Tests for safe_to_vec function.""" - - def test_safe_to_vec_basic(self): - """Test safe_to_vec with basic input.""" - result = safe_to_vec("AB") - # safe_to_vec always has enable_fallback=True - if FALLBACK_AVAILABLE: - assert "AB" in result or "⃗" in result - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_safe_to_vec_unicode_strategy(self): - """Test safe_to_vec with unicode strategy.""" - result = safe_to_vec("AB", fallback_strategy="unicode") - if FALLBACK_AVAILABLE: - assert result == "AB⃗" - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_safe_to_vec_plain_strategy(self): - """Test safe_to_vec with plain strategy.""" - result = safe_to_vec("AB", fallback_strategy="plain") - if FALLBACK_AVAILABLE: - assert result == "vec(AB)" - else: - assert result == r"\overrightarrow{\mathrm{AB}}" - - def test_safe_to_vec_empty(self): - """Test safe_to_vec with empty string.""" - result = safe_to_vec("") - assert result == "" - - -class TestVectorNotationAlias: - """Tests for vector_notation alias.""" - - def test_vector_notation_is_to_vec(self): - """Test that vector_notation is alias for to_vec.""" - assert vector_notation is to_vec - - def test_vector_notation_works(self): - """Test that vector_notation produces same results.""" - result = vector_notation("AB", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{AB}}" - - -class TestFallbackAvailability: - """Tests for fallback availability handling.""" - - def test_fallback_available_flag(self): - """Test FALLBACK_AVAILABLE is a boolean.""" - assert isinstance(FALLBACK_AVAILABLE, bool) - - def test_behavior_without_fallback_module(self): - """Test behavior when fallback is disabled.""" - result = to_vec("test", enable_fallback=False) - assert result == r"\overrightarrow{\mathrm{test}}" - - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_to_vec.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # Time-stamp: "2025-06-05 12:00:00 (ywatanabe)" -# # File: ./src/scitex/tex/_to_vec.py -# -# """ -# LaTeX vector notation with fallback mechanisms. -# -# Functionality: -# - Convert strings to LaTeX vector notation with automatic fallback -# - Handle LaTeX rendering failures gracefully -# Input: -# String representation of vector -# Output: -# LaTeX vector notation with fallback support -# Prerequisites: -# scitex.str._latex_fallback -# """ -# -# try: -# from scitex.str._latex_fallback import safe_latex_render, latex_fallback_decorator -# -# FALLBACK_AVAILABLE = True -# except ImportError: -# FALLBACK_AVAILABLE = False -# -# def latex_fallback_decorator(fallback_strategy="auto", preserve_math=True): -# def decorator(func): -# return func -# -# return decorator -# -# def safe_latex_render(text, fallback_strategy="auto", preserve_math=True): -# return text -# -# -# @latex_fallback_decorator(fallback_strategy="auto", preserve_math=True) -# def to_vec(v_str, enable_fallback=True, fallback_strategy="auto"): -# r""" -# Convert a string to LaTeX vector notation with automatic fallback. -# -# Parameters -# ---------- -# v_str : str -# String representation of the vector -# enable_fallback : bool, optional -# Whether to enable LaTeX fallback mechanisms, by default True -# fallback_strategy : str, optional -# Fallback strategy: "auto", "mathtext", "unicode", "plain", by default "auto" -# -# Returns -# ------- -# str -# LaTeX representation of the vector with automatic fallback -# -# Examples -# -------- -# >>> vector = to_vec("AB") -# >>> print(vector) # LaTeX: \overrightarrow{\mathrm{AB}} -# -# >>> vector = to_vec("AB") # Falls back to unicode if LaTeX fails -# >>> print(vector) # Unicode: A⃗B or AB⃗ -# -# Notes -# ----- -# If LaTeX rendering fails, this function automatically falls back to: -# - mathtext: Uses matplotlib's built-in math rendering -# - unicode: Uses Unicode vector symbols (⃗) -# - plain: Returns plain text with "vec()" notation -# """ -# if not v_str: -# return "" -# -# # Create LaTeX vector notation -# latex_vector = f"\\overrightarrow{{\\mathrm{{{v_str}}}}}" -# -# if enable_fallback and FALLBACK_AVAILABLE: -# # Custom fallback handling for vectors -# if fallback_strategy == "auto": -# # Try mathtext first, then unicode -# try: -# mathtext_result = safe_latex_render(f"${latex_vector}$", "mathtext") -# return mathtext_result -# except Exception: -# # Fall back to unicode vector notation -# return f"{v_str}⃗" # Unicode combining right arrow above -# elif fallback_strategy == "unicode": -# return f"{v_str}⃗" # Unicode combining right arrow above -# elif fallback_strategy == "plain": -# return f"vec({v_str})" -# else: -# return safe_latex_render(f"${latex_vector}$", fallback_strategy) -# else: -# return latex_vector -# -# -# def safe_to_vec(v_str, fallback_strategy="auto"): -# """ -# Safe version of to_vec with explicit fallback control. -# -# Parameters -# ---------- -# v_str : str -# String representation of the vector -# fallback_strategy : str, optional -# Explicit fallback strategy: "auto", "mathtext", "unicode", "plain" -# -# Returns -# ------- -# str -# Vector notation with specified fallback behavior -# """ -# return to_vec(v_str, enable_fallback=True, fallback_strategy=fallback_strategy) -# -# -# # Backward compatibility -# vector_notation = to_vec -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/tex/_to_vec.py -# --------------------------------------------------------------------------------