Source code for molfoundry.post

import shutil
from pathlib import Path
from typing import List


def is_running_in_jupyter():
    try:
        from IPython import get_ipython
        shell = get_ipython().__class__.__name__
        if shell == "ZMQInteractiveShell":
            return True   # Jupyter notebook or JupyterLab
        if shell == "TerminalInteractiveShell":
            return False  # IPython terminal
        return False
    except:
        return False


SUMMARY_PATH = Path('./summary')
RUNNING_IN_JUPYTER = is_running_in_jupyter()
_post_enabled = False
_output: List[str] = []
_chapter_counter = 0
_section_counter = 0
# Standalone files (e.g. interactive stochsim viewers) to drop into the summary
# folder alongside summary.html. Written by run_post *after* it recreates the
# folder, so they survive the rmtree; keyed by filename to de-duplicate.
_extra_files: "dict[str, str]" = {}


[docs] def enable_post(): global _post_enabled _post_enabled = True
# noinspection PyPep8Naming def registerExtraFile(filename: str, html: str): """Register a standalone HTML file to be written into the summary folder. Enables the post pipeline so the file is flushed at exit. In Jupyter (where nothing is written to disk) the page is shown inline in an iframe instead. """ global _extra_files, _post_enabled _post_enabled = True if RUNNING_IN_JUPYTER: import html as _html from IPython.display import HTML, display srcdoc = _html.escape(html, quote=True) display(HTML( f'<iframe srcdoc="{srcdoc}" style="width:100%;height:660px;' f'border:1px solid var(--bs-border-color,#ccc);border-radius:12px;">' f'</iframe>' )) return _extra_files[filename] = html
[docs] def disable_post(): global _post_enabled _post_enabled = False
# noinspection PyPep8Naming def summaryRaw(source: str): global _output if RUNNING_IN_JUPYTER: from IPython.display import HTML, display display(HTML(source)) else: _output.append(source) # noinspection PyPep8Naming
[docs] def postChapter(title: str): global _output, _chapter_counter _chapter_counter += 1 source = f"""<h1>{_chapter_counter}. {title}</h1> """ if RUNNING_IN_JUPYTER: from IPython.display import HTML, display display(HTML(source)) else: _output.append(source)
# noinspection PyPep8Naming
[docs] def postSection(title: str): global _output, _section_counter _section_counter += 1 source = f"""<h2>{_chapter_counter}.{_section_counter}. {title}</h2> """ if RUNNING_IN_JUPYTER: from IPython.display import HTML, display display(HTML(source)) else: _output.append(source)
def run_post(): global _post_enabled, _output if not _post_enabled: # or len(_output) == 0: return if RUNNING_IN_JUPYTER: return if len(_output) == 0 and len(_extra_files) == 0: return if SUMMARY_PATH.exists(): shutil.rmtree(SUMMARY_PATH) SUMMARY_PATH.mkdir() # Standalone extra files (interactive viewers) next to summary.html. for filename, html in _extra_files.items(): with open(SUMMARY_PATH / filename, 'w', encoding='utf-8') as f: f.write(html) if len(_output) == 0: return with open(SUMMARY_PATH / 'summary.html', 'w', encoding='utf-8') as f: f.write("""<!doctype html> <html lang="en" data-bs-theme="light"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>MolFoundry Summary</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous"> <style> body { transition: background-color 0.3s, color 0.3s; } .floating-toolbar { position: fixed; top: 8px; left: 50%; transform: translateX(-50%); width: 90%; z-index: 1050; border-radius: 16px; box-shadow: 0 4px 4px rgba(0,0,0,0.15); } .btn-group-tiny>.btn, .btn-tiny { --bs-btn-padding-x: 0.5rem; --bs-btn-font-size: 0.6rem; --bs-btn-line-height: 1; height: 24px; --bs-btn-border-radius: var(--bs-border-radius-sm); } .dropdown-menu { border-radius: 16px; overflow-y: auto; overflow-x: hidden; max-height: 180px; width: 300px; transform: translate(-100px, 33px) !important; } </style> </head> <body> <nav class="floating-toolbar pe-2 ps-4 py-2 d-flex align-items-center justify-content-between bg-body border"> <span class="fw-bold fs-5">Summary</span> <div class="dropdown"> <button class="btn btn-outline-secondary btn-sm rounded-pill px-3 dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> Navigate </button> <ul class="dropdown-menu" id="navDropdown"></ul> </div> <button id="themeToggle" class="btn btn-outline-secondary btn-sm rounded-pill px-3 d-flex align-items-center gap-2"> <span id="themeIcon">🌙</span> </button> </nav> <div style="margin-top: 72px" class="container-fluid"> """) f.write('\n'.join(_output)) f.write(""" </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script> <script> document.getElementById('themeToggle').addEventListener('click', () => { const isDark = document.documentElement.getAttribute('data-bs-theme') === 'dark'; document.documentElement.setAttribute('data-bs-theme', isDark ? 'light' : 'dark'); document.getElementById('themeIcon').textContent = isDark ? '🌙' : '☀️'; }); function buildNavDropdown() { const dropdown = document.getElementById('navDropdown'); dropdown.innerHTML = ''; const targets = document.querySelectorAll('h1, h2, .card-title'); if (targets.length === 0) { dropdown.innerHTML = '<li><span class="dropdown-item text-muted">No sections found</span></li>'; return; } const INDENT_PER_LEVEL = 12; let hasH1 = false; let hasH2 = false; targets.forEach((el, i) => { if (!el.id) { el.id = `nav-target-${i}`; } const tag = el.tagName?.toLowerCase(); const isH1 = tag === 'h1'; const isH2 = tag === 'h2'; const isCardTitle = el.classList.contains('card-title'); // Compute rank based on what actually appeared before this element let rank = 0; if (isH1) { rank = 0; hasH1 = true; hasH2 = false; // reset h2 context when a new h1 appears } else if (isH2) { rank = hasH1 ? 1 : 0; hasH2 = true; } else if (isCardTitle) { if (hasH2) { rank = 2; } else if (hasH1) { rank = 1; } else { rank = 0; } } const text = el.textContent.trim(); const li = document.createElement('li'); const a = document.createElement('a'); a.className = 'dropdown-item'; a.href = `#${el.id}`; a.textContent = text; a.style.paddingLeft = `${1 + rank * INDENT_PER_LEVEL / 16}rem`; a.style.fontSize = `${1 - rank * 0.05}rem`; a.style.opacity = `${1 - rank * 0.15}`; a.addEventListener('click', (e) => { e.preventDefault(); const top = el.getBoundingClientRect().top + window.scrollY - 72; window.scrollTo({ top, behavior: 'smooth' }); }); li.appendChild(a); dropdown.appendChild(li); }); } buildNavDropdown(); </script> </body> </html>""")