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