Utensil's Zettelkasten-style forest of evergreen notes on math and tech. utensil.tngl.sh/forest/
0

Configure Feed

Select the types of activity you want to include in your feed.

forest / split_bib.py
6.6 kB 190 lines
1#!/usr/bin/env -S uv run 2# /// script 3# requires-python = ">=3.11,<3.12" 4# dependencies = ["bibtexparser"] 5# /// 6import json 7import os 8import pathlib 9import shutil 10import subprocess 11import sys 12from pathlib import Path 13import bibtexparser 14from bibtexparser.bwriter import BibTexWriter, SortingStrategy 15import re 16 17BIB_TEX_WRITER = BibTexWriter() 18BIB_TEX_WRITER.display_order = [ 19 'title', 'author', 'year', 'date', 'isbn', 'doi', 'url', 'urldate', 'howpublished', 20 'journal', 'journaltitle', 'booktitle', 'edition', 'series', 21 'editor', 'volume', 'number', 'pages', 'publisher', 'institution', 'address' 22] 23BIB_TEX_WRITER.display_order_sorting = SortingStrategy.PRESERVE # ALPHABETICAL_ASC ALPHABETICAL_DESC 24 25 26# Get the script directory 27script_dir = Path(__file__).resolve().parent 28# Set the project root directory 29project_root = script_dir 30 31# Set the bib directory 32tex_dir = project_root / 'tex' 33bib_dir = project_root / 'trees' / 'refs' 34# Set the bib file name 35bib_filename = sys.argv[1] if len(sys.argv) > 1 else 'references' 36# create a directory to store the split files 37generated_dir = bib_dir / 'generated' 38os.makedirs(generated_dir, exist_ok=True) 39 40bib_file = pathlib.Path(tex_dir) / f'{bib_filename}.bib' 41csljson_file = generated_dir / f'{bib_filename}.json' 42 43print(f'📚 {bib_file.relative_to(project_root)} -> {csljson_file.relative_to(project_root)}') 44# Run the pandoc command 45subprocess.run(['pandoc', f'{bib_file}', '-s', '-f', 'biblatex', '-t', 'csljson', '-o', csljson_file], check=True) 46 47# Load the BibTeX file 48with open(bib_file, encoding='utf-8') as f: 49 bib_db = bibtexparser.load(f) 50 51csljson_file_name = csljson_file.stem 52 53with open(csljson_file, encoding='utf-8') as f: 54 references = json.load(f) 55 56TREE_TEMPLATE = r"""% {bib_filenames} 57\title{{{title}}} 58\date{{{date}}} 59{authors} 60\taxon{{reference}} 61{meta_doi}{meta_external} 62\meta{{bibtex}}{{\startverb 63{original_bibtex}\stopverb}} 64""" 65 66def format_author(author): 67 if 'literal' in author: 68 return author['literal'] 69 elif 'given' in author and 'family' in author: 70 author_name = [] 71 author_name.append(author['given']) 72 if 'dropping-particle' in author: 73 author_name.append(author['dropping-particle']) 74 author_name.append(author['family']) 75 return ' '.join(author_name) 76 else: 77 return '' 78 79# format a number with leading zeros 80def format_number(number, length=2): 81 format_string = "{:0" + str(length) + "}" 82 return format_string.format(number) 83 84def format_date(date_parts): 85 return '-'.join([format_number(part) for part in date_parts]) 86 87def format_doi(reference): 88 doi = reference.get('DOI', None) 89 return f'\\meta{{doi}}{{{doi}}}\n' if doi else '' 90 91def format_external(reference): 92 url = reference.get('URL', None) 93 if url is None: 94 publisher = reference.get('publisher', None) 95 # if publisher is a URL, use regex 96 if publisher is not None: 97 url = re.search('(https?://[^\s]+)', publisher) 98 if url is not None: 99 url = url.group(1) 100 print(f'🔵 {url}') 101 102 return f'\\meta{{external}}{{{url}}}\n' if url else '' 103 104def parse_frontmatter(line): 105 # strip beginning whitespace and % 106 line = line.lstrip().lstrip('%').lstrip() 107 # try parse it as JSON 108 try: 109 ret = json.loads(line) 110 # check if it's an array 111 if isinstance(ret, list): 112 return ret 113 else: 114 return [] 115 except json.JSONDecodeError: 116 return [] 117 118# print(f'📚 Splitting {csljson_file.relative_to(project_root)}') 119csl_file = bib_dir / 'forest.csl' 120for i, reference in enumerate(references): 121 citekey = reference['id'] 122 csljson_file_i = generated_dir / f'{citekey}.json' 123 tree_file_i = bib_dir / f'{citekey}.tree' 124 print(f' {csljson_file_i.relative_to(project_root)} -> {tree_file_i.relative_to(project_root)}') 125 126 bibtex_entry = bib_db.entries_dict[citekey] 127 entrydb = bibtexparser.bibdatabase.BibDatabase() 128 entrydb.entries = [bibtex_entry] 129 original_bibtex = bibtexparser.dumps(entrydb, BIB_TEX_WRITER) 130 131 with open(csljson_file_i, 'w', encoding='utf-8') as f: 132 # reference['title'] = reference.get('title', '').replace('\n', ' ') 133 # reference['title_short'] = reference.get('title', '').split(' ')[0].lower() 134 135 # print(original_bibtex) 136 reference['original_bibtex'] = original_bibtex 137 json.dump([reference], f) 138 f.flush() 139 140 bib_filenames_i = [bib_filename] 141 142 # if tree_file_i exists 143 if os.path.exists(tree_file_i): 144 # read the first line 145 with open(tree_file_i, 'r', encoding='utf-8') as f: 146 first_line = f.readline() 147 first_line_json = parse_frontmatter(first_line) 148 # remove bib_filename from it 149 bib_filenames_i = [ 150 filename for filename in first_line_json 151 if filename != bib_filename 152 ] 153 # add bib_filename to the end of it 154 bib_filenames_i.append(bib_filename) 155 # sort the list 156 bib_filenames_i.sort() 157 158 # detect duplication 159 if len(bib_filenames_i) > 1: 160 # {tree_file_i.relative_to(project_root)}: 161 print(f'🟡 {bib_filenames_i}') 162 163 formatted = TREE_TEMPLATE.format( 164 bib_filenames=json.dumps(bib_filenames_i), 165 title=reference['title'], 166 date=format_date(reference['issued']['date-parts'][0]), 167 meta_doi=format_doi(reference), 168 meta_external=format_external(reference), 169 authors=''.join([f'\\author{{{format_author(author)}}}' for author in reference['author']]), 170 original_bibtex=original_bibtex) 171 172 if tree_file_i.exists(): 173 with open(tree_file_i, 'r', encoding='utf-8') as f: 174 existing = f.read() 175 # get the first line 176 existing_first_line, existing_rest = existing.split('\n', 1) 177 existing_bib_filenames_i = parse_frontmatter(existing_first_line) 178 # if it's the same or shorter 179 if len(existing_bib_filenames_i) >= len(bib_filenames_i) or set(existing_bib_filenames_i) == set(bib_filenames_i): 180 formatted_first_line, formatted_rest = formatted.split('\n', 1) 181 if existing_rest == formatted_rest: 182 continue 183 # if manually modified, skip 184 if 'manual' in existing_bib_filenames_i: 185 print(f"🦘Skipping {tree_file_i.relative_to(project_root)} as it's manually modified") 186 continue 187 188 with open(tree_file_i, 'w', encoding='utf-8') as f: 189 f.write(formatted) 190 f.flush()