Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -394,3 +394,8 @@ rxivdraft/MANUSCRIPT/2025__antónio_d_brito_et_al__rxiv.pdf
.gemini-clipboard/
gha-creds-*.json
fatetracking/MANUSCRIPT/.rxiv_cache

# Claude-mem context (override negations above)
**/CLAUDE.md

**/*.docx
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.19.0] - 2026-02-05

### Added

- **bioRxiv Submission Package Command**: New `rxiv biorxiv` command generates complete submission package
- Generates bioRxiv author template (TSV format) with HTML entity encoding for special characters
- Includes manuscript PDF, source files (TeX, figures, bibliography)
- Creates ZIP archive ready for bioRxiv upload
- Supports custom submission directory and ZIP filename options
- HTML entity encoding for accented characters (António → António, Åbo → Åbo)
- Automatic handling of multiple corresponding authors (keeps last one)
- Command options: `--biorxiv-dir`, `--zip-filename`, `--no-zip`

### Changed

- **Code Architecture**: Centralized common submission logic in BaseCommand
- Refactored ArxivCommand and BioRxivCommand to share common patterns
- Added `_clear_output_directory()`, `_ensure_pdf_built()`, `_set_submission_defaults()` helper methods
- Eliminated ~64 lines of duplicated code between commands
- Improved maintainability and consistency across submission commands

### Fixed

- **bioRxiv Character Encoding**: Special characters now properly encoded as HTML entities
- Previously stripped accents to ASCII (António → Antonio)
- Now preserves original characters using HTML entities (António → António)
- Complies with bioRxiv's TSV import requirements for international author names

## [1.18.5] - 2026-02-05

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/rxiv_maker/__version__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information."""

__version__ = "1.18.5"
__version__ = "1.19.0"
2 changes: 2 additions & 0 deletions src/rxiv_maker/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .arxiv import arxiv
from .bibliography import bibliography
from .biorxiv import biorxiv
from .build import build as pdf
from .cache_management import cache_group as cache
from .changelog import changelog
Expand Down Expand Up @@ -29,6 +30,7 @@
__all__ = [
"arxiv",
"bibliography",
"biorxiv",
"cache",
"changelog",
"config",
Expand Down
51 changes: 51 additions & 0 deletions src/rxiv_maker/cli/commands/biorxiv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""bioRxiv submission package generation command."""

import rich_click as click

from ..framework.workflow_commands import BioRxivCommand


@click.command(context_settings={"help_option_names": ["-h", "--help"]})
@click.argument("manuscript_path", type=click.Path(exists=True, file_okay=False), required=False)
@click.option("--output-dir", "-o", default="output", help="Output directory for generated files")
@click.option(
"--biorxiv-dir",
"-b",
help="Custom bioRxiv submission directory (default: output/biorxiv_submission)",
)
@click.option(
"--zip-filename",
"-z",
help="Custom ZIP filename (default: {manuscript}_biorxiv.zip)",
)
@click.option(
"--no-zip",
is_flag=True,
help="Don't create ZIP file (only create submission directory)",
)
@click.pass_context
def biorxiv(ctx, manuscript_path, output_dir, biorxiv_dir, zip_filename, no_zip):
r"""Generate bioRxiv submission package.

Creates a complete submission package including:
- bioRxiv author template (TSV file)
- Manuscript PDF
- Source files (TeX, figures, bibliography)
- ZIP file for upload

\b
Example:
rxiv biorxiv # Full package with ZIP
rxiv biorxiv --no-zip # Package without ZIP
rxiv biorxiv -b custom_dir # Custom submission directory
rxiv biorxiv -z my_submission.zip # Custom ZIP filename
"""
command = BioRxivCommand()
return command.run(
ctx,
manuscript_path=manuscript_path,
output_dir=output_dir,
biorxiv_dir=biorxiv_dir,
zip_filename=zip_filename,
no_zip=no_zip,
)
91 changes: 91 additions & 0 deletions src/rxiv_maker/cli/framework/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,97 @@ def error_message(self, message: str, suggestion: Optional[str] = None) -> None:
if suggestion:
self.console.print(f"💡 {suggestion}", style="yellow")

def _clear_output_directory(self) -> None:
"""Clear and recreate the output directory.

Raises:
CommandExecutionError: If path_manager is not initialized
"""
import shutil

if not self.path_manager:
raise CommandExecutionError("Path manager not initialized")

if self.path_manager.output_dir.exists():
shutil.rmtree(self.path_manager.output_dir)
self.path_manager.output_dir.mkdir(parents=True, exist_ok=True)

def _ensure_pdf_built(self, progress_task=None, quiet: bool = True) -> None:
"""Ensure PDF is built, building it if necessary.

Args:
progress_task: Optional progress task to update
quiet: Whether to suppress build output

Raises:
CommandExecutionError: If path_manager is not initialized or build fails
"""
from ...engines.operations.build_manager import BuildManager

if not self.path_manager:
raise CommandExecutionError("Path manager not initialized")

pdf_filename = f"{self.path_manager.manuscript_name}.pdf"
pdf_path = self.path_manager.output_dir / pdf_filename

if not pdf_path.exists():
if progress_task:
progress_task.update(description="Building PDF first...")

build_manager = BuildManager(
manuscript_path=str(self.path_manager.manuscript_path),
output_dir=str(self.path_manager.output_dir),
verbose=self.verbose,
quiet=quiet,
)

try:
success = build_manager.build()
if not success:
raise CommandExecutionError("PDF build failed")
except Exception as e:
raise CommandExecutionError(f"Failed to build PDF: {e}") from e

def _set_submission_defaults(
self,
submission_type: str,
submission_dir: Optional[str] = None,
zip_filename: Optional[str] = None,
) -> tuple[str, str]:
"""Set default paths for submission directories and ZIP files.

Args:
submission_type: Type of submission ("arxiv" or "biorxiv")
submission_dir: Custom submission directory path (optional)
zip_filename: Custom ZIP filename (optional)

Returns:
Tuple of (submission_dir, zip_filename) with defaults applied

Raises:
CommandExecutionError: If path_manager is not initialized
"""
from pathlib import Path

if not self.path_manager:
raise CommandExecutionError("Path manager not initialized")

manuscript_output_dir = str(self.path_manager.output_dir)

# Set default submission directory
if submission_dir is None:
submission_dir = str(Path(manuscript_output_dir) / f"{submission_type}_submission")

# Set default ZIP filename
if zip_filename is None:
manuscript_name = self.path_manager.manuscript_name
if submission_type == "arxiv":
zip_filename = str(Path(manuscript_output_dir) / "for_arxiv.zip")
else:
zip_filename = str(Path(manuscript_output_dir) / f"{manuscript_name}_{submission_type}.zip")

return submission_dir, zip_filename

@abstractmethod
def execute_operation(self, **kwargs) -> Any:
"""Execute the main command operation.
Expand Down
130 changes: 99 additions & 31 deletions src/rxiv_maker/cli/framework/workflow_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,46 +366,25 @@ def execute_operation(
no_zip: Don't create zip file
"""
import sys
from pathlib import Path

from rxiv_maker.engines.operations.build_manager import BuildManager
from rxiv_maker.engines.operations.prepare_arxiv import main as prepare_arxiv_main

if self.path_manager is None:
raise CommandExecutionError("Path manager not initialized")

# Set defaults using shared helper
arxiv_dir, zip_filename = self._set_submission_defaults("arxiv", arxiv_dir, zip_filename)
manuscript_output_dir = str(self.path_manager.output_dir)

# Set defaults using PathManager
if arxiv_dir is None:
arxiv_dir = str(Path(manuscript_output_dir) / "arxiv_submission")
if zip_filename is None:
zip_filename = str(Path(manuscript_output_dir) / "for_arxiv.zip")

with self.create_progress() as progress:
# Clear output directory first (similar to PDF command)
# Clear output directory using shared helper
task = progress.add_task("Clearing output directory...", total=None)
if self.path_manager.output_dir.exists():
shutil.rmtree(self.path_manager.output_dir)
self.path_manager.output_dir.mkdir(parents=True, exist_ok=True)
self._clear_output_directory()

# First, ensure PDF is built
# Ensure PDF is built using shared helper
progress.update(task, description="Checking PDF exists...")
pdf_filename = f"{self.path_manager.manuscript_name}.pdf"
pdf_path = self.path_manager.output_dir / pdf_filename

if not pdf_path.exists():
progress.update(task, description="Building PDF first...")
build_manager = BuildManager(
manuscript_path=str(self.path_manager.manuscript_path),
output_dir=str(self.path_manager.output_dir),
verbose=self.verbose,
quiet=False,
)
success = build_manager.run()
if not success:
self.error_message("PDF build failed. Cannot prepare arXiv package.")
raise CommandExecutionError("PDF build failed")
try:
self._ensure_pdf_built(progress_task=task, quiet=False)
except CommandExecutionError:
self.error_message("PDF build failed. Cannot prepare arXiv package.")
raise

# Prepare arXiv package
progress.update(task, description="Preparing arXiv package...")
Expand Down Expand Up @@ -524,6 +503,95 @@ def _extract_author_and_year(self, config_path: Path) -> tuple[str, str]:
return year, first_author


class BioRxivCommand(BaseCommand):
"""BioRxiv command implementation for generating submission package."""

def execute_operation(
self,
output_dir: str = "output",
biorxiv_dir: Optional[str] = None,
zip_filename: Optional[str] = None,
no_zip: bool = False,
) -> None:
"""Execute bioRxiv submission package preparation.

Args:
output_dir: Output directory for generated files
biorxiv_dir: Custom bioRxiv submission directory path
zip_filename: Custom zip filename
no_zip: Don't create zip file
"""
from pathlib import Path

from ...engines.operations.prepare_biorxiv import (
BioRxivAuthorError,
create_biorxiv_zip,
generate_biorxiv_author_tsv,
prepare_biorxiv_package,
)

# Set defaults using shared helper
biorxiv_dir, zip_filename = self._set_submission_defaults("biorxiv", biorxiv_dir, zip_filename)

with self.create_progress() as progress:
# Clear output directory using shared helper
task = progress.add_task("Clearing output directory...", total=None)
self._clear_output_directory()

# Ensure PDF is built using shared helper
progress.update(task, description="Checking PDF exists...")
self._ensure_pdf_built(progress_task=task, quiet=True)

# Generate bioRxiv author template TSV
progress.update(task, description="Generating bioRxiv author template...")
output_path = self.path_manager.output_dir
tsv_file = output_path / "biorxiv_authors.tsv"

try:
generate_biorxiv_author_tsv(
config_path=self.path_manager.get_config_file_path(),
output_path=tsv_file,
)
except BioRxivAuthorError as e:
progress.update(task, completed=True)
raise CommandExecutionError(f"Failed to generate bioRxiv template: {e}") from e

# Prepare bioRxiv submission package
progress.update(task, description="Preparing bioRxiv submission package...")
try:
biorxiv_path = prepare_biorxiv_package(
manuscript_path=self.path_manager.manuscript_path,
output_dir=self.path_manager.output_dir,
biorxiv_dir=Path(biorxiv_dir),
)
except Exception as e:
progress.update(task, completed=True)
raise CommandExecutionError(f"Failed to prepare bioRxiv package: {e}") from e

# Create ZIP file if requested
zip_path = None
if not no_zip:
progress.update(task, description="Creating ZIP package...")
try:
zip_path = create_biorxiv_zip(
biorxiv_path=biorxiv_path,
zip_filename=zip_filename,
manuscript_path=self.path_manager.manuscript_path,
)
except Exception as e:
progress.update(task, completed=True)
raise CommandExecutionError(f"Failed to create ZIP: {e}") from e

progress.update(task, completed=True)

# Show success message
self.console.print("\n[green]✅ bioRxiv submission package ready![/green]")
self.console.print(f" 📁 Package directory: {biorxiv_path}")
if zip_path:
self.console.print(f" 📦 ZIP file: {zip_path}")
self.console.print("\n📤 Upload to: https://submit.biorxiv.org/")


class TrackChangesCommand(BaseCommand):
"""Track changes command implementation using the framework."""

Expand Down
3 changes: 2 additions & 1 deletion src/rxiv_maker/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
},
{
"name": "Workflow Commands",
"commands": ["get-rxiv-preprint", "arxiv", "track-changes", "setup"],
"commands": ["get-rxiv-preprint", "arxiv", "biorxiv", "track-changes", "setup"],
},
{
"name": "Configuration",
Expand Down Expand Up @@ -252,6 +252,7 @@ def main(
main.add_command(commands.figures)
main.add_command(commands.get_rxiv_preprint, name="get-rxiv-preprint")
main.add_command(commands.arxiv)
main.add_command(commands.biorxiv)
main.add_command(commands.init)
main.add_command(commands.bibliography)
main.add_command(commands.track_changes)
Expand Down
Loading
Loading