import os, re, json
EXAMPLES_ROOT = "/home/pep/Projects/processing-cpp.github.io/assets/examples"
EXAMPLES_JS_ROOT = "/home/pep/Projects/processing-cpp.github.io/assets/examples_js"
def strip_comment(code):
code = code.strip()
if code.startswith("/**"):
end = code.find("*/")
if end != -1:
code = code[end+2:].strip()
return code
def get_canvas_size(js_code):
m = re.search(r'createCanvas\s*\(\s*(\d+)\s*,\s*(\d+)', js_code)
if m: return int(m.group(1)), int(m.group(2))
m = re.search(r'\bsize\s*\(\s*(\d+)\s*,\s*(\d+)', js_code)
if m: return int(m.group(1)), int(m.group(2))
return 640, 360
def scan_section(section_name):
"""
Scans assets/examples////.pde
(and the matching assets/examples_js//... for the JS
translation, if it exists yet) into the same {category: [examples]}
shape the rest of this script already expects from `data`.
Used for both "Basics" (the only section with content today) and
"Topics" (currently empty placeholder folders -- this picks it up
automatically the moment real .pde/.js files are added, with zero
further changes needed here).
"""
base_cpp = os.path.join(EXAMPLES_ROOT, section_name)
base_js = os.path.join(EXAMPLES_JS_ROOT, section_name)
if not os.path.isdir(base_cpp):
return {}
section_data = {}
for cat in sorted(os.listdir(base_cpp)):
cat_path = os.path.join(base_cpp, cat)
if not os.path.isdir(cat_path):
continue
examples = []
for example in sorted(os.listdir(cat_path)):
ex_path = os.path.join(cat_path, example)
if not os.path.isdir(ex_path):
continue
pde = os.path.join(ex_path, example + ".pde")
if not os.path.exists(pde):
continue
with open(pde) as f:
code = f.read()
js_file = os.path.join(base_js, cat, example, example + ".js")
js_code = ""
w, h = 640, 360
if os.path.exists(js_file):
with open(js_file) as f:
js_code = strip_comment(f.read())
w, h = get_canvas_size(js_code)
slug = example.replace("_", "-").lower()
examples.append({
"id": section_name + "_" + cat + "_" + example,
"name": example.replace("_", " "),
"slug": slug,
"code": code,
"js": js_code,
"w": w,
"h": h,
})
if examples: # skip categories that exist as empty folders with no real content yet
section_data[cat] = examples
return section_data
# Every top-level folder under assets/examples/ is treated as a section
# (e.g. "Basics", "Topics") -- discovered automatically, not hardcoded, so
# adding real content under an existing empty Topics// folder
# is all that's needed to make it show up here; no script change required.
SECTION_NAMES = sorted(os.listdir(EXAMPLES_ROOT)) if os.path.isdir(EXAMPLES_ROOT) else []
sections = {}
for _section_name in SECTION_NAMES:
if not os.path.isdir(os.path.join(EXAMPLES_ROOT, _section_name)):
continue
scanned = scan_section(_section_name)
if scanned: # only keep sections that actually have at least one real example
sections[_section_name] = scanned
# `data` is kept as an alias for the first/primary section (normally
# "Basics") for backwards compatibility with anything below that hasn't
# been updated to iterate over `sections` yet.
data = sections.get("Basics", {})
def build_page_sidebar(active_id=""):
s = ""
for i, (section_name, section_data) in enumerate(sections.items()):
key = section_name.lower()
# First section starts open, the rest start collapsed -- matches
# the original Basics-open/Topics-collapsed behavior.
open_by_default = True
arrow = "â¾"
display = "block"
s += (
f''
f''
)
for cat, examples in section_data.items():
s += f'
{cat.replace("_"," ").title()}
'
for ex in examples:
active = 'class="active"' if ex["id"] == active_id else ""
s += f'
{ex["name"]}'
s += '
'
s += '
'
# Any section folder that exists but has no real example content yet
# (e.g. Topics before its .pde/.js files are added) still gets a
# collapsed "Coming soon" entry, so the sidebar doesn't just silently
# omit it.
empty_sections = [
name for name in SECTION_NAMES
if name not in sections and os.path.isdir(os.path.join(EXAMPLES_ROOT, name))
]
for name in empty_sections:
key = name.lower()
s += (
f''
f''
)
return s
shared_css = '''* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #111; background: #fff; }
a { color: #111; text-decoration: none; }
nav { border-bottom: 1px solid #e0e0e0; padding: 0 2rem; display: flex; align-items: center; justify-content: space-between; height: 60px; position: sticky; top: 0; background: #fff; z-index: 100; }
.hamburger { background: none; border: none; cursor: pointer; font-size: 22px; padding: 4px 8px; display: none; }
.layout { display: flex; min-height: calc(100vh - 60px); }
.sidebar-outer { width: 220px; min-width: 220px; border-right: 1px solid #e0e0e0; display: flex; flex-direction: column; position: sticky; top: 60px; height: calc(100vh - 60px); }
#site-sidebar { padding: 1.5rem 1.5rem 1rem; border-bottom: 1px solid #e0e0e0; display: flex; flex-direction: column; }
#site-sidebar a { font-size: 14px; color: #555; padding: 0.4rem 0; display: block; }
#site-sidebar a:hover { color: #111; }
#site-sidebar a.active { color: #111; font-weight: 500; }
.sidebar-examples { flex: 1; overflow-y: auto; }
.section-header { display: flex; justify-content: space-between; align-items: center; padding: 0.85rem 1.5rem; font-size: 13px; font-weight: 600; color: #111; cursor: pointer; border-bottom: 1px solid #e0e0e0; user-select: none; }
.section-header:hover { background: #f8f8f8; }
.arrow { font-size: 11px; color: #aaa; }
.category { margin-bottom: 0.25rem; }
.category-title { font-size: 11px; font-weight: 600; color: #aaa; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.75rem 1.5rem 0.25rem; }
.category a { display: block; font-size: 13px; color: #555; padding: 0.3rem 1.5rem; }
.category a:hover { color: #111; background: #f8f8f8; }
.category a.active { color: #111; font-weight: 500; background: #f4f4f4; }
.content { flex: 1; padding: 3rem 4rem; max-width: 900px; }
.content h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 0.75rem; color: #e8b400; }
.preview-wrap { border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; margin-bottom: 2rem; display: block; max-width: 100%; width: fit-content; }
.preview-wrap iframe { display: block; border: none; max-width: 100%; }
.code-block { background: #f8f8f8; border-radius: 8px; overflow: hidden; }
.code-header { padding: 0.75rem 1.25rem; border-bottom: 1px solid #e0e0e0; font-size: 12px; color: #888; font-family: monospace; display: flex; align-items: center; justify-content: space-between; }
.copy-btn { font-size: 12px; color: #555; background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 3px 10px; cursor: pointer; font-family: inherit; }
.copy-btn:hover { background: #f0f0f0; }
.copy-btn.copied { color: #090; border-color: #090; }
pre { padding: 1.5rem; font-family: "SF Mono","Fira Code",monospace; font-size: 13px; line-height: 1.7; overflow-x: auto; white-space: pre; }
.welcome h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 1rem; }
.welcome p { color: #555; line-height: 1.8; max-width: 500px; }
footer { border-top: 1px solid #e0e0e0; padding: 2rem; text-align: center; font-size: 13px; color: #888; }
.footer-contact { margin-top: 0.4rem; font-size: 12px; }
.footer-contact a { color: #aaa; border-bottom: 1px solid transparent; }
.footer-contact a:hover { color: #111; border-bottom-color: #111; }
.footer-sep { color: #ccc; margin: 0 0.5rem; }
@media (max-width: 768px) {
.hamburger { display: block; }
.sidebar-outer { position: fixed; top: 60px; left: -240px; width: 240px; height: calc(100vh - 60px); background: #fff; z-index: 200; transition: left 0.25s ease; box-shadow: 2px 0 12px rgba(0,0,0,0.08); }
.sidebar-outer.open { left: 0; }
.content { padding: 2rem 1.25rem; }
.preview-wrap { max-width: 100%; }
pre { font-size: 12px; }
}'''
shared_js = '''function copyCode() {
navigator.clipboard.writeText(document.getElementById('code-pre').innerText).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'Copied!'; btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
});
}
function toggleSection(name) {
const sec = document.getElementById(name+'-section');
const arrow = document.getElementById(name+'-arrow');
const open = sec.style.display !== 'none';
sec.style.display = open ? 'none' : 'block';
arrow.textContent = open ? 'â¸' : 'â¾';
}'''
def fix_asset_paths(js_code):
base = "https://processing-cpp.github.io/assets/data/"
pat = re.compile(r"""load(Image|Font|Model)\s*\(\s*["']([^"']+)["']\s*((?:,[^)]*)?)\)""")
def replacer(m):
return f'load{m.group(1)}("{base}{m.group(2)}"{m.group(3)})'
return pat.sub(replacer, js_code)
def make_iframe(js_code, w, h):
js_code = fix_asset_paths(js_code)
safe = js_code.replace('', '<\\/script>').replace('`','\\`')
return f'''