diff --git a/opensiddur/common/xslt.py b/opensiddur/common/xslt.py index 3ebf276..9d8a3a8 100644 --- a/opensiddur/common/xslt.py +++ b/opensiddur/common/xslt.py @@ -85,7 +85,7 @@ def xslt_transform( raise -def main(): +def main(): # pragma: no cover parser = ArgumentParser() parser.add_argument("-o", "--output", type=Path, required=False, default=None) parser.add_argument("transform_file", type=Path) @@ -96,5 +96,5 @@ def main(): xslt_transform(args.transform_file, args.input_file, args.output) -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() \ No newline at end of file diff --git a/opensiddur/exporter/compiler.py b/opensiddur/exporter/compiler.py index e57edb2..5a394c7 100644 --- a/opensiddur/exporter/compiler.py +++ b/opensiddur/exporter/compiler.py @@ -1006,5 +1006,5 @@ def main(): # pragma: no cover encoding='utf-8') -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover main() \ No newline at end of file diff --git a/opensiddur/exporter/pdf/pdf.py b/opensiddur/exporter/pdf/pdf.py index f6a9e80..bffcbd5 100755 --- a/opensiddur/exporter/pdf/pdf.py +++ b/opensiddur/exporter/pdf/pdf.py @@ -199,7 +199,7 @@ def export_to_pdf(input_file, output_pdf): return True -def main(): +def main(): # pragma: no cover """Main function to handle command line arguments and run the PDF generation.""" parser = argparse.ArgumentParser( description="Convert JLPTEI XML files to PDF format", diff --git a/opensiddur/exporter/refdb.py b/opensiddur/exporter/refdb.py index 11a0ee8..03311e3 100644 --- a/opensiddur/exporter/refdb.py +++ b/opensiddur/exporter/refdb.py @@ -615,7 +615,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self.close() -def main(): +def main(): # pragma: no cover """Synchronize the reference database with the project directory. Opens the default database and syncs all projects, printing a summary @@ -669,5 +669,5 @@ def main(): raise -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover main() diff --git a/opensiddur/exporter/tex/xelatex.py b/opensiddur/exporter/tex/xelatex.py index 7a373b0..4c4a01e 100644 --- a/opensiddur/exporter/tex/xelatex.py +++ b/opensiddur/exporter/tex/xelatex.py @@ -235,10 +235,9 @@ def extract_sources(xml_file_paths: list[Path]) -> tuple[str, str]: tuple: (preamble_tex, postamble_tex) for the bibliography """ index_files = set(get_project_index(fp) for fp in xml_file_paths) + bibtex_records_all = [] + unique_bibtex_records = set() for index_xml in index_files: - bibtex_records_all = [] - unique_bibtex_records = set() - try: # Convert the index xml to .bib using bibtex.xslt index_xml_text = index_xml.read_text(encoding="utf-8") @@ -251,24 +250,24 @@ def extract_sources(xml_file_paths: list[Path]) -> tuple[str, str]: except Exception as e: print(f"Could not extract bibtex from {index_xml}: {e}", file=sys.stderr) continue - bibtex_blob = "\n\n".join(bibtex_records_all) - preamble_tex = "" - postamble_tex = "" - if bibtex_blob: - preamble_tex = f"""\\begin{{filecontents*}}{{job.bib}} + bibtex_blob = "\n\n".join(bibtex_records_all) + preamble_tex = "" + postamble_tex = "" + if bibtex_blob: + preamble_tex = f"""\\begin{{filecontents*}}{{job.bib}} {bibtex_blob} \\end{{filecontents*}} \\addbibresource{{job.bib}} """ - postamble_tex = f""" + postamble_tex = f""" \\begingroup \\renewcommand{{\\refname}}{{Sources}} \\nocite{{*}} \\printbibliography \\endgroup """ - return preamble_tex, postamble_tex - return "", "" + return preamble_tex, postamble_tex + def get_file_references(input_file: Path, project_directory: Path = projects_source_root) -> list[Path]: """ @@ -346,12 +345,14 @@ def transform_xml_to_tex(input_file, xslt_file=XSLT_FILE, output_file=None): else: sys.stdout.write(result) + return result + except Exception as e: print(f"Transformation error: {e}", file=sys.stderr) sys.exit(1) -def main(): +def main(): # pragma: no cover """Main function to handle command line arguments and run the transformation.""" parser = argparse.ArgumentParser( description="Convert JLPTEI XML files to XeLaTeX format", diff --git a/opensiddur/tests/exporter/test_xelatex.py b/opensiddur/tests/exporter/test_xelatex.py index 27d8c1f..968e782 100644 --- a/opensiddur/tests/exporter/test_xelatex.py +++ b/opensiddur/tests/exporter/test_xelatex.py @@ -7,6 +7,9 @@ import tempfile from pathlib import Path from lxml import etree +from io import StringIO +from unittest.mock import patch, MagicMock +import sys from opensiddur.exporter.tex.xelatex import ( extract_licenses, @@ -16,6 +19,8 @@ group_credits, credits_to_tex, get_file_references, + extract_sources, + transform_xml_to_tex, LicenseRecord, CreditRecord, ) @@ -605,6 +610,946 @@ def test_get_file_references_nested_transclusions(self): self.assertIn(self.project_dir / "project3" / "file3.xml", result) +class TestExtractSources(unittest.TestCase): + """Test source extraction from index.xml files.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.test_dir = Path(self.temp_dir.name) / "project" + self.test_dir.mkdir(parents=True) + + def _create_xml_file(self, project: str, filename: str, content: bytes) -> Path: + """Helper to create an XML file in a project subdirectory.""" + project_dir = self.test_dir / project + project_dir.mkdir(parents=True, exist_ok=True) + file_path = project_dir / filename + file_path.write_bytes(content) + return file_path + + def test_extract_sources_with_valid_index(self): + """Test extracting sources from a valid index.xml file with bibl elements.""" + index_content = b''' + + + + Test Book + Test Author + 2023 + + +''' + + # Create a file in project1 + file1 = self._create_xml_file("project1", "doc1.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", index_content) + + result = extract_sources([file1]) + + preamble, postamble = result + self.assertIn(r'\begin{filecontents*}{job.bib}', preamble) + self.assertIn(r'\addbibresource{job.bib}', preamble) + self.assertIn(r'\printbibliography', postamble) + self.assertIn(r'\renewcommand{\refname}{Sources}', postamble) + # Should contain BibTeX entry with the actual source information + self.assertIn('@', preamble) + self.assertIn('Test Book', preamble) + self.assertIn('Test Author', preamble) + self.assertIn('2023', preamble) + + def test_extract_sources_no_bibl_elements(self): + """Test handling of index.xml with no bibl elements.""" + index_content = b''' + + No bibliography +''' + + file1 = self._create_xml_file("project1", "doc1.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", index_content) + + preamble, postamble = extract_sources([file1]) + + # Should return empty strings when no bibliography + self.assertEqual(preamble, "") + self.assertEqual(postamble, "") + + def test_extract_sources_missing_index_file(self): + """Test handling of missing index.xml file (graceful skipping).""" + file1 = self._create_xml_file("project1", "doc1.xml", b"") + # Don't create index.xml + + # Should not raise exception + preamble, postamble = extract_sources([file1]) + + # Should return empty strings + self.assertEqual(preamble, "") + self.assertEqual(postamble, "") + + def test_extract_sources_invalid_xml(self): + """Test handling of invalid XML in index file.""" + file1 = self._create_xml_file("project1", "doc1.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", b"not valid xml <") + + # Should not raise exception, should skip gracefully + preamble, postamble = extract_sources([file1]) + + self.assertEqual(preamble, "") + self.assertEqual(postamble, "") + + def test_extract_sources_multiple_projects(self): + """Test extracting sources from multiple projects with index files.""" + index1_content = b''' + + + + Book 1 + Author 1 + + +''' + + index2_content = b''' + + + + Book 2 + Author 2 + + +''' + + file1 = self._create_xml_file("project1", "doc1.xml", b"") + file2 = self._create_xml_file("project2", "doc2.xml", b"") + index1 = self._create_xml_file("project1", "index.xml", index1_content) + index2 = self._create_xml_file("project2", "index.xml", index2_content) + + result = extract_sources([file1, file2]) + + preamble, postamble = result + # Should contain bibliography entries from both projects + self.assertIn(r'\begin{filecontents*}{job.bib}', preamble) + # Should contain entries from both index files + self.assertIn('Book 1', preamble) + self.assertIn('Author 1', preamble) + self.assertIn('Book 2', preamble) + self.assertIn('Author 2', preamble) + # Should have exactly 2 BibTeX entries (one from each project) + bibtex_count = preamble.count('@') + self.assertEqual(bibtex_count, 2, + f"Expected exactly 2 BibTeX entries (one per project), but found {bibtex_count}") + + def test_extract_sources_deduplicates_index_files(self): + """Test that same index.xml is only processed once.""" + index_content = b''' + + + + Test Book + Test Author + + +''' + + # Multiple files from same project should reference same index + file1 = self._create_xml_file("project1", "doc1.xml", b"") + file2 = self._create_xml_file("project1", "doc2.xml", b"") + file3 = self._create_xml_file("project1", "doc3.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", index_content) + + result = extract_sources([file1, file2, file3]) + + preamble, postamble = result + # Should have bibliography + self.assertIn(r'\begin{filecontents*}{job.bib}', preamble) + # BibTeX should appear exactly once (deduplicated by set) + # Count '@' symbols which appear at the start of each BibTeX entry + bibtex_count = preamble.count('@') + # Should have exactly one entry since all files reference the same index + self.assertEqual(bibtex_count, 1, + f"Expected exactly 1 BibTeX entry, but found {bibtex_count}") + + +class TestTransformXmlToTex(unittest.TestCase): + """Test the main transform_xml_to_tex function.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.test_dir = Path(self.temp_dir.name) / "project" + self.test_dir.mkdir(parents=True) + + def _create_xml_file(self, project: str, filename: str, content: bytes) -> Path: + """Helper to create an XML file in a project subdirectory.""" + project_dir = self.test_dir / project + project_dir.mkdir(parents=True, exist_ok=True) + file_path = project_dir / filename + file_path.write_bytes(content) + return file_path + + def test_transform_xml_to_tex_basic(self): + """Test basic XML to LaTeX transformation.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Hello World + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should produce LaTeX output + self.assertIsInstance(result, str) + self.assertIn(r'\documentclass{book}', result) + self.assertIn(r'\begin{document}', result) + self.assertIn('Hello World', result) + self.assertIn(r'\end{document}', result) + + def test_transform_xml_to_tex_with_output_file(self): + """Test transformation with output file specified.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Test content + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + output_file = Path(self.temp_dir.name) / "output.tex" + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + with patch('sys.stdout'): + transform_xml_to_tex(input_file, output_file=str(output_file)) + + # Check that output file was created + self.assertTrue(output_file.exists()) + content = output_file.read_text(encoding='utf-8') + self.assertIn(r'\documentclass{book}', content) + self.assertIn('Test content', content) + + def test_transform_xml_to_tex_integrates_licenses(self): + """Test that transform integrates license extraction.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + + + Test License + + + + + + + Content + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should include license section in postamble + self.assertIn(r'\chapter{Legal}', result) + self.assertIn('Test License', result) + + def test_transform_xml_to_tex_integrates_credits(self): + """Test that transform integrates credit extraction.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + + + Author + Author Name + + + + + + + Content + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should include credits section in postamble + self.assertIn(r'\chapter{Contributor credits}', result) + self.assertIn('Author Name', result) + + def test_transform_xml_to_tex_integrates_sources(self): + """Test that transform integrates source extraction.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Content + + +''' + + index_content = b''' + + + + Source Book + Source Author + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + index_file = self._create_xml_file("project1", "index.xml", index_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should include bibliography in preamble and postamble + self.assertIn(r'\addbibresource{job.bib}', result) + self.assertIn(r'\printbibliography', result) + + def test_transform_xml_to_tex_handles_invalid_xml(self): + """Test error handling for invalid XML.""" + from unittest.mock import patch, Mock + import opensiddur.exporter.tex.xelatex as xelatex_module + + input_file = self._create_xml_file("project1", "invalid.xml", b"not valid xml <") + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + mock_exit = Mock() + with patch('sys.exit', mock_exit): + # The function should catch the exception and call sys.exit(1) + transform_xml_to_tex(input_file) + # Verify that sys.exit was called with exit code 1 + mock_exit.assert_called_once_with(1) + + def test_transform_xml_to_tex_with_stdout(self): + """Test transformation output to stdout when output_file is None.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Content + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + mock_stdout = StringIO() + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + with patch('sys.stdout', mock_stdout): + transform_xml_to_tex(input_file, output_file=None) + output = mock_stdout.getvalue() + + # Should have written to stdout + self.assertIn(r'\documentclass{book}', output) + self.assertIn('Content', output) + + +class TestXSLTTransformation(unittest.TestCase): + """Test XSLT transformation directly.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.xslt_file = Path(__file__).parent.parent.parent / "exporter" / "tex" / "xelatex.xslt" + + def test_xslt_tei_div(self): + """Test div element conversion.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + Chapter Title + Content + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\part{Chapter Title}', result) + self.assertIn('Content', result) + + def test_xslt_tei_p(self): + """Test paragraph element conversion.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + Paragraph text + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn('Paragraph text', result) + + def test_xslt_tei_milestone_chapter(self): + """Test milestone with chapter unit.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + Text + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\chapter{1}', result) + + def test_xslt_tei_milestone_verse(self): + """Test milestone with verse unit.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + Text + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\textsuperscript{5}', result) + + def test_xslt_tei_choice(self): + """Test choice element (kri/ktiv).""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + + read + written + + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\textit{read}', result) + self.assertIn('(written)', result) + + def test_xslt_tei_emph(self): + """Test emphasis element.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + emphasized + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\emph{emphasized}', result) + + def test_xslt_rend_italic(self): + """Test rend attribute with italic value.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + italic text + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\textit{italic text}', result) + + def test_xslt_rend_small_caps(self): + """Test rend attribute with small-caps value.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + small caps + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\textsc{small caps}', result) + + def test_xslt_rend_superscript(self): + """Test rend attribute with superscript value.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + superscript + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\textsuperscript{superscript}', result) + + def test_xslt_rend_align_right(self): + """Test rend attribute with align-right value.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + right aligned + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\begin{flushright}', result) + self.assertIn(r'\end{flushright}', result) + + def test_xslt_hebrew_language_inline(self): + """Test Hebrew language handling for inline text.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + עברית + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\texthebrew{', result) + self.assertIn('עברית', result) + + def test_xslt_hebrew_language_block(self): + """Test Hebrew language handling for block elements.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + עברית + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\begin{hebrew}', result) + self.assertIn(r'\end{hebrew}', result) + + def test_xslt_tei_foreign(self): + """Test foreign text element.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + עברית + Latin + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\texthebrew{עברית}', result) + self.assertIn(r'\textit{Latin}', result) + + def test_xslt_tei_note(self): + """Test note element conversion to footnote.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + TextNote content + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\footnote{Note content}', result) + + def test_xslt_tei_lb(self): + """Test line break element.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + Line 1Line 2 + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\\', result) + + def test_xslt_tei_pb(self): + """Test page break element (should be skipped).""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + Text + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + # Should not contain pb-related content + self.assertIn('Text', result) + + def test_xslt_tei_lg_l(self): + """Test line group and line elements (poetry).""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + + Line 1 + Line 2 + + + +''' + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={"additional-preamble": "", "additional-postamble": ""}) + + self.assertIn(r'\begin{verse}', result) + self.assertIn(r'\end{verse}', result) + self.assertIn('Line 1', result) + self.assertIn('Line 2', result) + + def test_xslt_additional_preamble_postamble(self): + """Test that additional-preamble and additional-postamble parameters work.""" + from opensiddur.common.xslt import xslt_transform_string + + xml_content = ''' + + + + Content + + +''' + + preamble = "\\usepackage{testpackage}\n" + postamble = "\\chapter{Appendix}\nAppendix content\n" + + result = xslt_transform_string(self.xslt_file, xml_content, + xslt_params={ + "additional-preamble": preamble, + "additional-postamble": postamble + }) + + self.assertIn(preamble, result) + self.assertIn(postamble, result) + + +class TestEdgeCases(unittest.TestCase): + """Test edge cases and error handling.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.test_dir = Path(self.temp_dir.name) / "project" + self.test_dir.mkdir(parents=True) + + def _create_xml_file(self, project: str, filename: str, content: bytes) -> Path: + """Helper to create an XML file.""" + project_dir = self.test_dir / project + project_dir.mkdir(parents=True, exist_ok=True) + file_path = project_dir / filename + file_path.write_bytes(content) + return file_path + + def test_extract_sources_empty_xml(self): + """Test extract_sources with empty XML file.""" + file1 = self._create_xml_file("project1", "doc1.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", b"") + + preamble, postamble = extract_sources([file1]) + + self.assertEqual(preamble, "") + self.assertEqual(postamble, "") + + def test_transform_xml_to_tex_minimal_structure(self): + """Test transform with minimal valid XML structure.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should still produce valid LaTeX structure + self.assertIn(r'\documentclass{book}', result) + self.assertIn(r'\begin{document}', result) + self.assertIn(r'\end{document}', result) + + def test_transform_xml_to_tex_special_characters(self): + """Test transform with special characters that need LaTeX escaping.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Text with $special & characters + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should handle special characters (XSLT will pass through) + self.assertIn('Text with', result) + + def test_extract_sources_files_outside_project(self): + """Test extract_sources with files outside project directory.""" + # Create a file outside the project structure + outside_file = Path(self.temp_dir.name) / "outside.xml" + outside_file.write_bytes(b"") + + # Should not crash, but skip the file gracefully + # Note: extract_sources expects files in project directories, + # so outside files will be skipped + preamble, postamble = extract_sources([outside_file]) + + self.assertEqual(preamble, "") + self.assertEqual(postamble, "") + + def test_transform_xml_to_tex_complex_nested_structure(self): + """Test transform with complex nested divs.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + + Section 1 + + Subsection + Nested content + + + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should handle nested structures + self.assertIn(r'\part{Section 1}', result) + self.assertIn('Nested content', result) + + def test_transform_xml_to_tex_mixed_languages(self): + """Test transform with mixed English and Hebrew content.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = ''' + + + + English text עברית more English + + +'''.encode('utf-8') + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should handle mixed languages + self.assertIn('English text', result) + self.assertIn(r'\texthebrew{', result) + self.assertIn('עברית', result) + + def test_extract_sources_multiple_files_same_project(self): + """Test extract_sources with multiple files from same project.""" + index_content = b''' + + + + Book + Author + + +''' + + file1 = self._create_xml_file("project1", "doc1.xml", b"") + file2 = self._create_xml_file("project1", "doc2.xml", b"") + index_file = self._create_xml_file("project1", "index.xml", index_content) + + preamble, postamble = extract_sources([file1, file2]) + + # Should extract from same index file (deduplicated) + self.assertIn(r'\begin{filecontents*}{job.bib}', preamble) + + def test_transform_xml_to_tex_empty_licenses_credits(self): + """Test transform with no licenses or credits.""" + from unittest.mock import patch + import opensiddur.exporter.tex.xelatex as xelatex_module + + xml_content = b''' + + + + Content only + + +''' + + input_file = self._create_xml_file("project1", "input.xml", xml_content) + + with patch.object(xelatex_module, 'projects_source_root', self.test_dir): + result = transform_xml_to_tex(input_file) + + # Should still produce valid LaTeX even without licenses/credits + self.assertIn(r'\documentclass{book}', result) + # Should not have empty metadata sections + # (The postamble will be empty if no licenses/credits/sources) + + if __name__ == '__main__': unittest.main()