Natural Hazards
Choose a Markdown page from the list.
#!/usr/bin/env python3 import json, re, shutil, subprocess, tempfile from pathlib import Path ROOT=Path(__file__).resolve().parent MD,FIG,SITE=ROOT/'markdown',ROOT/'figures',ROOT/'site' def main(): FIG.mkdir(exist_ok=True); sources=sorted(MD.glob('NaturalHazards.Chapter[0-9]*.md'),key=lambda p:int(re.search(r'Chapter(\d+)',p.name).group(1))); mapping={} for source in sources: ch=int(re.search(r'Chapter(\d+)',source.name).group(1)); pdf=ROOT/f'NaturalHazards.Chapter{ch}.pdf' if not pdf.exists(): continue text=source.read_text(encoding='utf-8'); text=re.sub(r'\n*!\[Source PDF page \d+, containing Figure [^\]]+\]\([^\n]+\)\n*', '\n', text); pages=subprocess.run(['pdftotext','-layout',str(pdf),'-'],check=True,capture_output=True,text=True).stdout.split('\f') raw_ids=re.findall(rf'Figure\s+{ch}\.\d+(?:\.[A-Z])?[A-Z]?(?![\d.])',text,re.I); ids=sorted({x.casefold():re.sub(r'^figure','Figure',x,flags=re.I) for x in raw_ids}.values(),key=lambda s:(int(re.search(r'\.(\d+)',s).group(1)),s)); found={} for fig in ids: cap=re.escape(fig)+r"(?![\d.])(?:\s*:\s*|\s+)(?=(?-i:[A-Z]))" md_caps=list(re.finditer(cap,text,re.I)); candidates=[] for i,page in enumerate(pages): for pm in re.finditer(cap,page,re.I): pdf_words=set(re.findall(r"[a-zA-Z]{4,}",page[pm.end():pm.end()+220].lower())) for mm in md_caps: md_words=set(re.findall(r"[a-zA-Z]{4,}",text[mm.end():mm.end()+220].lower())) score=len(pdf_words & md_words) candidates.append((score,i+1,mm.start())) if candidates: score,page,pos=max(candidates); found[fig]=page; found[fig+'|offset']=pos inserts=[] for key,page in list(found.items()): if '|' in key: continue pos=found.get(key+'|offset') if pos is not None: inserts.append((pos,f'\n\n\n\n')) found={k:v for k,v in found.items() if '|' not in k}; mapping[source.name]=found for pos,value in sorted(inserts,reverse=True): text=text[:pos]+value+text[pos:] source.write_text(text,encoding='utf-8') if found: with tempfile.TemporaryDirectory(prefix='natural-hazards-') as temp: for page in sorted(set(found.values())): target=FIG/f'chapter-{ch}-page-{page:02}.jpg' if target.exists(): continue prefix=str(Path(temp)/'page') subprocess.run(['pdftoppm','-f',str(page),'-l',str(page),'-jpeg','-scale-to','1400','-jpegopt','quality=78',str(pdf),prefix],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) shutil.copy2(f'{prefix}-{page:02}.jpg',target) # Appendices use lettered figure numbers and a separate source PDF. source=MD/'NaturalHazards.Appendices.md'; pdf=ROOT/'NaturalHazards.Appendices.pdf' text=source.read_text(encoding='utf-8'); text=re.sub(r'\n*!\[Source PDF page \d+, containing Figure [^\]]+\]\([^\n]+\)\n*','\n',text) pages=subprocess.run(['pdftotext','-layout',str(pdf),'-'],check=True,capture_output=True,text=True).stdout.split('\f') raw_ids=re.findall(r'Figure\s+[A-Z]\.\d+[A-Z]?(?![\d.])',text,re.I); ids=sorted({x.casefold():re.sub(r'^figure','Figure',x,flags=re.I) for x in raw_ids}.values()); found={} for fig in ids: cap=re.escape(fig)+r"(?![\d.])(?:\s*:\s*|\s+)(?=(?-i:[A-Z]))"; md_caps=list(re.finditer(cap,text,re.I)); candidates=[] for i,page in enumerate(pages): for pm in re.finditer(cap,page,re.I): pdf_words=set(re.findall(r'[a-zA-Z]{4,}',page[pm.end():pm.end()+220].lower())) for mm in md_caps: md_words=set(re.findall(r'[a-zA-Z]{4,}',text[mm.end():mm.end()+220].lower())); candidates.append((len(pdf_words & md_words),i+1,mm.start())) if candidates: score,page,pos=max(candidates); found[fig]=page; found[fig+'|offset']=pos inserts=[] for key,page in list(found.items()): if '|' not in key: inserts.append((found[key+'|offset'],f'\n\n\n\n')) found={k:v for k,v in found.items() if '|' not in k}; mapping[source.name]=found for pos,value in sorted(inserts,reverse=True): text=text[:pos]+value+text[pos:] source.write_text(text,encoding='utf-8') with tempfile.TemporaryDirectory(prefix='natural-hazards-appendices-') as temp: for page in sorted(set(found.values())): target=FIG/f'appendices-page-{page:02}.jpg' if target.exists(): continue prefix=str(Path(temp)/'page'); subprocess.run(['pdftoppm','-f',str(page),'-l',str(page),'-jpeg','-scale-to','1400','-jpegopt','quality=78',str(pdf),prefix],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL); shutil.copy2(f'{prefix}-{page:02}.jpg',target) # The complete-book Markdown also gets links to the same chapter and appendix images. full=MD/"Natural hazards_ earth's processes as hazards, disasters, and catastrophes.md" if full.exists(): full_text=full.read_text(encoding='utf-8'); full_text=re.sub(r'\n*!\[Source PDF page \d+, containing Figure [^\]]+\]\([^\n]+\)\n*','\n',full_text); assets={} for name,items in mapping.items(): if name==full.name: continue for fig,page in items.items(): if name=='NaturalHazards.Appendices.md': rel=f'../figures/appendices-page-{page:02}.jpg' else: chapter=re.search(r'Chapter(\d+)',name).group(1); rel=f'../figures/chapter-{chapter}-page-{page:02}.jpg' assets[fig]=(page,rel) inserts=[] for fig,(page,rel) in assets.items(): cap=re.escape(fig)+r"(?![\d.])(?:\s*:\s*|\s+)(?=(?-i:[A-Z]))"; match=re.search(cap,full_text,re.I) if match: inserts.append((match.start(),f'\n\n\n\n')) for pos,value in sorted(inserts,reverse=True): full_text=full_text[:pos]+value+full_text[pos:] full.write_text(full_text,encoding='utf-8'); mapping[full.name]={fig:rel for fig,(_,rel) in assets.items() if re.search(re.escape(fig)+r"(?![\d.])(?:\s*:\s*|\s+)(?=(?-i:[A-Z]))",full_text,re.I)} (ROOT/'figure-pages.json').write_text(json.dumps(mapping,indent=2)+'\n'); make_site(sources) def make_site(chapters): SITE.mkdir(exist_ok=True); files=list(chapters)+[MD/'NaturalHazards.Appendices.md']; known={p.name for p in files}; files += [p for p in sorted(MD.glob('*.md')) if p.name not in known] books=[] for p in files: if not p.exists(): continue title=p.stem.replace('NaturalHazards.','').replace('_',' ') if title.startswith('Natural hazards'): title='Full Book — Natural Hazards' title=re.sub(r'Chapter(\d+)',r'Chapter \1',title) title=re.sub(r'(?<=[a-z])(?=[A-Z])',' ',title).replace('.',' — ') books.append({'title':title,'path':'../'+p.relative_to(ROOT).as_posix()}) (SITE/'books.json').write_text(json.dumps(books,indent=2)+'\n'); (SITE/'index.html').write_text(HTML); (SITE/'app.js').write_text(APP); (SITE/'style.css').write_text(CSS); (SITE/'README.md').write_text('Serve the book directory over HTTP, then open `http://localhost:8000/site/`:\n\n```sh\npython3 -m http.server 8000\n```\n') HTML='''
Choose a Markdown page from the list.
Could not load this page. Run python3 -m http.server from the book directory.
'+p.map(x=>inl(x,b)).join(' ')+'
');p=[]}if(list){out.push('');list=false}if(table.length){let rs=table.filter(x=>!/^\|?\s*:?-{3,}/.test(x));out.push('