From 539553be131fb796d32dd0ded003729924fad6a1 Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 19 Sep 2026 17:02:52 +0300 Subject: [PATCH 1/4] add mod categories --- _config.yml | 10 ++++ _config_oak.yml | 4 ++ _config_oak2.yml | 4 ++ _config_willow1.yml | 4 ++ _config_willow2.yml | 4 ++ _data/categories.yml | 14 +++++ _layouts/category_index.html | 21 +++++++ _layouts/category_list.html | 17 ++++++ _layouts/mod.html | 12 ++++ _plugins/mod-category-gen.rb | 81 ++++++++++++++++++++++++++ developing/releasing_your_mod/index.md | 7 ++- 11 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 _data/categories.yml create mode 100644 _layouts/category_index.html create mode 100644 _layouts/category_list.html create mode 100644 _plugins/mod-category-gen.rb diff --git a/_config.yml b/_config.yml index bf3b047a..070e5e01 100644 --- a/_config.yml +++ b/_config.yml @@ -28,6 +28,16 @@ defaults: type: willow2_mods values: layout: mod + - scope: + path: "" + type: category_index + values: + layout: category_index + - scope: + path: "" + type: category_list + values: + layout: category_list - scope: path: "" values: diff --git a/_config_oak.yml b/_config_oak.yml index ad061235..e5d0e070 100644 --- a/_config_oak.yml +++ b/_config_oak.yml @@ -10,6 +10,10 @@ collections: permalink: /oak-mod-db/mods/:name/ output: true +category_index: + enabled: true + url: /oak-mod-db/categories/ + game_selector: collection: oak_mods games: diff --git a/_config_oak2.yml b/_config_oak2.yml index 4f0b453b..2bf0b6e8 100644 --- a/_config_oak2.yml +++ b/_config_oak2.yml @@ -10,6 +10,10 @@ collections: permalink: /oak2-mod-db/mods/:name/ output: true +category_index: + enabled: true + url: /oak2-mod-db/categories/ + just_the_docs: collections: oak2_mods: diff --git a/_config_willow1.yml b/_config_willow1.yml index 921c645c..36d508c2 100644 --- a/_config_willow1.yml +++ b/_config_willow1.yml @@ -10,6 +10,10 @@ collections: permalink: /willow1-mod-db/mods/:name/ output: true +category_index: + enabled: true + url: /willow1-mod-db/categories/ + game_selector: collection: willow1_mods games: diff --git a/_config_willow2.yml b/_config_willow2.yml index 093b7f21..80169ca9 100644 --- a/_config_willow2.yml +++ b/_config_willow2.yml @@ -10,6 +10,10 @@ collections: permalink: /willow2-mod-db/mods/:name/ output: true +category_index: + enabled: true + url: /willow2-mod-db/categories/ + game_selector: collection: willow2_mods games: diff --git a/_data/categories.yml b/_data/categories.yml new file mode 100644 index 00000000..bc60eef1 --- /dev/null +++ b/_data/categories.yml @@ -0,0 +1,14 @@ +# This file lists the categories we accept. +# The top level keys are what you add to your mod file. All keys are lowercase, and use dashes in +# place of spaces multiple words. +# Each value can then have a custom 'title', to fix spaces and/or capitalization, and a custom +# 'description', which is used in the category list. + +lorem: + title: Lorem Ipsum + description: > + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate + velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non + proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/_layouts/category_index.html b/_layouts/category_index.html new file mode 100644 index 00000000..02dc6436 --- /dev/null +++ b/_layouts/category_index.html @@ -0,0 +1,21 @@ +--- +layout: default +--- +{%- assign category = site.data.categories[page.mod_category] -%} +

{{ page.title }}

+{%- if category.description -%} + {{- category.description -}} +
+
+{%- endif -%} + diff --git a/_layouts/category_list.html b/_layouts/category_list.html new file mode 100644 index 00000000..d883d710 --- /dev/null +++ b/_layouts/category_list.html @@ -0,0 +1,17 @@ +--- +layout: default +--- +

{{- page.title -}}

+ diff --git a/_layouts/mod.html b/_layouts/mod.html index e2be4520..cbf9daab 100644 --- a/_layouts/mod.html +++ b/_layouts/mod.html @@ -7,6 +7,18 @@ {%- endunless -%}

{{ page.title }}

+{%- if site.category_index.enabled -%} + {%- assign categories = page.mod_categories | split: " " -%} + {%- for raw_category in categories -%} + + {%- assign category = site.data.categories[raw_category] -%} + {{- category.title | default: raw_category -}} + + {%- unless forloop.last -%} + {{- ", " -}} + {%- endunless -%} + {%- endfor -%} +{%- endif -%}
By
{%- assign pyproject_authors = page.pyproject.project.authors diff --git a/_plugins/mod-category-gen.rb b/_plugins/mod-category-gen.rb new file mode 100644 index 00000000..4fd5c3e2 --- /dev/null +++ b/_plugins/mod-category-gen.rb @@ -0,0 +1,81 @@ +class CategoryPageGenerator < Jekyll::Generator + safe true + + def generate(site) + if !(site.config["category_index"] || {})["enabled"] then + return + end + + # It turns out the normal tag/category system only applies to *posts*, not all pages + # Instead, we have to reimplement its logic, but do it for everything + @mod_categories = Hash.new { |hash, key| hash[key] = [] } + + site.collections.each_value do |collection| + collection.docs.each do |document| + collect_categories(document) + end + end + site.pages.flatten.each do |page| + collect_categories(page) + end + + if @mod_categories.empty? then + return + end + + site.pages << CategoryList.new(site, @mod_categories.keys()) + @mod_categories.each do |category, pages| + site.pages << CategoryIndex.new(site, category, pages) + end + end + + def collect_categories(page) + (page.data["mod_categories"] || "").split().reject { |cat| cat.empty? }.each do |cat| + @mod_categories[cat.downcase] << page + end + end +end + +CATEGORY_LIST_TITLE = "Browse Mod Categories" + +class CategoryList < Jekyll::Page + def initialize(site, categories) + @site = site + @base = site.source + @dir = site.config["category_index"]["url"] + @basename = "index" + @ext = ".html" + @name = "index.html" + @data = { + "all_categories" => categories, + # Get it to appear at the bottom of the sidebar + "title" => CATEGORY_LIST_TITLE, + "nav_order" => 999, + } + data.default_proc = proc do |_, key| + site.frontmatter_defaults.find(relative_path, :category_list, key) + end + end +end + +class CategoryIndex < Jekyll::Page + def initialize(site, category, pages) + @site = site + @base = site.source + @dir = site.config["category_index"]["url"] + category + @basename = "index" + @ext = ".html" + @name = "index.html" + @data = { + "mod_category" => category, + "mods" => pages, + # Make it appear in the sidebar, under the root + "title" => site.data["categories"].fetch(category, {})["title"] || category, + "parent" => CATEGORY_LIST_TITLE, + } + data.default_proc = proc do |_, key| + site.frontmatter_defaults.find(relative_path, :category_index, key) + end + end +end + diff --git a/developing/releasing_your_mod/index.md b/developing/releasing_your_mod/index.md index 08f7f0aa..004d5ca2 100644 --- a/developing/releasing_your_mod/index.md +++ b/developing/releasing_your_mod/index.md @@ -256,7 +256,8 @@ Misc URLs6 | `urls` | `project.urls` Download Link | `download` | `tool.sdkmod.download` Description | The page contents | `project.description`7 Native Modules Warning | `uses_native_modules` | `tool.sdkmod.uses_native_modules` -Redirects8 | `redirect_from` | Not supported +Categories8 | `mod_categories` | Not supported +Redirects9 | `redirect_from` | Not supported 1 Multiple authors are concatenated in the order given. 2 An array of strings. If not given, defaults to all games for the category you're in. @@ -267,7 +268,9 @@ Redirects8 | `redirect_from` | Not supported 5 Used as the name, with no url. 6 A dict where keys are the names and values are the urls. 7 HTML tags are stripped, rather than just being escaped. -8 An array of relative urls to redirect to this page - i.e. if you moved your mod, it's +8 A string with space-separated categories. See the + [full list here](https://github.com/bl-sdk/bl-sdk.github.io/tree/master/_data/categories.yml). +9 An array of relative urls to redirect to this page - i.e. if you moved your mod, it's old urls. See also [`jekyll-redirect-from`](https://github.com/jekyll/jekyll-redirect-from#usage). {: .fs-2 } From abe7b860ab9bb8341f514738bd5974d77fa52d6a Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 19 Sep 2026 17:41:22 +0300 Subject: [PATCH 2/4] add script to validate categories --- .github/workflows/jekyll.yml | 27 +++++++ _oak_mods/abcd.md | 1 + _validate_categories.py | 144 +++++++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 _validate_categories.py diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml index acbc399c..bf1f89be 100644 --- a/.github/workflows/jekyll.yml +++ b/.github/workflows/jekyll.yml @@ -121,6 +121,33 @@ jobs: uv run ./_validate_pyproject.py git_changed $(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + validate_categories: + runs-on: ubuntu-latest + if: > + github.event_name == 'pull_request' + || (github.ref == 'refs/heads/master' + && github.repository == 'bl-sdk/bl-sdk.github.io') + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + cache-dependency-glob: | + **/*requirements*.txt + **/*requirements*.in + **/*constraints*.txt + **/*constraints*.in + **/pyproject.toml + **/uv.lock + **/*.py.lock + _validate_categories.py + + - name: Validate mod categories + run: uv run ./_validate_categories.py + # Deployment job deploy: environment: diff --git a/_oak_mods/abcd.md b/_oak_mods/abcd.md index 84dd367b..0a3f2556 100644 --- a/_oak_mods/abcd.md +++ b/_oak_mods/abcd.md @@ -1,3 +1,4 @@ --- pyproject_url: https://raw.githubusercontent.com/apple1417/oak-sdk-mods/master/abcd/pyproject.toml +mod_categories: cheats lorem --- diff --git a/_validate_categories.py b/_validate_categories.py new file mode 100644 index 00000000..57ed3131 --- /dev/null +++ b/_validate_categories.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "pyyaml", +# ] +# /// + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import yaml + +if TYPE_CHECKING: + from collections.abc import Collection + +CATEGORIES_FILE = Path(__file__).parent / "_data" / "categories.yml" + + +PER_TREE_CATEGORIES: dict[str, tuple[str, ...]] = { + # Map from the category key to the mod folders it's allowed in + "lorem": ("_oak2_mods",), +} + + +def get_all_categories() -> Collection[str]: + """ + Gets a list of all valid categories. + + Returns: + The list of categories. + """ + with CATEGORIES_FILE.open() as file: + data = yaml.safe_load(file) + return data.keys() + + +VALID_CATEGORY_NAME_RE = re.compile("^[a-z-]+$") + + +def validate_category_name(categories: Collection[str]) -> int: + """ + Makes sure all category names are correctly formatted. + + Args: + categories: The list of categories to validate. + Returns: + The amount of errors - 0 on success. + """ + errors = 0 + for cat in categories: + if not VALID_CATEGORY_NAME_RE.match(cat): + sys.stderr.write(f"Invalid category name '{cat}'!\n") + errors += 1 + return errors + + +def parse_front_matter(path: Path) -> tuple[Collection[str], int]: + """ + Parses jekyll front matter, extracting the mod categories. + + Args: + path: Path to the file to parse. + Returns: + A (categories, num_errors) tuple. + """ + categories: set[str] = set() + + try: + with path.open() as file: + data = next(yaml.safe_load_all(file)) + assert isinstance(data, dict) + front_matter: dict[str, Any] = data # type: ignore + except Exception: # noqa: BLE001 + sys.stderr.write(f"Couldn't parse front matter from {path}\n") + return (), 1 + + if "mod_categories" not in front_matter: + return (), 0 + + mod_categories = front_matter["mod_categories"] + if not isinstance(mod_categories, str): + sys.stderr.write(f"'mod_categories' has invalid type {type(categories)}, from {path}\n") + return (), 1 + + split_categories = mod_categories.split() + if len(set(split_categories)) != len(split_categories): + sys.stderr.write(f"duplicate mod categories in {path}\n") + return split_categories, 1 + + return split_categories, 0 + + +def validate_mod_file( + all_categories: Collection[str], + path: Path, +) -> int: + """ + Validates the categories stored in a mod file. + + Args: + all_categories: A list of valid categories. + path: Path to the mod file to validate. + """ + mod_categories, errors = parse_front_matter(file) + + tree = path.parent.name # e.g. "_oak_mods" + + for category in mod_categories: + if category not in all_categories: + sys.stderr.write(f"invalid category '{category}', from {path}\n") + errors += 1 + continue + if ( + (allowed_trees := PER_TREE_CATEGORIES.get(category)) is not None # formatting + and tree not in allowed_trees + ): + sys.stderr.write( + f"'{category}' is not a valid category for mod files in '{tree}', from {path}\n", + ) + errors += 1 + continue + + return errors + + +if __name__ == "__main__": + errors = 0 + + all_categories = get_all_categories() + errors += validate_category_name(all_categories) + + for file in Path(__file__).resolve().parent.glob("_*_mods/*"): + errors += validate_mod_file(all_categories, file) + + if errors: + sys.stderr.write(f"{errors} total errors\n") + sys.exit(1) + else: + sys.stdout.write("No errors\n") From b9538a224f15d1a1b70e454d676d926941fcaa7a Mon Sep 17 00:00:00 2001 From: apple1417 Date: Sat, 19 Sep 2026 17:56:12 +0300 Subject: [PATCH 3/4] make filter affect category pages too --- _includes/components/nav/links.html | 15 +++------------ _includes/mod_filter_classes.html | 18 ++++++++++++++++++ _layouts/category_index.html | 3 ++- _oak_mods/abcd.md | 1 - 4 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 _includes/mod_filter_classes.html diff --git a/_includes/components/nav/links.html b/_includes/components/nav/links.html index 1b2574a8..8e09aafd 100644 --- a/_includes/components/nav/links.html +++ b/_includes/components/nav/links.html @@ -8,20 +8,11 @@ {%- for node in include.pages -%} {%- if include.all == true or node.nav_exclude != true -%} - {%- if site.game_selector.collection and node.collection == site.game_selector.collection -%} - {%- assign game_select_classes = "game-any" -%} - {%- include filter_games.html page=node -%} - {%- for game in filtered_games -%} - {%- assign game_class = " game-" | append: game | downcase -%} - {%- assign game_select_classes = game_select_classes | append: game_class -%} - {%- endfor -%} - {%- else -%} - {%- assign game_select_classes = "" -%} - {%- endif -%} + {%- include mod_filter_classes.html page=node -%} {%- if include.ancestors contains node.title -%} - {%- capture nav_error_report -%} @@ -36,7 +27,7 @@ {%- include components/nav/children.html node=node ancestors=include.ancestors all=include.all -%} -