Files
digital-waqif/assemble_pdf.py
T

336 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Assemble 'The Digital Waqif: Waqf, Faraid, and the Architecture of Legacy' PDF
Will & Testament Playbook — Legal-document aesthetic, 6×9in."""
import os, re, sys
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.lib.colors import HexColor
from reportlab.platypus import (
Paragraph, Spacer, PageBreak, Frame, PageTemplate, BaseDocTemplate
)
OUTPUT_DIR = os.path.expanduser("~/odysseus_book/digital_waqif")
CHAPTERS_DIR = os.path.join(OUTPUT_DIR, "chapters")
PDF_PATH = os.path.join(OUTPUT_DIR, "pdf", "The_Digital_Waqif.pdf")
MARGIN = 0.85 * inch
FOOTER_H = 0.4 * inch
PAGE_W = 6 * inch
PAGE_H = 9 * inch
NAVY = HexColor('#1B2A4A')
GOLD = HexColor('#C8A951')
DARK = HexColor('#1a1a1a')
GREY = HexColor('#666666')
INK = HexColor('#222222')
ACCENT_RED = HexColor('#8B0000')
style_title = ParagraphStyle('Title', fontName='Helvetica-Bold', fontSize=24,
leading=30, alignment=TA_CENTER, textColor=NAVY, spaceAfter=8)
style_subtitle = ParagraphStyle('Subtitle', fontName='Helvetica-Oblique', fontSize=13,
leading=17, alignment=TA_CENTER, textColor=GOLD, spaceAfter=20)
style_author = ParagraphStyle('Author', fontName='Helvetica', fontSize=12,
leading=16, alignment=TA_CENTER, textColor=GREY, spaceAfter=28)
style_copyright = ParagraphStyle('Copyright', fontName='Helvetica', fontSize=9,
leading=12, alignment=TA_CENTER, textColor=GREY)
style_ch_num = ParagraphStyle('ChNum', fontName='Helvetica-Bold', fontSize=10,
leading=13, alignment=TA_CENTER, textColor=GOLD, spaceAfter=4, spaceBefore=18)
style_ch_title = ParagraphStyle('ChTitle', fontName='Helvetica-Bold', fontSize=17,
leading=23, alignment=TA_LEFT, textColor=NAVY, spaceAfter=8, spaceBefore=4)
style_subtitle_ch = ParagraphStyle('SubtitleCh', fontName='Helvetica-Oblique', fontSize=11,
leading=15, alignment=TA_LEFT, textColor=ACCENT_RED, spaceAfter=14, spaceBefore=2)
style_framework = ParagraphStyle('Framework', fontName='Helvetica-Bold', fontSize=10.5,
leading=14, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=14,
backColor=HexColor('#F4F6FA'), borderColor=GOLD, borderWidth=1, borderPadding=8)
style_body = ParagraphStyle('Body', fontName='Times-Roman', fontSize=10,
leading=14.5, alignment=TA_JUSTIFY, textColor=INK, spaceAfter=7, firstLineIndent=0)
style_clause = ParagraphStyle('Clause', fontName='Times-Bold', fontSize=11,
leading=15, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10,
backColor=HexColor('#F4F6FA'), borderColor=NAVY, borderWidth=1, borderPadding=10)
style_nass = ParagraphStyle('Nass', fontName='Times-Italic', fontSize=10,
leading=14.5, alignment=TA_JUSTIFY, textColor=DARK, spaceAfter=6, leftIndent=24,
backColor=HexColor('#FDFBF3'), borderColor=GOLD, borderWidth=0.5, borderPadding=8)
style_faradi = ParagraphStyle('Faradi', fontName='Times-Bold', fontSize=10,
leading=14.5, alignment=TA_LEFT, textColor=NAVY, spaceAfter=3, leftIndent=24)
style_waqif = ParagraphStyle('Waqif', fontName='Times-Bold', fontSize=10,
leading=14.5, alignment=TA_LEFT, textColor=GOLD, spaceAfter=3, leftIndent=24)
style_principle = ParagraphStyle('Principle', fontName='Times-Bold', fontSize=11,
leading=15, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10,
backColor=HexColor('#F4F6FA'), borderColor=NAVY, borderWidth=1, borderPadding=10)
style_protocol = ParagraphStyle('Protocol', fontName='Times-Roman', fontSize=10,
leading=14.5, alignment=TA_LEFT, textColor=DARK, spaceAfter=6, leftIndent=36)
style_hisbah = ParagraphStyle('Hisbah', fontName='Times-Italic', fontSize=10.5,
leading=15, alignment=TA_CENTER, textColor=GREY, spaceAfter=16, spaceBefore=16,
backColor=HexColor('#F8F9FA'), borderColor=GREY, borderWidth=0.5, borderPadding=12)
style_toc_ch = ParagraphStyle('TOCCh', fontName='Helvetica-Bold', fontSize=11,
leading=17, alignment=TA_LEFT, textColor=NAVY)
style_toc_sub = ParagraphStyle('TOCSub', fontName='Helvetica', fontSize=9.5,
leading=13, alignment=TA_LEFT, textColor=GREY)
CHAPTERS = [
("Clause 1: The Wasiyyah — The Will That God Commanded", "The Will as Sunnah", "Wasiyyah Quadrant: Mandatory / Recommended / Permitted / Nullified — the 1/3 cap"),
("Clause 2: The Faraid Matrix — The Fixed Shares Decoded", "God's Distribution Algorithm", "Faraid Matrix: Ashab al-Furud / 1/2, 1/4, 1/8, 2/3, 1/3, 1/6"),
("Clause 3: The Waqf — The Three Exits of Wealth", "Wasiyyah vs Waqf vs Gift", "Exit Quadrant: Wasiyyah / Waqf / Hibah / Inheritance"),
("Clause 4: The Digital Kingdom — Inventorying the Invisible Estate", "The Estate Nobody Can See", "Digital Inventory: Crypto / Domains / IP / SaaS / DAO / Social"),
("Clause 5: The Keys — Custody and the Executor Who Cannot Be Bribed", "Access Is the Whole Game", "Custody Quadrant: Hot / Cold / Multi-sig / Inherited"),
("Clause 6: The Amil — Choosing the Executor of the Digital Estate", "The Executor as Deputy of the Waqif", "Executor Quadrant: Family / Professional / Institutional / Hybrid"),
("Clause 7: The Purification — Debt, Zakat, and Cleaning the Estate", "The Estate Must Be Clean Before It Is Shared", "Purification Order: Burial → Debts → Wasiyyah (1/3) → Faraid"),
("Clause 8: The Token — Smart-Contract Faraid and On-Chain Waqf", "The Code That Distributes What We Cannot", "Tokenization Quadrant: Smart wills / Multi-sig / Time-locked / On-chain"),
("Clause 9: The Successor — Business Continuity for the Digital Kingdom", "The Business That Survives Its Founder", "Continuity Quadrant: Sole / Partnership / Company / Waqf-owned"),
("Clause 10: The Perpetuity — The Eternal Contract of the Digital Waqif", "From Legacy to Ongoing Charity", "Perpetuity Quadrant: Exit / Inheritance / Waqf / Sadaqah Jariyah"),
]
def clean_text(text):
# Minimal markdown→HTML conversion for reportlab
import re
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
text = re.sub(r'(?<!\*)\*([^*]+)\*(?!\*)', r'<i>\1</i>', text)
text = re.sub(r'`([^`]+)`', r'<font face="Courier">\1</font>', text)
text = text.replace('\\n', '\n')
# Strip remaining markdown headers etc
text = re.sub(r'^#{1,6}\s*', '', text, flags=re.MULTILINE)
# Escape any stray angle brackets that would break reportlab
text = text.replace('&', '&amp;').replace('<b>', '&lt;b&gt;').replace('</b>', '&lt;/b&gt;')
text = text.replace('<i>', '&lt;i&gt;').replace('</i>', '&lt;/i&gt;')
text = text.replace('<font face="Courier">', '&lt;font&gt;').replace('</font>', '&lt;/font&gt;')
# Now re-apply our tags (since we escaped the originals)
text = re.sub(r'&lt;b&gt;', '<b>', text)
text = re.sub(r'&lt;/b&gt;', '</b>', text)
text = re.sub(r'&lt;i&gt;', '<i>', text)
text = re.sub(r'&lt;/i&gt;', '</i>', text)
text = re.sub(r'&lt;font&gt;(.+?)&lt;/font&gt;', r'<font face="Courier">\1</font>', text)
text = re.sub(r'&amp;', '&', text)
return text
def parse_chapter(md_text):
sections = {'clause': [], 'nass': [], 'faradi': [], 'waqif': [], 'declaration': [], 'execution': [], 'hisbah': []}
current = None
for line in md_text.split('\n'):
stripped = line.strip()
if not stripped:
continue
low = stripped.lower()
if low.startswith('# the clause') or low.startswith('## the clause') or low == 'the clause' or low.startswith('clause 1:'):
current = 'clause'
continue
elif low.startswith('# the nass') or low.startswith('## the nass') or low.startswith('the nass') or low == 'nass':
current = 'nass'
continue
elif low.startswith('# faradi') or low.startswith('## faradi') or low.startswith('faradi') or low.startswith('**faradi'):
current = 'faradi'
continue
elif low.startswith('# waqif') or low.startswith('## waqif') or low.startswith('waqif') or low.startswith('**waqif'):
current = 'waqif'
continue
elif low.startswith('# the declaration') or low.startswith('## the declaration') or low.startswith('the declaration') or low.startswith('declaration'):
current = 'declaration'
continue
elif low.startswith('# the execution') or low.startswith('## the execution') or low.startswith('the execution') or low.startswith('execution'):
current = 'execution'
continue
elif low.startswith('# the hisbah') or low.startswith('## the hisbah') or low.startswith('the hisbah') or low.startswith('hisbah') or low.startswith('muhasaba'):
current = 'hisbah'
continue
if stripped.startswith('#'):
continue
if stripped == '---' or stripped == '***':
continue
if current:
sections[current].append(stripped)
for k in sections:
sections[k] = '\n\n'.join(sections[k])
return sections
class BookDoc(BaseDocTemplate):
def __init__(self, *args, **kw):
BaseDocTemplate.__init__(self, *args, **kw)
def footer_fn(canvas, doc):
canvas.saveState()
canvas.setFont('Helvetica', 8)
canvas.setFillColor(HexColor('#999999'))
canvas.drawCentredString(PAGE_W / 2, 0.35 * inch, f'— {doc.page} —')
canvas.restoreState()
def build_pdf():
os.makedirs(os.path.join(OUTPUT_DIR, 'pdf'), exist_ok=True)
frame = Frame(MARGIN, MARGIN + FOOTER_H,
PAGE_W - 2 * MARGIN,
PAGE_H - 2 * MARGIN - FOOTER_H, id='normal')
doc = BookDoc(PDF_PATH, pagesize=(PAGE_W, PAGE_H),
leftMargin=MARGIN, rightMargin=MARGIN,
topMargin=MARGIN, bottomMargin=MARGIN,
title="The Digital Waqif: Waqf, Faraid, and the Architecture of Legacy",
author='Jauhari Che Wan')
doc.addPageTemplates([PageTemplate(id='main', frames=frame, onPage=footer_fn)])
story = []
# Title page
story.append(Spacer(1, PAGE_H * 0.16))
story.append(Paragraph("The Digital Waqif", style_title))
story.append(Paragraph("Waqf, Faraid, and the Architecture of Legacy", style_subtitle))
story.append(Paragraph("10 Clauses of the Digital Will &amp; Endowment", ParagraphStyle('Tagline',
fontName='Helvetica-Oblique', fontSize=12, leading=15, alignment=TA_CENTER, textColor=HexColor('#666666'), spaceAfter=24)))
story.append(Spacer(1, 14))
story.append(Paragraph('A FARADI &amp; WAQIF Playbook', ParagraphStyle('StyleLine',
fontName='Helvetica', fontSize=10, leading=13, alignment=TA_CENTER, textColor=HexColor('#888888'), spaceAfter=20)))
story.append(Spacer(1, 14))
story.append(Paragraph('Jauhari Che Wan', style_author))
story.append(Spacer(1, PAGE_H * 0.1))
story.append(Paragraph('FARADI × WAQIF × MAQASID',
ParagraphStyle('Tag', parent=style_copyright, fontName='Helvetica-Bold', fontSize=9, textColor=GOLD)))
story.append(PageBreak())
# Copyright
story.append(Spacer(1, PAGE_H * 0.28))
for line in ['FARADI × WAQIF PLAYBOOK', '', 'The Digital Waqif', 'Jauhari Che Wan',
'', 'Based on Ilm al-Faraid and Waqf Jurisprudence', 'Surah An-Nisa 4:11-12, 4:176', 'Maqasid al-Shariah',
'', 'Copyright \u00a9 2025', 'All rights reserved',
'', 'Typeset in Times / Helvetica', 'Printed in Malaysia']:
if line == '':
story.append(Spacer(1, 4))
else:
story.append(Paragraph(line, style_copyright))
story.append(PageBreak())
# Dedication
story.append(Spacer(1, PAGE_H * 0.28))
story.append(Paragraph('For every founder with a seed phrase, a domain, and no will.', ParagraphStyle('Ded',
fontName='Times-Italic', fontSize=11, leading=16, alignment=TA_CENTER, textColor=DARK, spaceAfter=24)))
story.append(PageBreak())
# TOC
story.append(Paragraph('Contents', ParagraphStyle('TOCHeader',
fontName='Helvetica-Bold', fontSize=18, leading=24, alignment=TA_LEFT, textColor=NAVY, spaceAfter=20)))
for i, (title, subtitle, _) in enumerate(CHAPTERS, 1):
story.append(Paragraph(f'<b>Clause {i}</b>: {title}', style_toc_ch))
story.append(Paragraph(f' <i>{subtitle}</i>', style_toc_sub))
story.append(Spacer(1, 4))
story.append(PageBreak())
# Chapters
for ch_num, (title, subtitle, framework) in enumerate(CHAPTERS, 1):
fname = os.path.join(CHAPTERS_DIR, f"Clause_0{ch_num}.md") if ch_num < 10 else os.path.join(CHAPTERS_DIR, f"Clause_{ch_num}.md")
if not os.path.exists(fname):
print(f" WARNING: {fname} not found")
continue
with open(fname, 'r', encoding='utf-8') as f:
md = f.read()
print(f" Adding Clause #{ch_num}: {title}...")
sections = parse_chapter(md)
# Chapter header
story.append(Paragraph(f'Clause {ch_num}', style_ch_num))
story.append(Paragraph(title, style_ch_title))
story.append(Paragraph(subtitle, style_subtitle_ch))
story.append(Paragraph(framework, style_framework))
story.append(Spacer(1, 8))
# THE CLAUSE
if sections['clause']:
clause_text = clean_text(' '.join(sections['clause'].split('\n')))
story.append(Paragraph(clause_text, style_clause))
story.append(Spacer(1, 6))
# THE NASS
if sections['nass']:
story.append(Paragraph('THE NASS — THE EVIDENCE', ParagraphStyle('NassHeader',
fontName='Helvetica-Bold', fontSize=12, leading=16, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10)))
for para in sections['nass'].split('\n\n'):
clean = clean_text(para.strip())
if clean:
story.append(Paragraph(clean, style_nass))
story.append(Spacer(1, 6))
# FARADI'S READING
if sections['faradi']:
story.append(Paragraph('FARADI\u2019S READING — THE JUSTICE', ParagraphStyle('FaradiHeader',
fontName='Helvetica-Bold', fontSize=12, leading=16, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10)))
for para in sections['faradi'].split('\n\n'):
clean = clean_text(para.strip())
if not clean:
continue
low = clean.lower()
if low.startswith('faradi:'):
story.append(Paragraph(f'<b>FARADI:</b> {clean[7:].strip()}', style_faradi))
else:
story.append(Paragraph(clean, style_body))
story.append(Spacer(1, 6))
# WAQIF'S READING
if sections['waqif']:
story.append(Paragraph('WAQIF\u2019S READING — THE PERPETUITY', ParagraphStyle('WaqifHeader',
fontName='Helvetica-Bold', fontSize=12, leading=16, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10)))
for para in sections['waqif'].split('\n\n'):
clean = clean_text(para.strip())
if not clean:
continue
low = clean.lower()
if low.startswith('waqif:'):
story.append(Paragraph(f'<b>WAQIF:</b> {clean[6:].strip()}', style_waqif))
else:
story.append(Paragraph(clean, style_body))
story.append(Spacer(1, 6))
# THE DECLARATION
if sections['declaration']:
story.append(Paragraph('THE DECLARATION — THE LEGACY PRINCIPLE', ParagraphStyle('DeclHeader',
fontName='Helvetica-Bold', fontSize=12, leading=16, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10)))
for para in sections['declaration'].split('\n\n'):
clean = clean_text(para.strip())
if not clean:
continue
low = clean.lower()
if low.startswith('hukm:'):
story.append(Paragraph(f'<b>HUKM:</b> {clean[5:].strip()}', style_body))
elif low.startswith('daleel:'):
story.append(Paragraph(f'<b>DALEEL:</b> {clean[7:].strip()}', style_body))
elif low.startswith('maqsad:'):
story.append(Paragraph(f'<b>MAQSAD:</b> {clean[7:].strip()}', style_body))
elif low.startswith('shurut:'):
story.append(Paragraph(f'<b>SHURUT:</b> {clean[7:].strip()}', style_body))
elif low.startswith('munkathirat:'):
story.append(Paragraph(f'<b>MUNKATHIRAT:</b> {clean[12:].strip()}', style_body))
else:
story.append(Paragraph(clean, style_body))
story.append(Spacer(1, 8))
# THE EXECUTION
if sections['execution']:
story.append(Paragraph('THE EXECUTION — DO THIS THIS WEEK', ParagraphStyle('ExecHeader',
fontName='Helvetica-Bold', fontSize=12, leading=16, alignment=TA_LEFT, textColor=NAVY, spaceAfter=6, spaceBefore=10)))
for i, para in enumerate(sections['execution'].split('\n\n'), 1):
clean = clean_text(para.strip())
if clean:
if clean.lower().startswith(f'step {i}') or clean.lower().startswith(f'step {i}:'):
clean = clean.split(':', 1)[-1].strip()
story.append(Paragraph(f'<b>STEP {i}:</b> {clean}', style_protocol))
story.append(Spacer(1, 8))
# THE HISBAH
if sections['hisbah']:
clean = clean_text(' '.join(sections['hisbah'].split('\n')))
story.append(Paragraph(clean, style_hisbah))
story.append(PageBreak())
print('Building PDF...')
doc.build(story)
print(f'Done: {PDF_PATH}')
import subprocess
result = subprocess.run(['wc', '-c', PDF_PATH], capture_output=True, text=True)
size_kb = int(result.stdout.split()[0]) / 1024
with open(PDF_PATH, 'rb') as f:
data = f.read()
pages = len(re.findall(rb'/Type\s*/Page[^s]', data))
print(f'Pages: {pages}, Size: {size_kb:.0f} KB')
if __name__ == '__main__':
build_pdf()