2121- If include_forthcoming: true, items missing volume/issue render under a dedicated:
2222 ## Forthcoming
2323 section.
24- - Re-running the script:
25- - **always refreshes "Forthcoming"** (if enabled)
26- - **appends only strictly newer numeric issues** than the latest one already in the file
27- - Can also create a new TOC file interactively with `--new-toc`, and **immediately updates it**.
24+ - Can also create a new TOC file interactively with `--new-toc`, and immediately updates it.
2825
2926Requirements
3027------------
3532Usage
3633-----
3734# Update/append TOCs in all *.md files with valid front matter
38- python md_toc_generator.py
35+ toc-sync
3936
4037# Limit to a file/glob
41- python md_toc_generator.py --only journal_toc.md
42- python md_toc_generator.py --only "toc-*.md"
38+ toc-sync --only journal_toc.md
39+ toc-sync --only "toc-*.md"
4340
44- # Force a full rewrite (not just append newer issues )
45- python md_toc_generator.py --rewrite
41+ # Force a full rewrite (not just incremental insert at top )
42+ toc-sync --rewrite
4643
4744# Create a new TOC interactively (ISSN lookup via Crossref) and update it
48- python md_toc_generator.py --new-toc \
45+ toc-sync --new-toc \
4946 --out toc-mis-quarterly.md \
5047 --new-pdfs-dir "/home/user/papers" \
5148 --new-format title_author_doi \
6360from collections import OrderedDict
6461from dataclasses import dataclass
6562from pathlib import Path
66- from typing import Any
67- from typing import Dict
68- from typing import Iterable
69- from typing import List
70- from typing import Optional
71- from typing import Tuple
63+ from typing import Any , Dict , Iterable , List , Optional , Tuple
7264
7365import colrev .record .record
7466
@@ -259,7 +251,7 @@ def _iter_pairs_desc(
259251
260252
261253# -----------------------------
262- # Incremental update (append only NEWER numeric issues )
254+ # Incremental update (insert at TOP after header )
263255# + always refresh Forthcoming; normalize legacy headings
264256# -----------------------------
265257
@@ -338,6 +330,22 @@ def _parse_existing_headings(
338330 return present , latest_key , has_forthcoming
339331
340332
333+ def _render_forthcoming_block (records : List [dict ], cfg : TocConfig ) -> List [str ]:
334+ lines = ["## Forthcoming\n \n " ]
335+ for d in records :
336+ lines .append (_record_to_md_line (d , cfg ) + "\n " )
337+ lines .append ("\n " )
338+ return lines
339+
340+
341+ def _render_issue_block (vol : str , iss : str , items : List [dict ], cfg : TocConfig ) -> List [str ]:
342+ lines = [f"## Volume { vol } - Number { iss } \n \n " ]
343+ for d in items :
344+ lines .append (_record_to_md_line (d , cfg ) + "\n " )
345+ lines .append ("\n " )
346+ return lines
347+
348+
341349def _write_forthcoming_section (
342350 f : typing .TextIO , records : List [dict ], cfg : TocConfig
343351) -> None :
@@ -390,6 +398,12 @@ def _write_full_markdown(
390398def _append_incremental (
391399 grouped : Dict [str , Dict [str , List [dict ]]], out_path : Path , cfg : TocConfig
392400) -> None :
401+ """
402+ Incremental update that:
403+ - Removes any existing 'Forthcoming' block and re-inserts a fresh one
404+ - Inserts strictly newer issues (vs. latest existing) right AFTER the header
405+ - Keeps existing content intact below the inserted block
406+ """
393407 if not out_path .exists ():
394408 _write_full_markdown (grouped , out_path , cfg )
395409 return
@@ -398,9 +412,12 @@ def _append_incremental(
398412 lines = f .readlines ()
399413 lines = _normalize_legacy_forthcoming (lines )
400414
401- present_pairs , latest_existing , has_forthcoming = _parse_existing_headings (lines )
415+ present_pairs , latest_existing , _has_forthcoming = _parse_existing_headings (lines )
416+
417+ # ---- Build insertion block (to be placed after header)
418+ insertion : List [str ] = []
402419
403- # ---- 1) Always refresh Forthcoming (when configured and available)
420+ # 1) Always refresh Forthcoming (when configured and available)
404421 forthcoming_records = None
405422 if (
406423 cfg .include_forthcoming
@@ -415,25 +432,17 @@ def _append_incremental(
415432 if rng is not None :
416433 start , end = rng
417434 del lines [start :end ]
418- if lines and not lines [- 1 ].endswith ("\n " ):
419- lines [- 1 ] += "\n "
420-
421- # Write back without Forthcoming, then append fresh Forthcoming section
422- with out_path .open ("w" , encoding = "utf-8" ) as f :
423- f .writelines (lines )
424- _write_forthcoming_section (f , forthcoming_records , cfg )
425-
426- # Reload lines for step 2
427- with out_path .open ("r" , encoding = "utf-8" ) as f :
428- lines = f .readlines ()
435+ # Add fresh forthcoming to insertion block
436+ insertion .extend (_render_forthcoming_block (forthcoming_records , cfg ))
429437
430- # ---- 2) Append strictly newer numbered issues
438+ # 2) Determine strictly newer numbered issues
431439 all_pairs_sorted : List [Tuple [str , str ]] = []
432440 for vol , issues in grouped .items ():
433441 for iss in issues .keys ():
434442 if vol == FORTHCOMING_VOL and iss == FORTHCOMING_ISS :
435- continue # handled already
443+ continue # forthcoming handled above
436444 all_pairs_sorted .append ((vol , iss ))
445+ # sort ASC by (vol, iss)
437446 all_pairs_sorted .sort (key = lambda p : _pair_sort_key (* p ))
438447
439448 to_add : List [Tuple [str , str ]] = []
@@ -445,17 +454,26 @@ def _append_incremental(
445454 if key > latest_existing :
446455 to_add .append ((vol , iss ))
447456
448- if not to_add :
457+ # Insert newer issues in NEWEST-FIRST order at the top
458+ to_add .sort (key = lambda p : _pair_sort_key (* p ), reverse = True )
459+
460+ for vol , iss in to_add :
461+ insertion .extend (_render_issue_block (vol , iss , grouped [vol ][iss ], cfg ))
462+
463+ # If nothing to insert, nothing to do
464+ if not insertion :
449465 return
450466
451- with out_path .open ("a" , encoding = "utf-8" ) as f :
452- if not lines or not lines [- 1 ].endswith ("\n " ):
453- f .write ("\n " )
454- for vol , iss in to_add :
455- f .write (f"## Volume { vol } - Number { iss } \n \n " )
456- for d in grouped [vol ][iss ]:
457- f .write (_record_to_md_line (d , cfg ) + "\n " )
458- f .write ("\n " )
467+ # ---- Find header end (first "## " or EOF if no headings yet)
468+ first_h2_idx = next ((i for i , ln in enumerate (lines ) if ln .startswith ("## " )), len (lines ))
469+
470+ # Ensure header ends with a blank line
471+ if first_h2_idx == len (lines ) or (first_h2_idx > 0 and not lines [first_h2_idx - 1 ].endswith ("\n " )):
472+ pass # we'll just splice raw; existing newlines are preserved
473+
474+ new_lines = lines [:first_h2_idx ] + insertion + lines [first_h2_idx :]
475+
476+ out_path .write_text ("" .join (new_lines ), encoding = "utf-8" )
459477
460478
461479# -----------------------------
@@ -669,7 +687,7 @@ def main() -> None:
669687 parser .add_argument (
670688 "--rewrite" ,
671689 action = "store_true" ,
672- help = "Rewrite full TOC instead of appending only newer issues " ,
690+ help = "Rewrite full TOC instead of incremental insert-at-top " ,
673691 )
674692
675693 # New TOC creation options
0 commit comments