rxiv-maker 1.16.7__py3-none-any.whl → 1.17.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,29 @@ logger = get_logger()
24
24
  class DocxWriter:
25
25
  """Writes structured content to DOCX files using python-docx."""
26
26
 
27
+ # Color mapping for different reference types
28
+ XREF_COLORS = {
29
+ "fig": WD_COLOR_INDEX.BRIGHT_GREEN, # Figures (bright green - lighter)
30
+ "sfig": WD_COLOR_INDEX.TURQUOISE, # Supplementary figures (turquoise - lighter cyan)
31
+ "stable": WD_COLOR_INDEX.TURQUOISE, # Supplementary tables (turquoise - lighter cyan)
32
+ "table": WD_COLOR_INDEX.BLUE, # Main tables
33
+ "eq": WD_COLOR_INDEX.VIOLET, # Equations
34
+ "snote": WD_COLOR_INDEX.TURQUOISE, # Supplementary notes (turquoise - lighter cyan)
35
+ "cite": WD_COLOR_INDEX.YELLOW, # Citations (yellow)
36
+ }
37
+
38
+ @staticmethod
39
+ def get_xref_color(xref_type: str):
40
+ """Get highlight color for a cross-reference type.
41
+
42
+ Args:
43
+ xref_type: Type of cross-reference (fig, sfig, stable, table, eq, snote, cite)
44
+
45
+ Returns:
46
+ WD_COLOR_INDEX color for the xref type, or YELLOW as default
47
+ """
48
+ return DocxWriter.XREF_COLORS.get(xref_type, WD_COLOR_INDEX.YELLOW)
49
+
27
50
  def write(
28
51
  self,
29
52
  doc_structure: Dict[str, Any],
@@ -32,6 +55,8 @@ class DocxWriter:
32
55
  include_footnotes: bool = True,
33
56
  base_path: Optional[Path] = None,
34
57
  metadata: Optional[Dict[str, Any]] = None,
58
+ table_map: Optional[Dict[str, int]] = None,
59
+ figures_at_end: bool = False,
35
60
  ) -> Path:
36
61
  """Write DOCX file from structured content.
37
62
 
@@ -42,6 +67,8 @@ class DocxWriter:
42
67
  include_footnotes: Whether to add DOI footnotes
43
68
  base_path: Base path for resolving relative figure paths
44
69
  metadata: Document metadata (title, authors, affiliations)
70
+ table_map: Mapping from table labels to numbers (for supplementary tables)
71
+ figures_at_end: Place main figures at end before SI/bibliography
45
72
 
46
73
  Returns:
47
74
  Path to created DOCX file
@@ -49,6 +76,7 @@ class DocxWriter:
49
76
  self.base_path = base_path or Path.cwd()
50
77
  self.bibliography = bibliography
51
78
  self.include_footnotes = include_footnotes
79
+ self.table_map = table_map or {}
52
80
  doc = Document()
53
81
 
54
82
  # Add title and author information if metadata provided
@@ -69,15 +97,36 @@ class DocxWriter:
69
97
  # Store figure map for use in text processing
70
98
  self.figure_map = figure_map
71
99
 
72
- # Process each section INCLUDING figures inline
100
+ # Collect main figures if figures_at_end is True
101
+ collected_main_figures = []
102
+
103
+ # Process each section
73
104
  figure_counter = 0
105
+ sfigure_counter = 0
74
106
  for section in doc_structure["sections"]:
75
107
  if section["type"] == "figure":
76
- figure_counter += 1
77
- self._add_figure(doc, section, figure_number=figure_counter)
108
+ is_supplementary = section.get("is_supplementary", False)
109
+ if is_supplementary:
110
+ # Supplementary figures always go inline (in SI section)
111
+ sfigure_counter += 1
112
+ self._add_figure(doc, section, figure_number=sfigure_counter, is_supplementary=True)
113
+ else:
114
+ # Main figures: collect if figures_at_end, otherwise add inline
115
+ figure_counter += 1
116
+ if figures_at_end:
117
+ collected_main_figures.append((section, figure_counter))
118
+ else:
119
+ self._add_figure(doc, section, figure_number=figure_counter, is_supplementary=False)
78
120
  else:
79
121
  self._add_section(doc, section, bibliography, include_footnotes)
80
122
 
123
+ # Add collected main figures at the end (before bibliography)
124
+ if figures_at_end and collected_main_figures:
125
+ doc.add_page_break()
126
+ doc.add_heading("Figures", level=1)
127
+ for section, fig_num in collected_main_figures:
128
+ self._add_figure(doc, section, figure_number=fig_num, is_supplementary=False)
129
+
81
130
  # Add bibliography section at the end
82
131
  if include_footnotes and bibliography:
83
132
  doc.add_page_break()
@@ -92,14 +141,14 @@ class DocxWriter:
92
141
  num_run = para.add_run(f"[{num}] ")
93
142
  num_run.bold = True
94
143
 
95
- # Add formatted bibliography text (slim format)
144
+ # Add formatted bibliography text (without DOI - added separately below)
96
145
  para.add_run(bib_entry["formatted"])
97
146
 
98
147
  # Add DOI as hyperlink with yellow highlighting if present
99
148
  if bib_entry.get("doi"):
100
149
  doi = bib_entry["doi"]
101
150
  doi_url = f"https://doi.org/{doi}" if not doi.startswith("http") else doi
102
- para.add_run(" ")
151
+ para.add_run("\nDOI: ")
103
152
  self._add_hyperlink(para, doi_url, doi_url, highlight=True)
104
153
 
105
154
  # Add spacing between entries
@@ -228,6 +277,8 @@ class DocxWriter:
228
277
  self._add_list(doc, section)
229
278
  elif section_type == "code_block":
230
279
  self._add_code_block(doc, section)
280
+ elif section_type == "comment":
281
+ self._add_comment(doc, section)
231
282
  elif section_type == "figure":
232
283
  self._add_figure(doc, section)
233
284
  elif section_type == "table":
@@ -310,11 +361,17 @@ class DocxWriter:
310
361
  run.italic = True
311
362
  if run_data.get("underline"):
312
363
  run.underline = True
364
+ if run_data.get("subscript"):
365
+ run.font.subscript = True
366
+ if run_data.get("superscript"):
367
+ run.font.superscript = True
313
368
  if run_data.get("code"):
314
369
  run.font.name = "Courier New"
315
370
  run.font.size = Pt(10)
316
371
  if run_data.get("xref"):
317
- run.font.highlight_color = WD_COLOR_INDEX.YELLOW
372
+ # Use color based on xref type (fig, sfig, stable, eq, etc.)
373
+ xref_type = run_data.get("xref_type", "cite")
374
+ run.font.highlight_color = self.get_xref_color(xref_type)
318
375
  if run_data.get("highlight_yellow"):
319
376
  run.font.highlight_color = WD_COLOR_INDEX.YELLOW
320
377
 
@@ -329,6 +386,14 @@ class DocxWriter:
329
386
  latex_content = run_data.get("latex", "")
330
387
  self._add_inline_equation(paragraph, latex_content)
331
388
 
389
+ elif run_data["type"] == "inline_comment":
390
+ # Add inline comment with gray highlighting
391
+ comment_text = run_data["text"]
392
+ run = paragraph.add_run(f"[Comment: {comment_text}]")
393
+ run.font.highlight_color = WD_COLOR_INDEX.GRAY_25
394
+ run.italic = True
395
+ run.font.size = Pt(10)
396
+
332
397
  elif run_data["type"] == "citation":
333
398
  cite_num = run_data["number"]
334
399
  # Add citation as [NN] inline with yellow highlighting
@@ -362,10 +427,16 @@ class DocxWriter:
362
427
  run.bold = True
363
428
  if run_data.get("italic"):
364
429
  run.italic = True
430
+ if run_data.get("subscript"):
431
+ run.font.subscript = True
432
+ if run_data.get("superscript"):
433
+ run.font.superscript = True
365
434
  if run_data.get("code"):
366
435
  run.font.name = "Courier New"
367
436
  if run_data.get("xref"):
368
- run.font.highlight_color = WD_COLOR_INDEX.YELLOW
437
+ # Use color based on xref type
438
+ xref_type = run_data.get("xref_type", "cite")
439
+ run.font.highlight_color = self.get_xref_color(xref_type)
369
440
  if run_data.get("highlight_yellow"):
370
441
  run.font.highlight_color = WD_COLOR_INDEX.YELLOW
371
442
  run.font.size = Pt(10)
@@ -379,11 +450,19 @@ class DocxWriter:
379
450
  # Add inline equation as Office Math
380
451
  latex_content = run_data.get("latex", "")
381
452
  self._add_inline_equation(paragraph, latex_content)
453
+ elif run_data["type"] == "inline_comment":
454
+ # Add inline comment with gray highlighting
455
+ comment_text = run_data["text"]
456
+ run = paragraph.add_run(f"[Comment: {comment_text}]")
457
+ run.font.highlight_color = WD_COLOR_INDEX.GRAY_25
458
+ run.italic = True
459
+ run.font.size = Pt(10)
382
460
  elif run_data["type"] == "citation":
383
461
  cite_num = run_data["number"]
384
462
  run = paragraph.add_run(f"[{cite_num}]")
385
463
  run.bold = True
386
464
  run.font.size = Pt(10)
465
+ run.font.highlight_color = WD_COLOR_INDEX.YELLOW
387
466
 
388
467
  def _add_code_block(self, doc: Document, section: Dict[str, Any]):
389
468
  """Add code block to document.
@@ -404,13 +483,45 @@ class DocxWriter:
404
483
  paragraph_format = paragraph.paragraph_format
405
484
  paragraph_format.left_indent = Pt(36) # Indent code blocks
406
485
 
407
- def _add_figure(self, doc: Document, section: Dict[str, Any], figure_number: int = None):
486
+ def _add_comment(self, doc: Document, section: Dict[str, Any]):
487
+ """Add comment to document with gray highlighting.
488
+
489
+ Args:
490
+ doc: Document object
491
+ section: Comment section data with 'text'
492
+ """
493
+ comment_text = section["text"]
494
+ paragraph = doc.add_paragraph()
495
+
496
+ # Add comment text with light gray highlighting to distinguish from colored xrefs
497
+ run = paragraph.add_run(f"[Comment: {comment_text}]")
498
+ run.font.highlight_color = WD_COLOR_INDEX.GRAY_25
499
+ run.italic = True
500
+ run.font.size = Pt(10)
501
+
502
+ def _check_poppler_availability(self) -> bool:
503
+ """Check if poppler is available for PDF conversion.
504
+
505
+ Returns:
506
+ True if poppler is available, False otherwise
507
+ """
508
+ from ..core.managers.dependency_manager import DependencyStatus, get_dependency_manager
509
+
510
+ manager = get_dependency_manager()
511
+ result = manager.check_dependency("pdftoppm")
512
+
513
+ return result.status == DependencyStatus.AVAILABLE
514
+
515
+ def _add_figure(
516
+ self, doc: Document, section: Dict[str, Any], figure_number: int = None, is_supplementary: bool = False
517
+ ):
408
518
  """Add figure to document with caption.
409
519
 
410
520
  Args:
411
521
  doc: Document object
412
522
  section: Figure section data with 'path', 'caption', 'label'
413
523
  figure_number: Figure number (1-indexed)
524
+ is_supplementary: Whether this is a supplementary figure
414
525
  """
415
526
  figure_path = Path(section["path"])
416
527
  caption = section.get("caption", "")
@@ -425,9 +536,31 @@ class DocxWriter:
425
536
  if not figure_path.exists():
426
537
  logger.warning(f"Figure file not found: {figure_path}")
427
538
  elif figure_path.suffix.lower() == ".pdf":
428
- # Convert PDF to image
429
- img_source = convert_pdf_to_image(figure_path)
430
- logger.debug(f" PDF converted: {img_source is not None}")
539
+ # Check poppler availability first (cached after first check)
540
+ if not hasattr(self, "_poppler_checked"):
541
+ self._poppler_available = self._check_poppler_availability()
542
+ self._poppler_checked = True
543
+
544
+ if not self._poppler_available:
545
+ logger.warning(
546
+ "Poppler not installed - PDF figures will be shown as placeholders. "
547
+ "Install with: brew install poppler (macOS) or sudo apt install poppler-utils (Linux)"
548
+ )
549
+
550
+ if self._poppler_available:
551
+ # Convert PDF to image
552
+ try:
553
+ from pdf2image.exceptions import PDFInfoNotInstalledError, PopplerNotInstalledError
554
+
555
+ img_source = convert_pdf_to_image(figure_path)
556
+ logger.debug(f" PDF converted: {img_source is not None}")
557
+ except (PopplerNotInstalledError, PDFInfoNotInstalledError) as e:
558
+ logger.error(f"Poppler utilities not found: {e}")
559
+ img_source = None
560
+ # Update our cached status
561
+ self._poppler_available = False
562
+ else:
563
+ img_source = None
431
564
  elif figure_path.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".bmp"]:
432
565
  # Use image file directly
433
566
  img_source = str(figure_path)
@@ -435,19 +568,45 @@ class DocxWriter:
435
568
  logger.warning(f"Unsupported image format: {figure_path.suffix}")
436
569
 
437
570
  if img_source:
438
- # Add image
571
+ # Add image with proper sizing to fit page
439
572
  try:
440
- doc.add_picture(img_source, width=Inches(6))
441
- logger.debug(f"Embedded figure: {figure_path}")
573
+ from PIL import Image as PILImage
574
+
575
+ # Get image dimensions
576
+ with PILImage.open(img_source) as img:
577
+ img_width, img_height = img.size
578
+ aspect_ratio = img_width / img_height
579
+
580
+ # Page dimensions with margins (Letter size: 8.5 x 11 inches, 1 inch margins)
581
+ max_width = Inches(6.5) # 8.5 - 2*1
582
+ max_height = Inches(9) # 11 - 2*1
583
+
584
+ # Add figure centered
585
+ # Note: add_picture() creates a paragraph automatically, but we need to add it explicitly
586
+ # to control alignment
587
+ fig_para = doc.add_paragraph()
588
+ fig_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
589
+
590
+ # Calculate optimal size maintaining aspect ratio
591
+ if aspect_ratio > (6.5 / 9): # Wide image - constrain by width
592
+ run = fig_para.add_run()
593
+ run.add_picture(img_source, width=max_width)
594
+ else: # Tall image - constrain by height
595
+ run = fig_para.add_run()
596
+ run.add_picture(img_source, height=max_height)
597
+
598
+ logger.debug(f"Embedded figure: {figure_path} ({img_width}x{img_height})")
442
599
  except Exception as e:
443
600
  logger.warning(f"Failed to embed figure {figure_path}: {e}")
444
- # Add placeholder text
601
+ # Add placeholder text (centered)
445
602
  p = doc.add_paragraph()
603
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
446
604
  run = p.add_run(f"[Figure: {figure_path.name}]")
447
605
  run.italic = True
448
606
  else:
449
- # Add placeholder if embedding failed
607
+ # Add placeholder if embedding failed (centered)
450
608
  p = doc.add_paragraph()
609
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
451
610
  run = p.add_run(f"[Figure: {figure_path.name}]")
452
611
  run.italic = True
453
612
  logger.warning(f"Could not embed figure: {figure_path}")
@@ -459,9 +618,12 @@ class DocxWriter:
459
618
  # Add small space before caption to separate from figure
460
619
  caption_para.paragraph_format.space_before = Pt(3)
461
620
 
462
- # Format as "Figure number: "
621
+ # Format as "Figure number: " or "Supp. Fig. number: "
463
622
  if figure_number:
464
- run = caption_para.add_run(f"Figure {figure_number}: ")
623
+ if is_supplementary:
624
+ run = caption_para.add_run(f"Supp. Fig. S{figure_number}. ")
625
+ else:
626
+ run = caption_para.add_run(f"Fig. {figure_number}. ")
465
627
  run.bold = True
466
628
  run.font.size = Pt(7)
467
629
  else:
@@ -487,21 +649,35 @@ class DocxWriter:
487
649
  run.bold = True
488
650
  if run_data.get("italic"):
489
651
  run.italic = True
652
+ if run_data.get("subscript"):
653
+ run.font.subscript = True
654
+ if run_data.get("superscript"):
655
+ run.font.superscript = True
490
656
  if run_data.get("code"):
491
657
  run.font.name = "Courier New"
492
658
  if run_data.get("xref"):
493
- run.font.highlight_color = WD_COLOR_INDEX.YELLOW
659
+ # Use color based on xref type
660
+ xref_type = run_data.get("xref_type", "cite")
661
+ run.font.highlight_color = self.get_xref_color(xref_type)
494
662
  if run_data.get("highlight_yellow"):
495
663
  run.font.highlight_color = WD_COLOR_INDEX.YELLOW
496
664
  elif run_data["type"] == "inline_equation":
497
665
  # Add inline equation as Office Math
498
666
  latex_content = run_data.get("latex", "")
499
667
  self._add_inline_equation(caption_para, latex_content)
668
+ elif run_data["type"] == "inline_comment":
669
+ # Add inline comment with gray highlighting
670
+ comment_text = run_data["text"]
671
+ run = caption_para.add_run(f"[Comment: {comment_text}]")
672
+ run.font.highlight_color = WD_COLOR_INDEX.GRAY_25
673
+ run.italic = True
674
+ run.font.size = Pt(7)
500
675
  elif run_data["type"] == "citation":
501
676
  cite_num = run_data["number"]
502
677
  run = caption_para.add_run(f"[{cite_num}]")
503
678
  run.bold = True
504
679
  run.font.size = Pt(7)
680
+ run.font.highlight_color = WD_COLOR_INDEX.YELLOW
505
681
 
506
682
  # Add spacing after figure (reduced from 12 to 6 for compactness)
507
683
  caption_para.paragraph_format.space_after = Pt(6)
@@ -567,10 +743,16 @@ class DocxWriter:
567
743
  run.italic = True
568
744
  if run_data.get("underline"):
569
745
  run.underline = True
746
+ if run_data.get("subscript"):
747
+ run.font.subscript = True
748
+ if run_data.get("superscript"):
749
+ run.font.superscript = True
570
750
  if run_data.get("code"):
571
751
  run.font.name = "Courier New"
572
752
  if run_data.get("xref"):
573
- run.font.highlight_color = WD_COLOR_INDEX.YELLOW
753
+ # Use color based on xref type
754
+ xref_type = run_data.get("xref_type", "cite")
755
+ run.font.highlight_color = self.get_xref_color(xref_type)
574
756
 
575
757
  # Add table caption if present
576
758
  caption = section.get("caption")
@@ -581,16 +763,28 @@ class DocxWriter:
581
763
  # Add small space before caption to separate from table
582
764
  caption_para.paragraph_format.space_before = Pt(3)
583
765
 
584
- # Determine table number from label (e.g., "stable:structural_models" -> "Supp. Table 1")
766
+ # Determine table number from label using table_map
585
767
  if label and label.startswith("stable:"):
586
- # Count how many supplementary tables we've seen so far
587
- # For now, we'll just format as "Supp. Table: caption"
588
- # A more sophisticated approach would track table numbers
589
- run = caption_para.add_run("Supp. Table: ")
768
+ # Extract label name (e.g., "stable:parameters" -> "parameters")
769
+ label_name = label.split(":", 1)[1] if ":" in label else label
770
+ # Look up number in table_map
771
+ table_num = self.table_map.get(label_name)
772
+ if table_num:
773
+ run = caption_para.add_run(f"Supp. Table S{table_num}. ")
774
+ else:
775
+ # Fallback if label not in map
776
+ run = caption_para.add_run("Supp. Table: ")
590
777
  run.bold = True
591
778
  run.font.size = Pt(7)
592
779
  elif label and label.startswith("table:"):
593
- run = caption_para.add_run("Table: ")
780
+ # Extract label name for main tables
781
+ label_name = label.split(":", 1)[1] if ":" in label else label
782
+ # Look up number in table_map (though main tables may not be in map)
783
+ table_num = self.table_map.get(label_name)
784
+ if table_num:
785
+ run = caption_para.add_run(f"Table {table_num}. ")
786
+ else:
787
+ run = caption_para.add_run("Table: ")
594
788
  run.bold = True
595
789
  run.font.size = Pt(7)
596
790
 
@@ -607,10 +801,16 @@ class DocxWriter:
607
801
  run.italic = True
608
802
  if run_data.get("underline"):
609
803
  run.underline = True
804
+ if run_data.get("subscript"):
805
+ run.font.subscript = True
806
+ if run_data.get("superscript"):
807
+ run.font.superscript = True
610
808
  if run_data.get("code"):
611
809
  run.font.name = "Courier New"
612
810
  if run_data.get("xref"):
613
- run.font.highlight_color = WD_COLOR_INDEX.YELLOW
811
+ # Use color based on xref type
812
+ xref_type = run_data.get("xref_type", "cite")
813
+ run.font.highlight_color = self.get_xref_color(xref_type)
614
814
 
615
815
  # Add spacing after table (reduced from 12 to 6 for compactness)
616
816
  caption_para.paragraph_format.space_after = Pt(6)
@@ -286,6 +286,7 @@ output/
286
286
  .rxiv_cache/
287
287
  *.pdf
288
288
  *.docx
289
+ *.zip
289
290
  *.log
290
291
  *.aux
291
292
  *.fdb_latexmk
@@ -425,10 +425,10 @@
425
425
  {}
426
426
  {0em}
427
427
  {#1}
428
- \titlespacing*{\section}{0pc}{3ex \@plus4pt \@minus3pt}{5pt}
429
- \titlespacing*{\subsection}{0pc}{2.5ex \@plus3pt \@minus2pt}{2pt}
430
- \titlespacing*{\subsubsection}{0pc}{2ex \@plus2.5pt \@minus1.5pt}{2pt}
431
- \titlespacing*{\paragraph}{0pc}{1.5ex \@plus2pt \@minus1pt}{12pt}
428
+ \titlespacing*{\section}{0pc}{1.5ex \@plus2pt \@minus1pt}{2pt}
429
+ \titlespacing*{\subsection}{0pc}{1.2ex \@plus2pt \@minus1pt}{1pt}
430
+ \titlespacing*{\subsubsection}{0pc}{1ex \@plus1pt \@minus0.5pt}{1pt}
431
+ \titlespacing*{\paragraph}{0pc}{0.8ex \@plus1pt \@minus0.5pt}{8pt}
432
432
 
433
433
  %% Figure caption style
434
434
  \DeclareCaptionFormat{smallformat}{\normalfont\sffamily\fontsize{7}{9}\selectfont#1#2#3}
@@ -536,7 +536,7 @@
536
536
  \def\abstract{%\vspace{-.3em}
537
537
  \ifbfabstract\small\bfseries\else\footnotesize\fi}
538
538
 
539
- \def\endabstract{\vspace{1.0em}\par\normalsize}
539
+ \def\endabstract{\vspace{0.2em}\par\normalsize}
540
540
 
541
541
  % \def\keywords{%\vspace{-.3em}
542
542
  % \noindent\ifbfabstract\Affilfont\bfseries\itshape\else\footnotesize\fi Keywords --- }
@@ -549,44 +549,44 @@
549
549
 
550
550
  \def\keywords{%\vspace{-.3em}
551
551
  \ifbfabstract\Affilfont\bfseries\else\footnotesize\fi}
552
- \def\endkeywords{\vspace{0.3em}\par\normalsize}
552
+ \def\endkeywords{\vspace{-0.3em}\par\normalsize}
553
553
 
554
554
  \newcommand{\at}{\makeatletter @\makeatother}
555
- \def\corrauthor{%\vspace{-.3em}
555
+ \def\corrauthor{\vspace{-.2em}
556
556
  \noindent\ifbfabstract\Affilfont\bfseries\else\footnotesize\fi Correspondence:\itshape}
557
- \def\endcorrauthor{\vspace{0.3em}\par\normalsize}
557
+ \def\endcorrauthor{\vspace{0.1em}\par\normalsize}
558
558
 
559
- \def\acknowledgements{\vspace{.3em}
560
- \Affilfont\bfseries ACKNOWLEDGEMENTS\par\mdseries}
561
- \def\endacknowledgements{\vspace{0.3em}\par\normalsize}
559
+ \def\acknowledgements{\vspace{.1em}
560
+ \Affilfont\bfseries ACKNOWLEDGEMENTS\par\vspace{-0.2em}\mdseries}
561
+ \def\endacknowledgements{\vspace{0.1em}\par\normalsize}
562
562
 
563
- \def\manuscriptinfo{\vspace{.3em}
564
- \Affilfont\bfseries ABOUT THIS MANUSCRIPT\par\mdseries}
565
- \def\endmanuscriptinfo{\vspace{0.3em}\par\normalsize}
563
+ \def\manuscriptinfo{\vspace{.1em}
564
+ \Affilfont\bfseries ABOUT THIS MANUSCRIPT\par\vspace{-0.2em}\mdseries}
565
+ \def\endmanuscriptinfo{\vspace{0.1em}\par\normalsize}
566
566
 
567
- \def\contributions{\vspace{.3em}
568
- \Affilfont\bfseries AUTHOR CONTRIBUTIONS\par\mdseries}
569
- \def\endcontributions{\vspace{0.3em}\par\normalsize}
567
+ \def\contributions{\vspace{.1em}
568
+ \Affilfont\bfseries AUTHOR CONTRIBUTIONS\par\vspace{-0.2em}\mdseries}
569
+ \def\endcontributions{\vspace{0.1em}\par\normalsize}
570
570
 
571
- \def\data{\vspace{.3em}
572
- \Affilfont\bfseries DATA AVAILABILITY\par\mdseries}
573
- \def\enddata{\vspace{0.3em}\par\normalsize}
571
+ \def\data{\vspace{.1em}
572
+ \Affilfont\bfseries DATA AVAILABILITY\par\vspace{-0.2em}\mdseries}
573
+ \def\enddata{\vspace{0.1em}\par\normalsize}
574
574
 
575
- \def\code{\vspace{.3em}
576
- \Affilfont\bfseries CODE AVAILABILITY\par\mdseries}
577
- \def\endcode{\vspace{0.3em}\par\normalsize}
575
+ \def\code{\vspace{.1em}
576
+ \Affilfont\bfseries CODE AVAILABILITY\par\vspace{-0.2em}\mdseries}
577
+ \def\endcode{\vspace{0.1em}\par\normalsize}
578
578
 
579
- \def\funding{\vspace{.3em}
580
- \Affilfont\bfseries FUNDING\par\mdseries}
581
- \def\endfunding{\vspace{0.3em}\par\normalsize}
579
+ \def\funding{\vspace{.1em}
580
+ \Affilfont\bfseries FUNDING\par\vspace{-0.2em}\mdseries}
581
+ \def\endfunding{\vspace{0.1em}\par\normalsize}
582
582
 
583
- \def\interests{\vspace{.3em}
584
- \Affilfont\bfseries COMPETING FINANCIAL INTERESTS\par\mdseries}
585
- \def\endinterests{\vspace{0.3em}\par\normalsize}
583
+ \def\interests{\vspace{.1em}
584
+ \Affilfont\bfseries COMPETING FINANCIAL INTERESTS\par\vspace{-0.2em}\mdseries}
585
+ \def\endinterests{\vspace{0.1em}\par\normalsize}
586
586
 
587
- \def\exauthor{\vspace{.3em}
588
- \Affilfont\bfseries EXTENDED AUTHOR INFORMATION \par\mdseries}
589
- \def\endexauthor{\vspace{0.3em}\par\normalsize}
587
+ \def\exauthor{\vspace{.1em}
588
+ \Affilfont\bfseries EXTENDED AUTHOR INFORMATION \par\vspace{-0.2em}\mdseries}
589
+ \def\endexauthor{\vspace{0.1em}\par\normalsize}
590
590
 
591
591
  %% Custom environment for extended author info with reduced indentation
592
592
  \newenvironment{extendedauthorlist}{%
@@ -714,5 +714,5 @@
714
714
 
715
715
  % Paragraph formatting: no indentation, space between paragraphs
716
716
  \parindent=0pt
717
- \parskip=0.75\baselineskip plus 3pt minus 2pt
717
+ \parskip=0.35\baselineskip plus 2pt minus 1pt
718
718
  \endinput
@@ -81,21 +81,41 @@ def generate_bst_file(format_type: str, output_dir: Path) -> Path:
81
81
  except IOError as e:
82
82
  raise IOError(f"Failed to read template .bst file: {e}") from e
83
83
 
84
- # Replace the format string on line 222
84
+ # Replace the format string in the format.names function (line ~222)
85
85
  # The line looks like: s nameptr "{ff~}{vv~}{ll}{, jj}" format.name$ 't :=
86
- # We need to replace the format string in quotes
87
- pattern = r'(s\s+nameptr\s+")([^"]+)("\s+format\.name\$)'
86
+ # We need to replace ONLY in format.names, NOT in format.full.names
87
+ #
88
+ # Strategy: Match the FUNCTION {format.names} block specifically
89
+ # This ensures we don't modify format.full.names which is used for citation labels
90
+ #
91
+ # Pattern explanation:
92
+ # 1. Match "FUNCTION {format.names}" to find the start of the function
93
+ # 2. Use non-greedy match (.*?) to capture until we find our target line
94
+ # 3. Match and capture the format string in quotes
95
+ # 4. This avoids matching format.full.names which appears later in the file
96
+ pattern = r'(FUNCTION\s+\{format\.names\}.*?s\s+nameptr\s+")([^"]+)("\s+format\.name\$)'
88
97
  replacement = rf"\1{format_string}\3"
89
98
 
90
- modified_content, num_subs = re.subn(pattern, replacement, bst_content)
99
+ modified_content, num_subs = re.subn(pattern, replacement, bst_content, flags=re.DOTALL)
91
100
 
92
101
  if num_subs == 0:
93
- logger.warning("No format string pattern found in .bst file. The .bst file may have been modified.")
102
+ logger.warning(
103
+ "No format string pattern found in format.names function. "
104
+ "The .bst file structure may have changed. "
105
+ "Citation formatting may not work as expected."
106
+ )
94
107
  # Still write the file but log a warning
95
108
  elif num_subs > 1:
96
- logger.warning(
97
- f"Found {num_subs} format string patterns in .bst file. Expected only 1. All have been replaced."
109
+ # This should never happen with the new pattern, but keep the check
110
+ logger.error(
111
+ f"Found {num_subs} matches in .bst file. Expected exactly 1. "
112
+ "This indicates an unexpected .bst file structure. "
113
+ "Please report this issue."
98
114
  )
115
+ raise ValueError(f"Unexpected .bst file structure: found {num_subs} matches for format.names, expected 1")
116
+ else:
117
+ # Success - exactly one match
118
+ logger.debug(f"Successfully updated format.names function with format '{format_type}'")
99
119
 
100
120
  # Create output directory if it doesn't exist
101
121
  output_dir = Path(output_dir)