diff --git a/src/article_cli/cli.py b/src/article_cli/cli.py index 6e71fa6..d25746c 100644 --- a/src/article_cli/cli.py +++ b/src/article_cli/cli.py @@ -76,6 +76,23 @@ def create_parser() -> argparse.ArgumentParser: action="store_true", help="Overwrite existing files", ) + init_parser.add_argument( + "--type", + choices=["article", "presentation", "poster"], + default="article", + help="Project type (default: article). Use 'presentation' for Beamer slides.", + ) + init_parser.add_argument( + "--theme", + default="", + help="Beamer theme for presentations (e.g., 'numpex', 'metropolis').", + ) + init_parser.add_argument( + "--aspect-ratio", + choices=["169", "43", "1610"], + default="169", + help="Aspect ratio for presentations (default: 169 for 16:9).", + ) # Setup command subparsers.add_parser("setup", help="Setup git hooks for gitinfo2") @@ -185,6 +202,9 @@ def handle_init_command(args: argparse.Namespace, config: Config) -> int: group_id=args.group_id, force=args.force, main_tex_file=args.tex_file, + project_type=args.type, + theme=args.theme, + aspect_ratio=args.aspect_ratio, ) else 1 ) diff --git a/src/article_cli/config.py b/src/article_cli/config.py index 5bf65e1..7153df3 100644 --- a/src/article_cli/config.py +++ b/src/article_cli/config.py @@ -174,6 +174,29 @@ def get_latex_config(self) -> Dict[str, Any]: "timeout": self.get("latex", "timeout", 300), } + def get_project_config(self) -> Dict[str, Any]: + """Get project-level configuration""" + return { + "project_type": self.get("project", "type", "article"), + } + + def get_presentation_config(self) -> Dict[str, Any]: + """Get presentation-specific configuration (for Beamer)""" + return { + "theme": self.get("presentation", "theme", ""), + "aspect_ratio": self.get("presentation", "aspect_ratio", "169"), + "color_theme": self.get("presentation", "color_theme", ""), + "font_theme": self.get("presentation", "font_theme", ""), + } + + def get_poster_config(self) -> Dict[str, Any]: + """Get poster-specific configuration""" + return { + "size": self.get("poster", "size", "a0"), + "orientation": self.get("poster", "orientation", "portrait"), + "columns": self.get("poster", "columns", 3), + } + def validate_zotero_config( self, args: argparse.Namespace ) -> Dict[str, Optional[str]]: @@ -275,6 +298,35 @@ def create_sample_config(self, path: Optional[Path] = None) -> Path: # Compilation timeout in seconds timeout = 300 + +[project] +# Project type: "article", "presentation", or "poster" +type = "article" + +# Presentation-specific settings (only used when type = "presentation") +[presentation] +# Beamer theme (e.g., "numpex", "metropolis", "default") +theme = "" + +# Aspect ratio: "169" (16:9), "43" (4:3), or "1610" (16:10) +aspect_ratio = "169" + +# Optional: color theme (e.g., "crane", "dolphin") +color_theme = "" + +# Optional: font theme (e.g., "professionalfonts") +font_theme = "" + +# Poster-specific settings (only used when type = "poster") +[poster] +# Poster size: "a0", "a1", "a2", etc. +size = "a0" + +# Orientation: "portrait" or "landscape" +orientation = "portrait" + +# Number of columns +columns = 3 """ try: diff --git a/src/article_cli/repository_setup.py b/src/article_cli/repository_setup.py index f7513c0..e80c219 100644 --- a/src/article_cli/repository_setup.py +++ b/src/article_cli/repository_setup.py @@ -33,9 +33,12 @@ def init_repository( group_id: str = "4678293", force: bool = False, main_tex_file: Optional[str] = None, + project_type: str = "article", + theme: str = "", + aspect_ratio: str = "169", ) -> bool: """ - Initialize a complete LaTeX article repository + Initialize a complete LaTeX repository (article, presentation, or poster) Creates: - GitHub Actions workflow for automated PDF compilation @@ -45,60 +48,79 @@ def init_repository( - .vscode/settings.json with LaTeX Workshop configuration - LTeX dictionary files for spell checking - hooks/post-commit for gitinfo2 integration - - main.tex if no .tex file exists + - main.tex if no .tex file exists (or presentation/poster template) Args: - title: Article title + title: Document title authors: List of author names group_id: Zotero group ID force: Overwrite existing files if True main_tex_file: Main .tex filename (auto-detected if None, created if missing) + project_type: Type of project ("article", "presentation", "poster") + theme: Beamer theme for presentations (e.g., "numpex", "metropolis") + aspect_ratio: Aspect ratio for presentations ("169", "43", "1610") Returns: True if successful, False otherwise """ - print_info(f"Initializing repository at: {self.repo_path}") + print_info(f"Initializing {project_type} repository at: {self.repo_path}") # Detect or validate main .tex file, create if missing - tex_file = self._detect_or_create_tex_file(main_tex_file, title, authors, force) + tex_file = self._detect_or_create_tex_file( + main_tex_file, title, authors, force, project_type, theme, aspect_ratio + ) if not tex_file: print_error("Failed to detect or create main .tex file") return False project_name = self.repo_path.name print_info(f"Project name: {project_name}") + print_info(f"Project type: {project_type}") print_info(f"Main tex file: {tex_file}") - print_info(f"Article title: {title}") + print_info(f"Title: {title}") print_info(f"Authors: {', '.join(authors)}") + if project_type == "presentation" and theme: + print_info(f"Beamer theme: {theme}") try: # Create directory structure self._create_directories() # Create GitHub Actions workflows - if not self._create_workflow(project_name, tex_file, force): + if not self._create_workflow(project_name, tex_file, force, project_type): return False # Create pyproject.toml if not self._create_pyproject( - project_name, title, authors, group_id, force + project_name, + title, + authors, + group_id, + force, + project_type, + theme, + aspect_ratio, ): return False # Create README - if not self._create_readme(project_name, title, authors, tex_file, force): + if not self._create_readme( + project_name, title, authors, tex_file, force, project_type + ): return False # Create .gitignore if needed self._create_gitignore(force) # Create VS Code configuration - self._create_vscode_settings(force) + self._create_vscode_settings(force, project_type) # Create git hooks directory and post-commit hook self._create_git_hooks(force) - print_success("\n✅ Repository initialization complete!") + print_success( + f"\n✅ {project_type.capitalize()} repository initialization complete!" + ) print_info("\nNext steps:") print_info(" 1. Review and edit pyproject.toml") print_info(" 2. Add ZOTERO_API_KEY secret to GitHub repository") @@ -113,16 +135,26 @@ def init_repository( return False def _detect_or_create_tex_file( - self, specified: Optional[str], title: str, authors: List[str], force: bool + self, + specified: Optional[str], + title: str, + authors: List[str], + force: bool, + project_type: str = "article", + theme: str = "", + aspect_ratio: str = "169", ) -> Optional[str]: """ Detect main .tex file in repository or create one if missing Args: specified: User-specified filename (takes priority) - title: Article title (for creating new .tex file) + title: Document title (for creating new .tex file) authors: List of author names (for creating new .tex file) force: Overwrite existing file if True + project_type: Type of project ("article", "presentation", "poster") + theme: Beamer theme for presentations + aspect_ratio: Aspect ratio for presentations Returns: Main .tex filename or None on failure @@ -132,7 +164,9 @@ def _detect_or_create_tex_file( if tex_path.exists(): return specified # Specified file doesn't exist - create it - if self._create_tex_file(specified, title, authors, force): + if self._create_tex_file( + specified, title, authors, force, project_type, theme, aspect_ratio + ): return specified return None @@ -140,10 +174,17 @@ def _detect_or_create_tex_file( tex_files = list(self.repo_path.glob("*.tex")) if not tex_files: - # No .tex files found - create main.tex - default_name = "main.tex" + # No .tex files found - create default based on project type + if project_type == "presentation": + default_name = "presentation.tex" + elif project_type == "poster": + default_name = "poster.tex" + else: + default_name = "main.tex" print_info(f"No .tex file found, creating {default_name}") - if self._create_tex_file(default_name, title, authors, force): + if self._create_tex_file( + default_name, title, authors, force, project_type, theme, aspect_ratio + ): return default_name return None @@ -151,7 +192,13 @@ def _detect_or_create_tex_file( return tex_files[0].name # Multiple .tex files - prefer common patterns - for pattern in ["main.tex", "article.tex", f"{self.repo_path.name}.tex"]: + for pattern in [ + "main.tex", + "article.tex", + "presentation.tex", + "poster.tex", + f"{self.repo_path.name}.tex", + ]: if (self.repo_path / pattern).exists(): return pattern @@ -163,16 +210,26 @@ def _detect_or_create_tex_file( return tex_files[0].name def _create_tex_file( - self, filename: str, title: str, authors: List[str], force: bool + self, + filename: str, + title: str, + authors: List[str], + force: bool, + project_type: str = "article", + theme: str = "", + aspect_ratio: str = "169", ) -> bool: """ - Create a basic LaTeX article file + Create a LaTeX file based on project type Args: filename: Name of the .tex file to create - title: Article title + title: Document title authors: List of author names force: Overwrite if exists + project_type: Type of project ("article", "presentation", "poster") + theme: Beamer theme for presentations + aspect_ratio: Aspect ratio for presentations Returns: True if successful @@ -184,9 +241,24 @@ def _create_tex_file( return True # Format authors for LaTeX - authors_latex = " \\and ".join(authors) + authors_latex = " \\\\and ".join(authors) - tex_content = f"""\\documentclass[a4paper,11pt]{{article}} + if project_type == "presentation": + tex_content = self._get_presentation_template( + title, authors_latex, theme, aspect_ratio + ) + elif project_type == "poster": + tex_content = self._get_poster_template(title, authors_latex) + else: + tex_content = self._get_article_template(title, authors_latex) + + tex_path.write_text(tex_content) + print_success(f"Created: {tex_path.relative_to(self.repo_path)}") + return True + + def _get_article_template(self, title: str, authors_latex: str) -> str: + """Get article template content""" + return f"""\\documentclass[a4paper,11pt]{{article}} % Essential packages \\usepackage[utf8]{{inputenc}} @@ -246,9 +318,140 @@ def _create_tex_file( \\end{{document}} """ - tex_path.write_text(tex_content) - print_success(f"Created: {tex_path.relative_to(self.repo_path)}") - return True + def _get_presentation_template( + self, title: str, authors_latex: str, theme: str, aspect_ratio: str + ) -> str: + """Get Beamer presentation template content""" + theme_line = f"\\usetheme{{{theme}}}" if theme else "% \\usetheme{default}" + + return f"""\\documentclass[aspectratio={aspect_ratio}]{{beamer}} + +% Theme configuration +{theme_line} + +% Essential packages +\\usepackage{{tikz}} +\\usepackage{{pgfplots}} +\\pgfplotsset{{compat=newest}} +\\usepackage{{booktabs}} +\\usepackage{{hyperref}} + +% Bibliography (optional) +% \\usepackage[style=numeric,sorting=none]{{biblatex}} +% \\addbibresource{{references.bib}} + +% Git version information +\\usepackage{{gitinfo2}} + +% Title and authors +\\title{{{title}}} +\\author{{{authors_latex}}} +\\date{{\\today}} +\\institute{{Your Institution}} + +\\begin{{document}} + +\\maketitle + +\\begin{{frame}}{{Outline}} + \\tableofcontents +\\end{{frame}} + +\\section{{Introduction}} + +\\begin{{frame}}{{Introduction}} + \\begin{{itemize}} + \\item First point + \\item Second point + \\item Third point + \\end{{itemize}} +\\end{{frame}} + +\\section{{Main Content}} + +\\begin{{frame}}{{Main Content}} + Your main content goes here. +\\end{{frame}} + +\\section{{Conclusion}} + +\\begin{{frame}}{{Conclusion}} + \\begin{{itemize}} + \\item Summary point 1 + \\item Summary point 2 + \\end{{itemize}} +\\end{{frame}} + +\\begin{{frame}}{{Questions?}} + \\centering + \\Large Thank you for your attention! + + \\vspace{{1cm}} + \\small + Git version: \\gitAbbrevHash{{}} (\\gitAuthorIsoDate) +\\end{{frame}} + +\\end{{document}} +""" + + def _get_poster_template(self, title: str, authors_latex: str) -> str: + """Get poster template content""" + return f"""\\documentclass[a0paper,portrait]{{tikzposter}} + +% Essential packages +\\usepackage{{amsmath,amssymb}} +\\usepackage{{graphicx}} +\\usepackage{{booktabs}} + +% Git version information +\\usepackage{{gitinfo2}} + +% Title and authors +\\title{{{title}}} +\\author{{{authors_latex}}} +\\institute{{Your Institution}} +\\date{{\\today}} + +% Theme +\\usetheme{{Default}} + +\\begin{{document}} + +\\maketitle + +\\begin{{columns}} + \\column{{0.5}} + + \\block{{Introduction}}{{ + Your introduction goes here. + }} + + \\block{{Methods}}{{ + Your methods description goes here. + }} + + \\column{{0.5}} + + \\block{{Results}}{{ + Your results go here. + }} + + \\block{{Conclusions}}{{ + Your conclusions go here. + }} + +\\end{{columns}} + +\\block{{References}}{{ + Your references go here. +}} + +\\note[targetoffsetx=0cm, targetoffsety=-8cm, width=0.4\\textwidth]{{ + Git version: \\gitAbbrevHash{{}} (\\gitAuthorIsoDate) +}} + +\\end{{document}} +""" def _create_directories(self) -> None: """Create necessary directory structure""" @@ -262,7 +465,13 @@ def _create_directories(self) -> None: directory.mkdir(parents=True, exist_ok=True) print_info(f"Created directory: {directory.relative_to(self.repo_path)}") - def _create_workflow(self, project_name: str, tex_file: str, force: bool) -> bool: + def _create_workflow( + self, + project_name: str, + tex_file: str, + force: bool, + project_type: str = "article", + ) -> bool: """ Create GitHub Actions workflow file @@ -270,6 +479,8 @@ def _create_workflow(self, project_name: str, tex_file: str, force: bool) -> boo project_name: Name of the project tex_file: Main .tex filename force: Overwrite if exists + project_type: Type of project for workflow customization + force: Overwrite if exists Returns: True if successful @@ -747,16 +958,22 @@ def _create_pyproject( authors: List[str], group_id: str, force: bool, + project_type: str = "article", + theme: str = "", + aspect_ratio: str = "169", ) -> bool: """ Create pyproject.toml file Args: project_name: Name of the project - title: Article title + title: Document title authors: List of author names group_id: Zotero group ID force: Overwrite if exists + project_type: Type of project + theme: Beamer theme for presentations + aspect_ratio: Aspect ratio for presentations Returns: True if successful @@ -764,14 +981,39 @@ def _create_pyproject( pyproject_path = self.repo_path / "pyproject.toml" if pyproject_path.exists() and not force: - print_info(f"pyproject.toml already exists (use --force to overwrite)") + print_info("pyproject.toml already exists (use --force to overwrite)") return True # Format authors for TOML authors_toml = ",\n ".join([f'{{name = "{author}"}}' for author in authors]) - pyproject_content = f"""# Article Repository Dependency Management -# This file manages dependencies for the LaTeX article project + # Determine default engine based on project type + default_engine = "xelatex" if project_type == "presentation" else "latexmk" + + # Build project type specific config + project_type_config = f""" +[tool.article-cli.project] +type = "{project_type}" +""" + + if project_type == "presentation": + project_type_config += f""" +[tool.article-cli.presentation] +theme = "{theme}" +aspect_ratio = "{aspect_ratio}" +color_theme = "" +font_theme = "" +""" + elif project_type == "poster": + project_type_config += """ +[tool.article-cli.poster] +size = "a0" +orientation = "portrait" +columns = 3 +""" + + pyproject_content = f"""# {project_type.capitalize()} Repository Dependency Management +# This file manages dependencies for the LaTeX {project_type} project [project] name = "{project_name}" @@ -783,8 +1025,8 @@ def _create_pyproject( readme = "README.md" requires-python = ">=3.8" dependencies = [ - "article-cli>=1.1.0", - # Add other dependencies your article might need: + "article-cli>=1.2.0", + # Add other dependencies your project might need: # "matplotlib>=3.5.0", # "numpy>=1.20.0", # "pandas>=1.3.0", @@ -804,9 +1046,10 @@ def _create_pyproject( clean_extensions = [ ".aux", ".bbl", ".blg", ".log", ".out", ".pyg", ".fls", ".synctex.gz", ".toc", ".fdb_latexmk", - ".idx", ".ilg", ".ind", ".lof", ".lot" + ".idx", ".ilg", ".ind", ".lof", ".lot", ".nav", ".snm", ".vrb" ] -""" +engine = "{default_engine}" +{project_type_config}""" pyproject_path.write_text(pyproject_content) print_success(f"Created: {pyproject_path.relative_to(self.repo_path)}") @@ -819,16 +1062,18 @@ def _create_readme( authors: List[str], tex_file: str, force: bool, + project_type: str = "article", ) -> bool: """ Create README.md file Args: project_name: Name of the project - title: Article title + title: Document title authors: List of author names tex_file: Main .tex filename force: Overwrite if exists + project_type: Type of project Returns: True if successful @@ -836,11 +1081,22 @@ def _create_readme( readme_path = self.repo_path / "README.md" if readme_path.exists() and not force: - print_info(f"README.md already exists (use --force to overwrite)") + print_info("README.md already exists (use --force to overwrite)") return True authors_list = "\n".join([f"- {author}" for author in authors]) + # Determine build command based on project type + if project_type == "presentation": + build_cmd = f"latexmk -xelatex {tex_file}" + doc_type = "presentation" + elif project_type == "poster": + build_cmd = f"latexmk -xelatex {tex_file}" + doc_type = "poster" + else: + build_cmd = f"latexmk -pdf {tex_file}" + doc_type = "article" + readme_content = f"""# {title} ## Authors @@ -849,7 +1105,7 @@ def _create_readme( ## Overview -This repository contains the LaTeX source for the article "{title}". +This repository contains the LaTeX source for the {doc_type} "{title}". ## Prerequisites @@ -890,7 +1146,12 @@ def _create_readme( ### Local Build ```bash -latexmk -pdf {tex_file} +{build_cmd} +``` + +Or using article-cli: +```bash +article-cli compile {tex_file} ``` ### Clean Build Files @@ -1042,12 +1303,15 @@ def _create_gitignore(self, force: bool) -> bool: return True - def _create_vscode_settings(self, force: bool) -> bool: + def _create_vscode_settings( + self, force: bool, project_type: str = "article" + ) -> bool: """ Create VS Code settings for LaTeX Workshop Args: force: Overwrite if exists + project_type: Type of project (article, presentation, poster) Returns: True if successful @@ -1060,7 +1324,91 @@ def _create_vscode_settings(self, force: bool) -> bool: ) return True - settings_content = """{ + # Use XeLaTeX for presentations and posters + if project_type in ("presentation", "poster"): + settings_content = """{ + "latex-workshop.latex.recipes": [ + { + "name": "latexmk-xelatex", + "tools": [ + "latexmk-xelatex-shell-escape" + ] + }, + { + "name": "latexmk-lualatex", + "tools": [ + "latexmk-lualatex-shell-escape" + ] + }, + { + "name": "xelatex-shell-escape-recipe", + "tools": [ + "xelatex-shell-escape" + ] + } + ], + "latex-workshop.latex.tools": [ + { + "name": "latexmk-xelatex-shell-escape", + "command": "latexmk", + "args": [ + "--shell-escape", + "-xelatex", + "-interaction=nonstopmode", + "-synctex=1", + "%DOC%" + ], + "env": {} + }, + { + "name": "latexmk-lualatex-shell-escape", + "command": "latexmk", + "args": [ + "--shell-escape", + "-lualatex", + "-interaction=nonstopmode", + "-synctex=1", + "%DOC%" + ], + "env": {} + }, + { + "name": "xelatex-shell-escape", + "command": "xelatex", + "args": [ + "--shell-escape", + "-synctex=1", + "-interaction=nonstopmode", + "-file-line-error", + "%DOC%" + ] + } + ], + "latex-workshop.latex.autoBuild.run": "onSave", + "latex-workshop.latex.autoBuild.enabled": true, + "latex-workshop.latex.build.showOutput": "always", + "latex-workshop.latex.outDir": "%DIR%", + "latex-workshop.latex.clean.subfolder.enabled": true, + "latex-workshop.message.badbox.show": "none", + "workbench.editor.pinnedTabsOnSeparateRow": true, + "ltex.latex.commands": { + "\\\\author{}": "ignore", + "\\\\IfFileExists{}{}": "ignore", + "\\\\todo{}": "ignore", + "\\\\todo[]{}": "ignore", + "\\\\ts{}": "ignore", + "\\\\cp{}": "ignore", + "\\\\pgfmathprintnumber{}": "dummy", + "\\\\feelpp{}": "dummy", + "\\\\pgfplotstableread[]{}": "ignore", + "\\\\xpatchcmd{}{}{}{}{}": "ignore" + }, + "ltex.enabled": true, + "ltex.language": "en-US" +} +""" + else: + settings_content = """{ "latex-workshop.latex.recipes": [ { "name": "latexmk-pdf", diff --git a/tests/test_repository_setup.py b/tests/test_repository_setup.py new file mode 100644 index 0000000..bddce17 --- /dev/null +++ b/tests/test_repository_setup.py @@ -0,0 +1,482 @@ +""" +Tests for article-cli repository setup module + +Tests project type support including article, presentation, and poster templates. +""" + +import os +import tempfile +from pathlib import Path +from unittest.mock import patch, MagicMock +import pytest + +from article_cli.repository_setup import RepositorySetup + + +class TestRepositorySetupProjectTypes: + """Test cases for project type support in RepositorySetup""" + + def test_init_default_repo_path(self): + """Test RepositorySetup uses current directory by default""" + setup = RepositorySetup() + assert setup.repo_path == Path.cwd() + + def test_init_custom_repo_path(self): + """Test RepositorySetup with custom path""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + assert setup.repo_path == Path(temp_dir) + + def test_detect_or_create_tex_file_article_default(self): + """Test default tex filename for article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._detect_or_create_tex_file( + None, "Test Title", ["Author One"], False, "article", "", "169" + ) + # Should create main.tex for articles + assert result == "main.tex" + assert (Path(temp_dir) / "main.tex").exists() + + def test_detect_or_create_tex_file_presentation_default(self): + """Test default tex filename for presentation type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._detect_or_create_tex_file( + None, "Test Title", ["Author One"], False, "presentation", "", "169" + ) + # Should create presentation.tex for presentations + assert result == "presentation.tex" + assert (Path(temp_dir) / "presentation.tex").exists() + + def test_detect_or_create_tex_file_poster_default(self): + """Test default tex filename for poster type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._detect_or_create_tex_file( + None, "Test Title", ["Author One"], False, "poster", "", "169" + ) + # Should create poster.tex for posters + assert result == "poster.tex" + assert (Path(temp_dir) / "poster.tex").exists() + + def test_detect_existing_tex_file(self): + """Test detection of existing .tex file""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create an existing tex file + existing_tex = Path(temp_dir) / "existing.tex" + existing_tex.write_text("\\documentclass{article}") + + setup = RepositorySetup(Path(temp_dir)) + result = setup._detect_or_create_tex_file( + None, "Test Title", ["Author One"], False, "article", "", "169" + ) + # Should detect the existing file + assert result == "existing.tex" + + +class TestArticleTemplate: + """Test cases for article template generation""" + + def test_get_article_template_content(self): + """Test article template contains expected elements""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_article_template("Test Article", ["John Doe"]) + + assert "\\documentclass" in template + assert "article" in template + assert "Test Article" in template + assert "\\usepackage{amsmath" in template + assert "\\maketitle" in template + + def test_get_article_template_multiple_authors(self): + """Test article template with multiple authors""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_article_template( + "Multi-Author Article", ["Alice Smith", "Bob Jones", "Carol White"] + ) + + # Template includes authors list + assert "Alice Smith" in template or "Multi-Author Article" in template + + +class TestPresentationTemplate: + """Test cases for presentation (Beamer) template generation""" + + def test_get_presentation_template_basic(self): + """Test presentation template contains expected Beamer elements""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_presentation_template( + "Test Presentation", ["Presenter One"], "", "169" + ) + + assert "\\documentclass" in template + assert "beamer" in template + assert "Test Presentation" in template + assert "Presenter One" in template + + def test_get_presentation_template_with_theme(self): + """Test presentation template with custom Beamer theme""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_presentation_template( + "Themed Presentation", ["Speaker"], "metropolis", "169" + ) + + assert "metropolis" in template + assert "\\usetheme{metropolis}" in template + + def test_get_presentation_template_aspect_ratio_169(self): + """Test presentation template with 16:9 aspect ratio""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_presentation_template( + "Wide Presentation", ["Speaker"], "", "169" + ) + + assert "aspectratio=169" in template + + def test_get_presentation_template_aspect_ratio_43(self): + """Test presentation template with 4:3 aspect ratio""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_presentation_template( + "Standard Presentation", ["Speaker"], "", "43" + ) + + assert "aspectratio=43" in template + + def test_get_presentation_template_aspect_ratio_1610(self): + """Test presentation template with 16:10 aspect ratio""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_presentation_template( + "16:10 Presentation", ["Speaker"], "", "1610" + ) + + assert "aspectratio=1610" in template + + +class TestPosterTemplate: + """Test cases for poster (beamerposter or tikzposter) template generation""" + + def test_get_poster_template_basic(self): + """Test poster template contains expected elements""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_poster_template( + "Test Poster", ["Researcher One", "Researcher Two"] + ) + + assert "\\documentclass" in template + # May use tikzposter or beamerposter + assert "poster" in template.lower() + assert "Test Poster" in template + + def test_get_poster_template_size_a0(self): + """Test poster template uses A0 size by default""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + template = setup._get_poster_template("Poster", ["Author"]) + + # Should have A0 paper size settings + assert "a0" in template.lower() + + +class TestConfigGeneration: + """Test cases for configuration file generation""" + + def test_create_pyproject_article_type(self): + """Test pyproject.toml generation for article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_pyproject( + "test-project", + "Test Article", + ["Author"], + "12345", + False, + "article", + "", + "169", + ) + + assert result is True + pyproject_path = Path(temp_dir) / "pyproject.toml" + assert pyproject_path.exists() + + content = pyproject_path.read_text() + assert "[tool.article-cli.project]" in content + assert 'type = "article"' in content + + def test_create_pyproject_presentation_type(self): + """Test pyproject.toml generation for presentation type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_pyproject( + "test-presentation", + "Test Presentation", + ["Speaker"], + "12345", + False, + "presentation", + "numpex", + "169", + ) + + assert result is True + pyproject_path = Path(temp_dir) / "pyproject.toml" + content = pyproject_path.read_text() + + assert "[tool.article-cli.project]" in content + assert 'type = "presentation"' in content + assert "[tool.article-cli.presentation]" in content + assert 'theme = "numpex"' in content + assert 'aspect_ratio = "169"' in content + # Presentations should default to xelatex + assert 'engine = "xelatex"' in content + + def test_create_pyproject_poster_type(self): + """Test pyproject.toml generation for poster type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_pyproject( + "test-poster", + "Test Poster", + ["Researcher"], + "12345", + False, + "poster", + "", + "169", + ) + + assert result is True + pyproject_path = Path(temp_dir) / "pyproject.toml" + content = pyproject_path.read_text() + + assert "[tool.article-cli.project]" in content + assert 'type = "poster"' in content + assert "[tool.article-cli.poster]" in content + + +class TestReadmeGeneration: + """Test cases for README.md generation""" + + def test_create_readme_article_type(self): + """Test README.md generation for article type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_readme( + "test-article", "Test Article", ["Author"], "main.tex", False, "article" + ) + + assert result is True + readme_path = Path(temp_dir) / "README.md" + assert readme_path.exists() + + content = readme_path.read_text() + assert "Test Article" in content + assert "latexmk -pdf main.tex" in content + assert "article" in content.lower() + + def test_create_readme_presentation_type(self): + """Test README.md generation for presentation type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_readme( + "test-presentation", + "Test Presentation", + ["Speaker"], + "presentation.tex", + False, + "presentation", + ) + + assert result is True + readme_path = Path(temp_dir) / "README.md" + content = readme_path.read_text() + + assert "Test Presentation" in content + assert "latexmk -xelatex presentation.tex" in content + assert "presentation" in content.lower() + + def test_create_readme_poster_type(self): + """Test README.md generation for poster type""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_readme( + "test-poster", + "Test Poster", + ["Researcher"], + "poster.tex", + False, + "poster", + ) + + assert result is True + readme_path = Path(temp_dir) / "README.md" + content = readme_path.read_text() + + assert "Test Poster" in content + assert "latexmk -xelatex poster.tex" in content + assert "poster" in content.lower() + + +class TestVSCodeSettingsGeneration: + """Test cases for VS Code settings generation""" + + def test_create_vscode_settings_article(self): + """Test VS Code settings for article type (pdflatex)""" + with tempfile.TemporaryDirectory() as temp_dir: + vscode_dir = Path(temp_dir) / ".vscode" + vscode_dir.mkdir() + + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_vscode_settings(False, "article") + + assert result is True + settings_path = vscode_dir / "settings.json" + assert settings_path.exists() + + content = settings_path.read_text() + assert "latexmk-pdf" in content + assert '"-pdf"' in content + + def test_create_vscode_settings_presentation(self): + """Test VS Code settings for presentation type (xelatex)""" + with tempfile.TemporaryDirectory() as temp_dir: + vscode_dir = Path(temp_dir) / ".vscode" + vscode_dir.mkdir() + + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_vscode_settings(False, "presentation") + + assert result is True + settings_path = vscode_dir / "settings.json" + content = settings_path.read_text() + + assert "latexmk-xelatex" in content + assert '"-xelatex"' in content + + def test_create_vscode_settings_poster(self): + """Test VS Code settings for poster type (xelatex)""" + with tempfile.TemporaryDirectory() as temp_dir: + vscode_dir = Path(temp_dir) / ".vscode" + vscode_dir.mkdir() + + setup = RepositorySetup(Path(temp_dir)) + result = setup._create_vscode_settings(False, "poster") + + assert result is True + settings_path = vscode_dir / "settings.json" + content = settings_path.read_text() + + assert "latexmk-xelatex" in content + assert '"-xelatex"' in content + + +class TestFullRepositoryInit: + """Integration tests for full repository initialization""" + + def test_init_article_repository(self): + """Test full article repository initialization""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup.init_repository( + title="Test Article", + authors=["Author One", "Author Two"], + group_id="12345", + force=False, + project_type="article", + ) + + assert result is True + # Check all expected files exist + assert (Path(temp_dir) / "main.tex").exists() + assert (Path(temp_dir) / "pyproject.toml").exists() + assert (Path(temp_dir) / "README.md").exists() + assert (Path(temp_dir) / ".gitignore").exists() + assert (Path(temp_dir) / ".vscode" / "settings.json").exists() + assert (Path(temp_dir) / ".github" / "workflows").exists() + + def test_init_presentation_repository(self): + """Test full presentation repository initialization""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup.init_repository( + title="Test Presentation", + authors=["Speaker One"], + group_id="12345", + force=False, + project_type="presentation", + theme="metropolis", + aspect_ratio="169", + ) + + assert result is True + # Check presentation-specific files + assert (Path(temp_dir) / "presentation.tex").exists() + + # Check pyproject.toml has presentation config + pyproject = (Path(temp_dir) / "pyproject.toml").read_text() + assert 'type = "presentation"' in pyproject + assert "[tool.article-cli.presentation]" in pyproject + + def test_init_poster_repository(self): + """Test full poster repository initialization""" + with tempfile.TemporaryDirectory() as temp_dir: + setup = RepositorySetup(Path(temp_dir)) + result = setup.init_repository( + title="Test Poster", + authors=["Researcher"], + group_id="12345", + force=False, + project_type="poster", + ) + + assert result is True + # Check poster-specific files + assert (Path(temp_dir) / "poster.tex").exists() + + # Check pyproject.toml has poster config + pyproject = (Path(temp_dir) / "pyproject.toml").read_text() + assert 'type = "poster"' in pyproject + assert "[tool.article-cli.poster]" in pyproject + + +class TestConfigProjectMethods: + """Test cases for Config class project type methods""" + + def test_get_project_config_defaults(self): + """Test getting project configuration with defaults""" + from article_cli.config import Config + + config = Config() + project_config = config.get_project_config() + + # Uses 'project_type' key, defaults to 'article' + assert project_config["project_type"] == "article" + + def test_get_presentation_config_defaults(self): + """Test getting presentation configuration with defaults""" + from article_cli.config import Config + + config = Config() + presentation_config = config.get_presentation_config() + + assert presentation_config["theme"] == "" + assert presentation_config["aspect_ratio"] == "169" + + def test_get_poster_config_defaults(self): + """Test getting poster configuration with defaults""" + from article_cli.config import Config + + config = Config() + poster_config = config.get_poster_config() + + assert poster_config["size"] == "a0" + assert poster_config["orientation"] == "portrait"