diff --git a/.gitignore b/.gitignore
index 9dee836..37c2e02 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
_build
*.pyc
+
+.vscode/
\ No newline at end of file
diff --git a/_static/custom.css b/_static/custom.css
new file mode 100644
index 0000000..cc50cc4
--- /dev/null
+++ b/_static/custom.css
@@ -0,0 +1,209 @@
+:root {
+ --pst-color-border: rgba(0, 0, 0, 0.125) !important;
+ }
+
+ .bd-main .bd-content .bd-article-container {
+ max-width: 100%; /* default is 60em */
+ }
+ .bd-page-width {
+ max-width: 100%; /* default is 88rem */
+ }
+
+ .sd-card-footer {
+ background: rgba(var(--spt-color-gray-100), 1) !important;
+ padding: 4px;
+ }
+
+ main.banner-main #project-pythia {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ }
+
+ main.banner-main #project-pythia p {
+ font-size: 1.4rem; /* default: 1.25rem */
+ /* font-weight: 700; default: 300 */
+ }
+
+ main.banner-main #project-pythia a,
+ main.banner-main #project-pythia a:visited {
+ color: rgba(var(--spt-color-light), 1);
+ text-decoration: underline dotted rgba(var(--spt-color-gray-400), 1);
+ }
+
+ main.banner-main #project-pythia a.headerlink:hover {
+ color: #DDD;
+ }
+
+ main.banner-main #project-pythia a.btn-light {
+ color: rgba(var(--pst-color-primary), 1)
+ }
+
+ .modal {
+ display: none;
+ position: fixed;
+ background: #f8f9fa;
+ border-radius: 5px;
+ padding: 3rem;
+ width: calc(100% - 8rem);
+ height: auto !important;
+ max-height: calc(100% - 8rem);
+ overflow: scroll;
+ top: 4rem;
+ left: 4rem;
+ z-index: 20001;
+ }
+
+ .modal-backdrop {
+ display: none;
+ position: fixed;
+ background: rgba(0, 0, 0, 0.5);
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ z-index: 20000;
+ }
+
+ .modal-btn {
+ color: #1a658f;
+ text-decoration: none;
+ }
+
+ .modal-img {
+ float: right;
+ margin: 0 0 2rem 2rem;
+ max-width: 260px;
+ max-height: 260px;
+ }
+
+ .gallery-menu {
+ margin-bottom: 1rem;
+ }
+
+ .gallery-card div.container {
+ padding: 0 0 0 1rem;
+ }
+
+ .gallery-thumbnail {
+ display: block;
+ float: left;
+ margin: auto 0;
+ padding: 0;
+ max-width: 160px;
+ background: transparent !important;
+ }
+
+ .card-subtitle {
+ font-size: 0.8rem;
+ }
+
+ .my-2 {
+ color: inherit;
+ }
+
+ .text-decoration-none {
+ text-decoration: none;
+ color: inherit;
+ }
+
+ @media (max-width: 576px) {
+ .modal {
+ padding: 2rem;
+ width: calc(100% - 4rem);
+ max-height: calc(100% - 4rem);
+ top: 2rem;
+ left: 2rem;
+ }
+
+ .modal-img {
+ display: none;
+ }
+
+ .gallery-card {
+ flex-direction: column;
+ }
+
+ .gallery-thumbnail {
+ float: none;
+ margin: 0 0 1rem 0;
+ max-width: 100%;
+ }
+
+ .gallery-card div.container {
+ padding: 0;
+ }
+
+ .gallery-return-btn {
+ padding-bottom: 1rem;
+ }
+ }
+
+ div.horizontalgap {
+ float: left;
+ overflow: hidden;
+ height: 1px;
+ width: 0px;
+ }
+
+ .badge.mybadges {
+ margin-bottom: 0;
+ font-weight: 0;
+ }
+
+ .tagsandbadges {
+ padding: 0 0;
+ }
+
+ .dropdown ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ .dropdown-item {
+ display: block;
+ }
+
+ .dropdown-item input[type="checkbox"] {
+ margin-right: 0.5em;
+ }
+
+ details.sd-dropdown {
+ box-shadow: none !important;
+ }
+
+ details.sd-dropdown summary.sd-card-header + div.sd-summary-content {
+ background-color: white !important;
+ border: 0.2rem solid var(--pst-sd-dropdown-color) !important;
+ border-radius: calc(.25rem - 1px);
+ }
+
+ .sd-summary-content.sd-card-body.docutils {
+ position: absolute;
+ z-index: 100;
+ }
+
+ details.sd-dropdown:not([open])>.sd-card-header {
+ background-color: white !important;
+ border: 2px solid #1a648f !important;
+ color: #1a648f;
+ border-radius: .5rem;
+ }
+
+ details.sd-dropdown[open]>.sd-card-header {
+ background-color: #1a648f !important;
+ color: white;
+ border-radius: .5rem;
+ }
+
+ p {
+ color: black;
+ }
+
+ main.bd-content #main-content a {
+ color: #1a648f;
+ }
+
+ .sd-col.sd-d-flex-row.docutils.has-visible-card {
+ margin-bottom: 1rem;
+ }
\ No newline at end of file
diff --git a/_static/custom.js b/_static/custom.js
new file mode 100644
index 0000000..07d4ea7
--- /dev/null
+++ b/_static/custom.js
@@ -0,0 +1,92 @@
+function getClassOfCheckedCheckboxes(checkboxes) {
+ var tags = [];
+ checkboxes.forEach(function (cb) {
+ if (cb.checked) {
+ tags.push(cb.getAttribute("rel"));
+ }
+ });
+ return tags;
+ }
+
+ function change() {
+ console.log("Change event fired.");
+ var domainsCbs = document.querySelectorAll(".domains input[type='checkbox']");
+ var packagesCbs = document.querySelectorAll(".packages input[type='checkbox']");
+
+ var domainTags = getClassOfCheckedCheckboxes(domainsCbs);
+ var packageTags = getClassOfCheckedCheckboxes(packagesCbs);
+
+ var filters = {
+ domains: domainTags,
+ packages: packageTags
+ };
+
+ filterResults(filters);
+ }
+
+ function filterResults(filters) {
+ console.log("Filtering results...");
+ var rElems = document.querySelectorAll(".tagged-card");
+
+ rElems.forEach(function (el) {
+ var isVisible = true; // Assume visible by default
+
+ // Check if the element has any domain or package filter
+ if (filters.domains.length > 0 || filters.packages.length > 0) {
+ var hasMatchingDomain = filters.domains.length === 0 || filters.domains.some(domain => el.classList.contains(domain));
+ var hasMatchingPackage = filters.packages.length === 0 || filters.packages.some(package => el.classList.contains(package));
+
+ // The element should be visible if it matches any filter within each category
+ isVisible = hasMatchingDomain && hasMatchingPackage;
+ }
+
+ // Toggle visibility based on the result
+ if (isVisible) {
+ el.classList.remove("d-none");
+ el.classList.add("d-flex");
+ } else {
+ el.classList.remove("d-flex");
+ el.classList.add("d-none");
+ }
+ });
+
+ // Update the margins after filtering
+ updateMargins();
+ }
+
+ var checkboxes = document.querySelectorAll('input[type="checkbox"]');
+ checkboxes.forEach(function (checkbox) {
+ checkbox.addEventListener("change", change);
+ });
+
+ function updateMargins() {
+ const columns = document.querySelectorAll('.sd-col.sd-d-flex-row.docutils');
+
+ columns.forEach(column => {
+ // Check if this column has any visible cards
+ const hasVisibleCard = Array.from(column.children).some(child => !child.classList.contains('d-none'));
+
+ // Toggle a class based on whether there are visible cards
+ if (hasVisibleCard) {
+ column.classList.add('has-visible-card');
+ } else {
+ column.classList.remove('has-visible-card');
+ }
+ });
+ }
+
+ function clearCbs() {
+ // Select all checkbox inputs and uncheck them
+ var checkboxes = document.querySelectorAll('input[type="checkbox"]');
+ checkboxes.forEach(function(checkbox) {
+ checkbox.checked = false;
+ });
+
+ change();
+ }
+
+ // Initial call to set up correct margins when the page loads
+ document.addEventListener('DOMContentLoaded', updateMargins);
+
+ console.log("Script loaded.");
+
\ No newline at end of file
diff --git a/myst.yml b/myst.yml
index c4d4741..1ae7332 100644
--- a/myst.yml
+++ b/myst.yml
@@ -20,3 +20,6 @@ site:
options:
hide_toc: true
hide_outline: true
+html:
+ extra_head:
+ -
diff --git a/pythia-gallery.py b/pythia-gallery.py
index a815b35..739faf9 100755
--- a/pythia-gallery.py
+++ b/pythia-gallery.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
import sys
import json
import urllib.request
@@ -32,44 +32,58 @@ def fetch_yaml(url: str):
return yaml.load(body, yaml.SafeLoader)
-def render_cookbook(name: str):
+def fetch_cookbook_data(name):
+ """Fetch and return cookbook metadata from myst.yml and _gallery_info.yml."""
try:
- print(f"Rendering {name}", file=sys.stderr, flush=True)
- raw_base_url = (
- f"https://raw.githubusercontent.com/ProjectPythia-MystMD/{name}/main"
- )
- config_url = f"{raw_base_url}/myst.yml"
- book_url = f"https://projectpythia-mystmd.github.io/{name}"
+ print(f"Fetching metadata for {name}", file=sys.stderr, flush=True)
+ raw_base_url = f"https://raw.githubusercontent.com/ProjectPythia-MystMD/{name}/main"
- # Load JB data
+ # Load myst.yml
+ config_url = f"{raw_base_url}/myst.yml"
config = fetch_yaml(config_url)
title = config["project"]["title"]
- # Fetch gallery metadata
+ # Load _gallery_info.yml
gallery_url = f"{raw_base_url}/_gallery_info.yml"
gallery_data = fetch_yaml(gallery_url)
- image_name = gallery_data["thumbnail"]
- image_url = f"{raw_base_url}/{image_name}"
- # Build tags
- tags = gallery_data["tags"]
+ # Extract image details and tags
+ image_url = f"{raw_base_url}/{gallery_data['thumbnail']}"
+ tags = gallery_data.get("tags", {})
+ return {
+ "title": title,
+ "book_url": f"https://projectpythia-mystmd.github.io/{name}",
+ "image_url": image_url,
+ "tags": tags, # Dictionary of {"domains": [...], "packages": [...]}
+ }
+
+ except Exception as err:
+ print(f"Error fetching data for {name}", file=sys.stderr)
+ traceback.print_exception(err, file=sys.stderr)
+ return None
+
+
+def render_cookbook(name, data):
+ """Render a cookbook card from fetched metadata."""
+ try:
+ print(f"Rendering {name}", file=sys.stderr, flush=True)
return {
"type": "card",
- "url": book_url,
+ "url": data["book_url"],
+ "class": ["tagged-card"] + [item for _, items in data["tags"].items() for item in items],
"children": [
- {"type": "cardTitle", "children": [text(title)]},
+ {"type": "cardTitle", "children": [text(data["title"])]},
div(
[
- image(image_url),
+ image(data["image_url"]),
div(
[
span(
[text(item)],
- style=styles.get(name, DEFAULT_STYLE),
+ style=styles.get(category, DEFAULT_STYLE),
)
- for name, items in tags.items()
- if items is not None
+ for category, items in data["tags"].items() if items
for item in items
]
),
@@ -85,9 +99,67 @@ def render_cookbook(name: str):
def render_cookbooks(pool):
with open("cookbook_gallery.txt") as f:
- body = f.read()
-
- return [c for c in pool.map(render_cookbook, body.splitlines()) if c is not None]
+ body = f.read().splitlines()
+
+ # Fetch all cookbook data in parallel
+ cookbook_entries = list(pool.map(fetch_cookbook_data, body))
+
+ cookbook_data = {}
+ tag_lists = {"domains": set(), "packages": set()}
+
+ for name, data in zip(body, cookbook_entries):
+ if data:
+ cookbook_data[name] = render_cookbook(name, data) # Use pre-fetched data
+
+ # Collect tags into tag_lists
+ for category in tag_lists.keys():
+ tag_lists[category].update(data["tags"].get(category, []))
+
+ cookbook_nodes = [cookbook_data[name] for name in body if name in cookbook_data]
+
+ # Generate checkboxes for domains
+ domains_controls = div(
+ [
+ span([text("Filter by Domain:")], style={"fontWeight": "bold"}),
+ *[
+ div(
+ [
+ # f', ' {tag}'
+ # text(f" -[] {tag}")
+ text(f" {tag}")
+ ],
+ style=styles['domains']
+ )
+ for tag in sorted(tag_lists["domains"])
+ ]
+ ],
+ **{"class": ["domains"]}
+ )
+
+ # Generate checkboxes for packages
+ packages_controls = div(
+ [
+ span([text("Filter by Package:")], style={"fontWeight": "bold"}),
+ *[
+ div(
+ [
+ text(f" {tag}")
+ ],
+ style=styles['packages']
+)
+ for tag in sorted(tag_lists["packages"])
+ ]
+ ],
+ **{"class": ["packages"]}
+ )
+
+ # Combine both controls in a container
+ controls_node = div(
+ [domains_controls, packages_controls],
+ **{"class": ["filter-controls"]}
+ )
+ cookbook_nodes = [cookbook_data[name] for name in body if name in cookbook_data]
+ return controls_node, cookbook_nodes
def run_directive(name, data):
@@ -97,18 +169,13 @@ def run_directive(name, data):
def run_transform(name, data):
with concurrent.futures.ThreadPoolExecutor() as pool:
- # Find our cookbook nodes in the AST
cookbook_nodes = find_all_by_type(data, "pythia-cookbooks")
-
- # In-place mutate the AST to replace cookbook nodes with card grids
- children = render_cookbooks(pool)
-
- # Mutate our cookbook nodes in-place
+ controls_node, card_nodes = render_cookbooks(pool)
+ grid_node = grid([1, 1, 2, 3], card_nodes)
for node in cookbook_nodes:
node.clear()
- node.update(grid([1, 1, 2, 3], children))
- node["children"] = children
-
+ node["type"] = "container"
+ node["children"] = [controls_node, grid_node]
return data
@@ -144,4 +211,4 @@ def run_transform(name, data):
elif args.role:
raise NotImplementedError
else:
- json.dump(PLUGIN_SPEC, sys.stdout)
+ json.dump(PLUGIN_SPEC, sys.stdout)
\ No newline at end of file