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>""")