skill
Adom Wiki Skill Pack
Public Unreviewedby John Lauer
How to use the Adom Wiki without breaking pages — the wiki is six tools in one (git, adompkg, releases, discussions, PRs, discovery), and three repo archetypes have emerged from real use: Page, Skillpack, and Family. Plus how to publish rich component pages (chips, molecules, boards). One install, 15 skills, plus a write-up of the design patterns for the people who build the wiki.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
#!/usr/bin/env python3
# Data-driven Adom component-page generator.
# Reads a chip-fetcher chip dir (info/stock/extracted/provenance + SVGs + CAD) and
# renders a self-contained, Adom-UI-compliant standalone HTML page with class-aware
# specs and REAL provenance (per-pin + per-dimension datasheet page citations, with
# cropped page images). No emoji, no em/en-dashes, brand fonts, monochrome icons.
import json, os, re, base64, glob, subprocess, sys
LIBROOT="/home/adom/project/chip-fetcher/library"
# Per-chip config: published wiki slug (=> embed LIVE viewers), hero, mfr resources, family.
CFG={
"nRF54L15":{"slug":"nrf54l15","wiki":"https://wiki.adom.inc/john/nrf54l15",
"hero":"/home/adom/project/wiki-nrf54l15/screenshots/hero.png",
"resources":[("Nordic nRF54L15 product page","https://www.nordicsemi.com/Products/nRF54L15"),
("Product Specification (845-page datasheet)","https://docs.nordicsemi.com/bundle/ps_nrf54L15/page/keyfeatures_html5.html"),
("nRF Connect SDK","https://www.nordicsemi.com/Products/Development-software/nRF-Connect-SDK"),
("nRF54L15 DK (dev kit)","https://www.nordicsemi.com/Products/Development-hardware/nRF54L15-DK"),
("Nordic DevZone (support)","https://devzone.nordicsemi.com/")],
"family":"nRF54L10 / nRF54L05 (same QFN-48 pinout, less NVM/RAM); broader nRF54H / nRF53 / nRF52 series."},
"DMG2305UX-7":{"resources":[("Diodes Inc DMG2305UX product page","https://www.diodes.com/part/view/DMG2305UX/")],
"family":"DMG3415UX, DMG2301L and other Diodes P-channel SOT-23 MOSFETs."},
"LM358D":{"resources":[("TI LM358 product page","https://www.ti.com/product/LM358"),("LM358 datasheet","https://www.ti.com/lit/ds/symlink/lm358.pdf")],
"family":"LM358DR (tape and reel), LM2904 (automotive), LM324 (quad)."},
"BME688":{"resources":[("Bosch BME688 product page","https://www.bosch-sensortec.com/products/environmental-sensors/gas-sensors/bme688/")],
"family":"BME680 (no AI gas), BME280 (T/RH/P, no gas)."},
"TPS25751SRSMR":{"resources":[("TI TPS25751 product page","https://www.ti.com/product/TPS25751")],
"family":"TPS25750, TPS65987 USB-PD controllers."},
"WSL2010R0500FEA":{"resources":[("Vishay WSL2010 product page","https://www.vishay.com/en/product/30100/")],
"family":"WSL series current-sense resistors (WSL1206, WSL2512)."},
}
FONTS="/home/adom/datasheet-visualizer/public/fonts"
OUT="/tmp/nrf-site" # served dir
def b64f(p): return base64.b64encode(open(p,'rb').read()).decode()
def rd(p):
try: return open(p,encoding='utf-8').read()
except: return ""
def jload(p):
try: return json.load(open(p,encoding='utf-8'))
except: return None
def scrub(s):
if s is None: return ""
s=str(s)
for a,b in [("—","-"),("–","-"),("−","-"),("…","..."),("·"," ")]:
s=s.replace(a,b)
return s
FAM5=b64f(f"{FONTS}/familjen-grotesk-500-normal.woff2")
FAM7=b64f(f"{FONTS}/familjen-grotesk-700-normal.woff2")
SAT4=b64f(f"{FONTS}/satoshi-400-normal.woff2")
SAT5=b64f(f"{FONTS}/satoshi-500-normal.woff2")
SAT7=b64f(f"{FONTS}/satoshi-700-normal.woff2")
P={
"cube":'<path d="M12 2l9 5v10l-9 5-9-5V7z"/><path d="M12 22V12M21 7l-9 5L3 7"/>',
"symbol":'<rect x="4" y="4" width="16" height="16" rx="1"/><path d="M4 9h3M4 15h3M17 9h3M17 15h3"/>',
"footprint":'<rect x="4" y="4" width="16" height="16" rx="1"/><circle cx="8" cy="8" r="1"/><circle cx="16" cy="8" r="1"/><circle cx="8" cy="16" r="1"/><circle cx="16" cy="16" r="1"/>',
"doc":'<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/>',
"cart":'<circle cx="9" cy="20" r="1"/><circle cx="18" cy="20" r="1"/><path d="M2 3h3l2.4 12.4a1 1 0 0 0 1 .8h8.9a1 1 0 0 0 1-.8L21 7H6"/>',
"download":'<path d="M12 3v12M7 11l5 5 5-5M5 21h14"/>',
"list":'<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/>',
"grid":'<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>',
"dollar":'<path d="M12 1v22"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>',
"box":'<rect x="3" y="7" width="18" height="13" rx="1"/><path d="M3 11h18M9 7V4h6v3"/>',
"building":'<path d="M4 21V5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v16M16 8h3a1 1 0 0 1 1 1v12"/><path d="M8 8h.01M12 8h.01M8 12h.01M12 12h.01M8 16h.01M12 16h.01"/>',
"layers":'<path d="M12 2l9 5-9 5-9-5z"/><path d="M3 12l9 5 9-5M3 17l9 5 9-5"/>',
"link":'<path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/>',
"open":'<path d="M15 3h6v6M10 14 21 3M21 14v7H3V3h7"/>',
"warn":'<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h16.9a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>',
"shield":'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/>',
"chip":'<rect x="6" y="6" width="12" height="12" rx="1"/><path d="M9 2v4M15 2v4M9 18v4M15 18v4M2 9h4M2 15h4M18 9h4M18 15h4"/>',
}
def ic(n,s=18):
return f'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" width="{s}" height="{s}" style="vertical-align:middle;flex:none">{P[n]}</svg>'
def detect_class(info, sym):
t=((info.get('family') or '')+' '+(info.get('description') or '')+' '+(sym.get('category') or '')).lower()
if 'mosfet' in t or ('fet' in t and 'channel' in t): return 'mosfet'
if 'op-amp' in t or 'op amp' in t or 'opamp' in t or 'amplifier' in t: return 'opamp'
if 'resistor' in t or 'capacitor' in t or 'inductor' in t: return 'passive'
if 'sensor' in t or 'imu' in t or 'gyro' in t or 'accel' in t: return 'sensor'
if any(k in t for k in ('pd controller','type-c','charger','buck','boost','dc-dc','dc/dc','ldo','regulator','power switch','gate driver','pmic')): return 'power'
if 'adc' in t or 'converter' in t: return 'converter'
if 'mcu' in t or 'soc' in t or 'cortex' in t or 'microcontroller' in t or 'radio' in t: return 'mcu'
return 'generic'
def find(d, *pats):
s=(d.get('description') or '')+' '+(d.get('family') or '')
for p in pats:
m=re.search(p, s, re.I)
if m: return scrub(m.group(0))
return None
def spec_rows(cls, info, sym):
d=info; desc=scrub(d.get('description') or '')
rows=[]
if cls=='mcu':
rows=[("Class","Wireless MCU / SoC"),("Core",find(d,r'cortex[- ]m\w+[^,.;]*') or "see datasheet"),
("Memory",find(d,r'\d+\.?\d*\s?MB\s?(NVM|flash)[^,.;]*') or find(d,r'\d+\s?KB[^,.;]*') or "see datasheet"),
("Radio",find(d,r'2\.4\s?GHz[^,.;]*') or find(d,r'(BLE|Bluetooth)[^,.;]*') or "2.4 GHz"),
("Package",scrub(d.get('package'))),("Pins",str(len(sym.get('pins',[])))+" extracted")]
elif cls=='mosfet':
rows=[("Class","MOSFET"),("Channel",find(d,r'[PN]-channel') or "see datasheet"),
("Vds",find(d,r'-?\d+\.?\d*\s?V\s?Vds') or find(d,r'Vds[^,.;]*') or "see datasheet"),
("Id (cont.)",find(d,r'-?\d+\.?\d*\s?A\s?(continuous)?[^,.;]*') or "see datasheet"),
("Rds(on)",find(d,r'Rds\(on\)[^,.;]*') or "see datasheet"),
("Vgs(th)",find(d,r'(low-?vgs\(th\)|Vgs\(th\)[^,.;]*)') or "see datasheet"),
("Package",scrub(d.get('package')))]
elif cls=='opamp':
rows=[("Class","Operational amplifier"),("Channels",find(d,r'(dual|single|quad)') or "see datasheet"),
("GBW",find(d,r'[\d.]+\s?MHz\s?GBW') or find(d,r'GBW[^,.;]*') or "see datasheet"),
("Slew rate",find(d,r'[\d.]+\s?V/[uµ]s') or "see datasheet"),
("Supply",find(d,r'(single|split)?\s?[±]?[\d.]+\s?V[^,.;]*') or "see datasheet"),
("Package",scrub(d.get('package')))]
elif cls=='sensor':
rows=[("Class","Sensor"),("Type",scrub(d.get('family') or sym.get('category') or "see datasheet")),
("Interface",find(d,r'(I.?2.?C|SPI|UART|analog)[^,.;]*') or "see datasheet"),
("Supply",find(d,r'[\d.]+\s?V[^,.;]*') or "see datasheet"),
("Package",scrub(d.get('package'))),("Pins",str(len(sym.get('pins',[])))+" extracted")]
elif cls=='passive':
rows=[("Class","Passive"),("Value",find(d,r'[\d.]+\s?(m?[ΩROhm]|[munpf]F|[munp]?H)[^,.;]*') or find(d,r'[\d.]+\s?m[ΩO][^,.;]*') or "see datasheet"),
("Tolerance",find(d,r'[±]?[\d.]+\s?%') or "see datasheet"),
("Rating",find(d,r'[\d.]+\s?W[^,.;]*') or find(d,r'[\d.]+\s?V[^,.;]*') or "see datasheet"),
("Package",scrub(d.get('package'))) ]
elif cls=='power':
rows=[("Class","Power / controller"),("Function",scrub(d.get('family') or sym.get('category') or "see datasheet")),
("Vin",find(d,r'[\d.]+\s?V?\s?to\s?[\d.]+\s?V[^,.;]*') or find(d,r'[\d.]+\s?V\s?(input|Vin)[^,.;]*') or "see datasheet"),
("Iout",find(d,r'[\d.]+\s?A[^,.;]*') or "see datasheet"),
("Package",scrub(d.get('package'))),("Pins",str(len(sym.get('pins',[])))+" extracted")]
else:
rows=[("Class",scrub(sym.get('category') or info.get('family') or "Component")),
("Package",scrub(d.get('package'))),("Pins",str(len(sym.get('pins',[])))+" extracted")]
return rows
def crop_pages(pdf, pages, mpn):
"""Crop cited datasheet pages to base64 PNGs. Returns {page:b64}. Cap at 5."""
out={}
if not pdf or not os.path.exists(pdf): return out
for pg in sorted(set(pages))[:5]:
png=f"/tmp/gen/{mpn}-p{pg}.png"
try:
subprocess.run(["pdftoppm","-png","-r","120","-f",str(pg),"-l",str(pg),pdf,f"/tmp/gen/{mpn}-pp{pg}"],
check=True, capture_output=True, timeout=60)
src=glob.glob(f"/tmp/gen/{mpn}-pp{pg}*.png")
if not src: continue
subprocess.run(["convert",src[0],"-resize","850x",png],check=True,capture_output=True,timeout=60)
out[pg]=b64f(png)
except Exception as e:
pass
return out
def gen(mpn, slug):
D=f"{LIBROOT}/{mpn}"
info=jload(f"{D}/info.json") or {}
stock=jload(f"{D}/stock.json")
sym=jload(glob.glob(f"{D}/*-symbol.extracted.json")[0]) if glob.glob(f"{D}/*-symbol.extracted.json") else {}
prov=jload(glob.glob(f"{D}/*provenance*.json")[0]) if glob.glob(f"{D}/*provenance*.json") else {}
sym_svg=scrub(re.sub(r'<\?xml[^>]*\?>','', rd(glob.glob(f"{D}/{mpn}-symbol.svg")[0]) if glob.glob(f"{D}/{mpn}-symbol.svg") else ""))
fp_svg=scrub(re.sub(r'<\?xml[^>]*\?>','', rd(glob.glob(f"{D}/{mpn}-footprint.svg")[0]) if glob.glob(f"{D}/{mpn}-footprint.svg") else ""))
pdf=(glob.glob(f"{D}/*.pdf") or [None])
pdf=sorted(pdf, key=lambda p:-(os.path.getsize(p) if p and os.path.exists(p) else 0))[0] if pdf and pdf[0] else None
cls=detect_class(info, sym)
pins=sym.get('pins',[])
pin_prov={str(c.get('pin_number')):c for c in (prov.get('symbol',{}).get('pin_provenance') or [])}
dim_prov=prov.get('footprint',{}).get('dimension_provenance') or []
review=prov.get('review_required') or []
cited=set()
for c in pin_prov.values():
if c.get('page'): cited.add(int(c['page']))
for dp in dim_prov:
if dp.get('page'): cited.add(int(dp['page']))
crops=crop_pages(pdf, cited, mpn)
tooling=prov.get('tooling',{})
ds=prov.get('datasheet',{})
# ---- build pin rows grouped ----
groups={}
for p in pins:
groups.setdefault(p.get('group','OTHER'),[]).append(p)
pin_html=""
for g,ps in groups.items():
pin_html+=f'<tr><td colspan="4" style="color:var(--purple);font-weight:600;padding-top:12px">{scrub(g)} ({len(ps)})</td></tr>'
for p in ps:
num=str(p.get('number','')); c=pin_prov.get(num,{})
pg=c.get('page'); cap=scrub(c.get('table_caption') or '')
cite=f'<span class="evlink" onclick="LB(EV[{pg}],\'{scrub(cap)} (p.{pg})\')">p.{pg}</span>' if pg and pg in crops else (f'p.{pg}' if pg else '')
pin_html+=f'<tr><td class="k">{scrub(p.get("name"))}</td><td class="k">{num}</td><td>{scrub(p.get("description"))[:200]}</td><td class="ev">{cite}</td></tr>'
# ---- dimension provenance rows ----
dim_html=""
for dp in dim_prov:
pg=dp.get('page')
cite=f'<span class="evlink" onclick="LB(EV[{pg}],\'{scrub(dp.get("figure_label"))} (p.{pg})\')">p.{pg}</span>' if pg and pg in crops else (f'p.{pg}' if pg else '')
dim_html+=f'<tr><td class="k">{scrub(dp.get("field"))}</td><td class="k">{scrub(dp.get("value"))}</td><td>{scrub(dp.get("figure_label"))}</td><td class="ev">{cite}</td></tr>'
# ---- review flags ----
rev_html=""
for r in review:
rev_html+=f'<li>{scrub(r.get("reason"))}: {scrub(", ".join(r.get("pins",[])) if r.get("pins") else r.get("package",""))[:160]}</li>'
# ---- spec rows ----
specs=spec_rows(cls, info, sym)
spec_html="".join(f'<tr><th>{scrub(k)}</th><td>{scrub(v)}</td></tr>' for k,v in specs)
# ---- sourcing ----
src_html=""
if stock and stock.get('mouser'):
m=stock['mouser']; tiers=m.get('price_tiers',[])
qty="".join(f'<th>{t["qty"]}</th>' for t in tiers); pr="".join(f'<td>{scrub(t["price"])}</td>' for t in tiers)
src_html=f'''<div class="ctxrow"><span class="ctx">Mouser {scrub(m.get("mouser_pn"))}, <b>{m.get("stock")} in stock</b>, lifecycle {scrub(m.get("lifecycle"))}, {scrub(m.get("rohs"))}</span><span class="src real">real, {scrub(stock.get("checked_at","")[:10])}</span></div>
<table><tr><th>Qty</th>{qty}</tr><tr><td>Unit</td>{pr}</tr></table>
<p style="margin-top:8px"><a href="{m.get("product_url")}" target="_blank">Buy on Mouser</a></p>'''
else:
src_html='<p class="src mock">No stock.json for this part. TODO: fetch live price and stock from Mouser / DigiKey.</p>'
# ---- provenance evidence images ----
ev_js="var EV={"+",".join(f'{pg}:"data:image/png;base64,{b}"' for pg,b in crops.items())+"};"
prov_imgs="".join(f'<div style="margin-top:12px"><div class="viewer-cap"><span>Datasheet p.{pg}</span></div><img class="provimg" src="data:image/png;base64,{b}" onclick="LB(EV[{pg}],\'Datasheet p.{pg}\')"></div>' for pg,b in list(crops.items())[:3])
cls_label={'mcu':'Wireless MCU / SoC','mosfet':'MOSFET','opamp':'Op-amp','converter':'Data converter','passive':'Passive','sensor':'Sensor','power':'Power / controller','generic':'Component'}[cls]
desc=scrub(info.get('description') or '')
pkg=scrub(info.get('package') or '')
mfr=scrub(info.get('manufacturer') or '')
has_glb=bool(glob.glob(f"{D}/{mpn}.glb"))
cad=[("KiCad symbol",f"{mpn}.kicad_sym"),("KiCad footprint",f"{mpn}.kicad_mod"),("STEP",f"{mpn}.step"),("Fusion .lbr",f"{mpn}.lbr")]
if has_glb: cad.append(("GLB (web 3D)",f"{mpn}.glb"))
cad_html="".join(f'<div class="dlcard"><div class="n">{f}</div><div class="d">{scrub(lbl)}</div></div>' for lbl,f in cad if glob.glob(f"{D}/{f}"))
cfg=CFG.get(mpn,{})
pub=cfg.get("slug") # published wiki slug => live viewers
wikiurl=cfg.get("wiki","#")
# ---- viewers: live iframe when published, else inline static SVG ----
def liveviewer(kind,label):
u=f"https://wiki.adom.inc/viewer/{kind}/component/{pub}?embed=1"
return f'<div class="viewer-cap"><span class="live">{label} (live)</span><span>drag, zoom, layer toggle</span></div><iframe class="viewer" src="{u}" loading="lazy"></iframe>'
# Prefer the viewer HTML generated by the actual adom-symbol / adom-footprint apps
# (mkviewers.mjs -> <slug>/symview.html, fpview.html), embedded same-origin.
def appviewer(fname,label,extra=""):
return f'<div class="viewer-cap"><span class="live">{label}</span><span>generated by the app, embedded{extra}</span></div><iframe class="viewer" src="{fname}" loading="lazy"></iframe>'
if os.path.exists(f"{OUT}/{slug}/symview.html"):
symview=appviewer("symview.html","adom-symbol SymView")
elif pub: symview=liveviewer("symbol","adom-symbol SymView")
else: symview=f'<div class="viewer-cap"><span class="src real">adom-symbol render</span></div><div class="render">{sym_svg}</div>'
if os.path.exists(f"{OUT}/{slug}/fpview.html"):
fpview=appviewer("fpview.html","adom-footprint FpView",", pin 1 top-left, toggle solder blobs")
elif pub: fpview=liveviewer("footprint","adom-footprint FpView")
else: fpview=f'<div class="viewer-cap"><span class="src real">adom-footprint render</span></div><div class="render">{fp_svg}</div>'
d3view = (liveviewer("3d","Adom 3D viewer (Babylon)") if pub else f'<p style="color:var(--text-2);font-size:14px">{("GLB present" if has_glb else "STEP present")}; live Babylon viewer when published.</p>')
# ---- hero ----
heroimg=""
hp=cfg.get("hero")
if hp and os.path.exists(hp): heroimg="data:image/png;base64,"+b64f(hp)
elif glob.glob(f"{D}/{mpn}-3d-iso-named.png"): heroimg="data:image/png;base64,"+b64f(glob.glob(f"{D}/{mpn}-3d-iso-named.png")[0])
hero_disp = f'<div class="hero"><img src="{heroimg}" alt="{mpn}"></div>' if heroimg else ""
# ---- toolbar buttons ----
mu=(stock or {}).get("mouser",{}).get("product_url") if stock else None
dk=f"https://www.digikey.com/en/products/result?keywords={mpn}"
ICc,ICs,ICf,ICd,ICdl,ICca=ic('cube'),ic('symbol'),ic('footprint'),ic('doc',14),ic('download',16),ic('cart',16)
if pub:
direct=f'<a class="btn go" target="_blank" href="https://wiki.adom.inc/viewer/3d/component/{pub}">{ICc} 3D viewer</a><a class="btn go" target="_blank" href="https://wiki.adom.inc/viewer/symbol/component/{pub}">{ICs} Symbol viewer</a><a class="btn go" target="_blank" href="https://wiki.adom.inc/viewer/footprint/component/{pub}">{ICf} Footprint viewer</a><a class="btn go" target="_blank" href="{wikiurl}">{ICd} Wiki page</a>'
directsrc='<span class="src real">real wiki viewers</span>'
else:
direct='<span class="btn dis">3D viewer</span><span class="btn dis">Symbol viewer</span><span class="btn dis">Footprint viewer</span><span class="src mock" style="align-self:center">live when published</span>'
directsrc='<span class="src mock">publish to enable</span>'
dlbtns="".join(f'<span class="btn" data-tip="{scrub(lbl)}">{ICdl} {f.split(".")[-1].upper()}</span>' for lbl,f in cad if glob.glob(f"{D}/{f}"))
buybtns=(f'<a class="btn buy" target="_blank" href="{mu}">{ICca} Mouser</a>' if mu else '<span class="btn dis">Mouser</span>')+f'<a class="btn buy" target="_blank" href="{dk}">{ICca} DigiKey</a>'
# ---- reel note ----
reel=("This part number ends in -R: Tape and Reel." if mpn.upper().endswith(("R","-R","R7")) or "R" in mpn[-2:] else "Check the distributor packaging (Cut Tape / Tape and Reel / Tube / Tray).")
# ---- resources ----
res=cfg.get("resources",[])
if res:
resources='<ul class="r">'+"".join(f'<li><a href="{u}" target="_blank">{scrub(t)}</a></li>' for t,u in res)+'<li class="src mock">Errata, app notes, reference designs: auto-list all supplemental docs (TODO).</li></ul>'
else:
resources='<p class="src mock">Manufacturer resource links not configured for this part (TODO: pull from info.json / product page).</p>'
# ---- variants (from ds2sf symbol.variants) ----
vs=sym.get('variants') or []
if vs:
rows="".join(f'<tr><td class="k">{scrub(v.get("package"))}</td><td>{scrub(v.get("pin_count"))} pins</td><td>{"pin-compatible" if v.get("compatible_pinout") else "NOT pin-compatible"}</td><td>{scrub((v.get("notes") or ""))[:160]}</td></tr>' for v in vs)
variants=f'<table><tr><th>Package</th><th>Pins</th><th>Compatibility</th><th>Notes</th></tr>{rows}</table><div class="cite"><b>Source:</b> ds2sf variant extraction.</div>'
else:
variants='<p class="src mock">No variant data extracted. TODO: list package / temperature / speed variants.</p>'
related=f'<p style="color:var(--text-2);font-size:14px"><b>Family:</b> {scrub(cfg.get("family","see manufacturer portfolio."))}</p><p style="color:var(--text-2);font-size:14px;margin-top:8px"><b>Molecules using this chip:</b> <span class="src mock">query adom-molecules; none indexed yet. TODO: auto backlink index.</span></p>'
HTML=TEMPLATE
reps={
"__MPN__":mpn,"__MFR__":mfr,"__PKG__":pkg,"__DESC__":desc,"__CLS__":cls_label,
"__PINCOUNT__":str(len(pins)),"__SYM__":sym_svg,"__FP__":fp_svg,
"__SPECS__":spec_html,"__PINS__":pin_html,"__DIMS__":dim_html or '<tr><td colspan="4" class="src mock">No dimension provenance.</td></tr>',
"__REVIEW__":rev_html or "<li>None flagged.</li>","__SRC__":src_html,"__CAD__":cad_html,
"__DSPAGES__":str(ds.get('page_count','?')),"__DSSHA__":scrub((ds.get('sha256') or '')[:16]),
"__TOOL__":scrub(tooling.get('extractor','ds2sf'))+" "+scrub(tooling.get('extractor_version',''))+", "+scrub(tooling.get('claude_model','')),
"__PROVIMGS__":prov_imgs,"__EVJS__":ev_js,
"__WIKIURL__":wikiurl,"__HERO__":hero_disp,"__DIRECTSRC__":directsrc,"__DIRECTBTNS__":direct,
"__DLBTNS__":dlbtns,"__BUYBTNS__":buybtns,"__SYMVIEW__":symview,"__FPVIEW__":fpview,"__3DVIEW__":d3view,
"__REELNOTE__":scrub(reel),"__RESOURCES__":resources,"__VARIANTS__":variants,"__RELATED__":related,
"__IC_OPEN14__":ic("open",14),"__IC_BOX__":ic("box"),"__IC_BUILD__":ic("building"),"__IC_LAYERS__":ic("layers"),"__IC_LINK__":ic("link"),
"__FAM5__":FAM5,"__FAM7__":FAM7,"__SAT4__":SAT4,"__SAT5__":SAT5,"__SAT7__":SAT7,
"__IC_CHIP__":ic("chip"),"__IC_OPEN__":ic("open",16),"__IC_SYM__":ic("symbol"),"__IC_FP__":ic("footprint"),
"__IC_CUBE__":ic("cube"),"__IC_GRID__":ic("grid"),"__IC_LIST__":ic("list"),"__IC_DOLLAR__":ic("dollar"),
"__IC_BOX__":ic("box"),"__IC_BUILD__":ic("building"),"__IC_LAYERS__":ic("layers"),"__IC_LINK__":ic("link"),
"__IC_SHIELD__":ic("shield"),"__IC_DL__":ic("download",16),"__IC_DOC__":ic("doc",14),"__IC_WARN__":ic("warn",16),"__IC_CART__":ic("cart",16),
}
for k,v in reps.items(): HTML=HTML.replace(k,v)
bad=[c for c in HTML if c in "—–…−" or ord(c)>0x1F000]
print(f"{mpn}: forbidden chars:", sorted(set(bad)) if bad else "NONE", "| cited pages cropped:", sorted(crops.keys()), "| class:", cls, "| pins:", len(pins))
os.makedirs(f"{OUT}/{slug}",exist_ok=True)
open(f"{OUT}/{slug}/index.html","w",encoding="utf-8").write(HTML)
# copy CAD downloads into served dir
for lbl,f in cad:
srcf=glob.glob(f"{D}/{f}")
if srcf:
import shutil; shutil.copy(srcf[0], f"{OUT}/{slug}/{f}")
print(f" wrote {OUT}/{slug}/index.html ({len(HTML)} bytes)")
TEMPLATE=open('/tmp/gen/template.html',encoding='utf-8').read()
if __name__=="__main__":
gen("nRF54L15","nrf54l15")
gen("DMG2305UX-7","dmg2305")
gen("LM358D","lm358d")
gen("BME688","bme688")
gen("TPS25751SRSMR","tps25751")
gen("WSL2010R0500FEA","wsl2010")