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.
- rxiv_maker/__version__.py +1 -1
- rxiv_maker/cli/commands/build.py +7 -0
- rxiv_maker/cli/commands/docx.py +74 -0
- rxiv_maker/cli/framework/workflow_commands.py +66 -2
- rxiv_maker/converters/citation_processor.py +5 -3
- rxiv_maker/core/managers/config_manager.py +1 -0
- rxiv_maker/core/managers/dependency_manager.py +12 -0
- rxiv_maker/exporters/docx_citation_mapper.py +99 -0
- rxiv_maker/exporters/docx_content_processor.py +128 -30
- rxiv_maker/exporters/docx_exporter.py +179 -24
- rxiv_maker/exporters/docx_writer.py +227 -27
- rxiv_maker/templates/registry.py +1 -0
- rxiv_maker/tex/style/rxiv_maker_style.cls +33 -33
- rxiv_maker/utils/bst_generator.py +27 -7
- rxiv_maker/utils/docx_helpers.py +62 -3
- rxiv_maker/utils/pdf_splitter.py +116 -0
- {rxiv_maker-1.16.7.dist-info → rxiv_maker-1.17.0.dist-info}/METADATA +2 -1
- {rxiv_maker-1.16.7.dist-info → rxiv_maker-1.17.0.dist-info}/RECORD +21 -20
- {rxiv_maker-1.16.7.dist-info → rxiv_maker-1.17.0.dist-info}/WHEEL +0 -0
- {rxiv_maker-1.16.7.dist-info → rxiv_maker-1.17.0.dist-info}/entry_points.txt +0 -0
- {rxiv_maker-1.16.7.dist-info → rxiv_maker-1.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
-
#
|
|
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
|
-
|
|
77
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
766
|
+
# Determine table number from label using table_map
|
|
585
767
|
if label and label.startswith("stable:"):
|
|
586
|
-
#
|
|
587
|
-
|
|
588
|
-
#
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
rxiv_maker/templates/registry.py
CHANGED
|
@@ -425,10 +425,10 @@
|
|
|
425
425
|
{}
|
|
426
426
|
{0em}
|
|
427
427
|
{#1}
|
|
428
|
-
\titlespacing*{\section}{0pc}{
|
|
429
|
-
\titlespacing*{\subsection}{0pc}{
|
|
430
|
-
\titlespacing*{\subsubsection}{0pc}{
|
|
431
|
-
\titlespacing*{\paragraph}{0pc}{
|
|
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{
|
|
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{
|
|
555
|
+
\def\corrauthor{\vspace{-.2em}
|
|
556
556
|
\noindent\ifbfabstract\Affilfont\bfseries\else\footnotesize\fi Correspondence:\itshape}
|
|
557
|
-
\def\endcorrauthor{\vspace{0.
|
|
557
|
+
\def\endcorrauthor{\vspace{0.1em}\par\normalsize}
|
|
558
558
|
|
|
559
|
-
\def\acknowledgements{\vspace{.
|
|
560
|
-
\Affilfont\bfseries ACKNOWLEDGEMENTS\par\mdseries}
|
|
561
|
-
\def\endacknowledgements{\vspace{0.
|
|
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{.
|
|
564
|
-
\Affilfont\bfseries ABOUT THIS MANUSCRIPT\par\mdseries}
|
|
565
|
-
\def\endmanuscriptinfo{\vspace{0.
|
|
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{.
|
|
568
|
-
\Affilfont\bfseries AUTHOR CONTRIBUTIONS\par\mdseries}
|
|
569
|
-
\def\endcontributions{\vspace{0.
|
|
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{.
|
|
572
|
-
\Affilfont\bfseries DATA AVAILABILITY\par\mdseries}
|
|
573
|
-
\def\enddata{\vspace{0.
|
|
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{.
|
|
576
|
-
\Affilfont\bfseries CODE AVAILABILITY\par\mdseries}
|
|
577
|
-
\def\endcode{\vspace{0.
|
|
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{.
|
|
580
|
-
\Affilfont\bfseries FUNDING\par\mdseries}
|
|
581
|
-
\def\endfunding{\vspace{0.
|
|
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{.
|
|
584
|
-
\Affilfont\bfseries COMPETING FINANCIAL INTERESTS\par\mdseries}
|
|
585
|
-
\def\endinterests{\vspace{0.
|
|
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{.
|
|
588
|
-
\Affilfont\bfseries EXTENDED AUTHOR INFORMATION \par\mdseries}
|
|
589
|
-
\def\endexauthor{\vspace{0.
|
|
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.
|
|
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
|
|
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
|
|
87
|
-
|
|
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(
|
|
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
|
-
|
|
97
|
-
|
|
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)
|