From 0f6050fccadd12c1f2860126f27df5100f97ac73 Mon Sep 17 00:00:00 2001 From: object-Object Date: Sun, 1 Oct 2023 02:15:01 -0400 Subject: [PATCH] Add WIP crafting table rendering --- .vscode/launch.json | 9 +- doc/nodemon.json | 5 +- doc/properties.toml | 29 +++++++ .../hexcasting/lang/en_us.flatten.json5 | 19 ++-- .../textures/gui/hexdoc/crafting_table.png | Bin 0 -> 471 bytes doc/src/hexdoc/_templates/category.html.jinja | 6 +- .../components/table_of_contents.html.jinja | 8 +- .../_templates/components/welcome.html.jinja | 2 +- doc/src/hexdoc/_templates/entry.html.jinja | 6 +- .../formatting.html.jinja} | 12 --- .../_templates/macros/recipes.html.jinja | 64 ++++++++++++++ doc/src/hexdoc/_templates/main.css.jinja | 74 ++++++++++++++-- doc/src/hexdoc/_templates/main.js.jinja | 15 ++-- .../pages/hexcasting/brainsweep.html.jinja | 4 +- .../hexcasting/crafting_multi.html.jinja | 9 +- .../hexcasting/manual_pattern.html.jinja | 4 +- .../pages/patchouli/crafting.html.jinja | 9 +- .../pages/patchouli/image.html.jinja | 2 +- .../pages/patchouli/text.html.jinja | 2 +- doc/src/hexdoc/cli/main.py | 11 +-- doc/src/hexdoc/cli/render.py | 4 +- doc/src/hexdoc/minecraft/__init__.py | 2 + doc/src/hexdoc/minecraft/assets.py | 82 ++++++++++++++++++ doc/src/hexdoc/minecraft/i18n.py | 8 +- doc/src/hexdoc/minecraft/recipe/__init__.py | 10 +-- .../minecraft/recipe/abstract_recipes.py | 32 +++++-- .../hexdoc/minecraft/recipe/ingredients.py | 31 ++++++- doc/src/hexdoc/minecraft/recipe/recipes.py | 34 ++++---- doc/src/hexdoc/patchouli/book_context.py | 3 +- doc/src/hexdoc/utils/iterators.py | 2 +- doc/src/hexdoc/utils/jinja_extensions.py | 13 ++- doc/src/hexdoc/utils/properties.py | 15 ++++ doc/src/hexdoc/utils/resource.py | 7 ++ doc/src/hexdoc/utils/resource_loader.py | 36 +++++--- pyproject.toml | 19 ++-- 35 files changed, 440 insertions(+), 148 deletions(-) create mode 100644 doc/src/hexdoc/_export/resources/assets/hexcasting/textures/gui/hexdoc/crafting_table.png rename doc/src/hexdoc/_templates/{common/macros.html.jinja => macros/formatting.html.jinja} (77%) create mode 100644 doc/src/hexdoc/_templates/macros/recipes.html.jinja create mode 100644 doc/src/hexdoc/minecraft/assets.py diff --git a/.vscode/launch.json b/.vscode/launch.json index be85c780..7996e48b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,9 +16,14 @@ "name": "Python: Generate Docs", "type": "python", "request": "launch", - "module": "hexdoc.scripts.hexdoc", + "module": "hexdoc.cli.main", "args": [ - "doc/properties.toml", "-o", "out", "--lang", "en_us", + "render", + "doc/properties.toml", + "_site/src/docs", + "--lang", + "en_us", + "--allow-missing", ], "console": "integratedTerminal", "justMyCode": false diff --git a/doc/nodemon.json b/doc/nodemon.json index 4365d47e..102934e7 100644 --- a/doc/nodemon.json +++ b/doc/nodemon.json @@ -1,5 +1,6 @@ { - "watch": ["doc/src/hexdoc/_templates"], - "ext": "jinja,html,css,js", + "watch": ["doc/src/hexdoc/_templates", "doc/properties.toml"], + "ignore": ["**/generated/**"], + "ext": "jinja,html,css,js,toml,json,json5", "exec": "hexdoc serve doc/properties.toml --src _site/src/docs --dst _site/dst/docs --lang en_us --allow-missing --release" } \ No newline at end of file diff --git a/doc/properties.toml b/doc/properties.toml index 37fb95bf..31fbf501 100644 --- a/doc/properties.toml +++ b/doc/properties.toml @@ -26,6 +26,35 @@ regex = "{^_pattern_regex}" path = "{^_fabric.package}/FabricHexInitializer.kt" regex = "{^_pattern_regex}" +[minecraft_assets] +# https://github.com/PrismarineJS/minecraft-assets/tree/83e2169afbbce40990d69fc53e5962e4a793d467/data/1.19.1 +ref = "83e2169afbbce40990d69fc53e5962e4a793d467" +version = "1.19.1" + +[textures] +missing = [ + "farmersdelight:skillet", + "hexcasting:*colorizer*", + "hexcasting:amethyst_sconce", + "hexcasting:edified_button", + "hexcasting:edified_pressure_plate", + "hexcasting:edified_slab", + "hexcasting:edified_stairs", +] + +[textures.override] +"hexcasting:akashic_connector" = "hexcasting:akashic_ligature" +"hexcasting:directrix/empty" = "hexcasting:circle/directrix/empty/front_dim" +"hexcasting:focus" = "hexcasting:focus_empty" +"hexcasting:impetus/empty" = "hexcasting:circle/impetus/empty/front_dim" +"hexcasting:quenched_allay_shard" = "hexcasting:quenched_shard_0" +"hexcasting:scroll_large" = "hexcasting:scroll_pristine_large" +"hexcasting:scroll_medium" = "hexcasting:scroll_pristine_medium" +"hexcasting:scroll_small" = "hexcasting:scroll_pristine_small" +"hexcasting:scroll" = "hexcasting:scroll_pristine_large" +"hexcasting:slate_block" = "hexcasting:slate" +"hexcasting:spellbook" = "hexcasting:spellbook_empty" +"hexcasting:staff/quenched" = "hexcasting:staff/quenched_0" [template] static_dir = "static" diff --git a/doc/src/hexdoc/_export/resources/assets/hexcasting/lang/en_us.flatten.json5 b/doc/src/hexdoc/_export/resources/assets/hexcasting/lang/en_us.flatten.json5 index e367185c..4de9bfec 100644 --- a/doc/src/hexdoc/_export/resources/assets/hexcasting/lang/en_us.flatten.json5 +++ b/doc/src/hexdoc/_export/resources/assets/hexcasting/lang/en_us.flatten.json5 @@ -38,23 +38,16 @@ not_supported: "Your browser does not support visualizing patterns. Pattern code: {}", }, + recipe: { + show: "Click to show recipe: {}", + hide: "Click to hide recipe: {}", + }, + pages: { - patchouli: { - crafting: { - description: "Depicted in the book: The crafting recipe for the", - separator: " and ", - }, - }, - hexcasting: { - crafting_multi: { - description: "Depicted in the book: Several crafting recipes, for the", - separator: ", ", - }, - brainsweep: { description: "Depicted in the book: A mind-flaying recipe producing the", - separator: "", // this value intentionally left blank + separator: "", }, }, }, diff --git a/doc/src/hexdoc/_export/resources/assets/hexcasting/textures/gui/hexdoc/crafting_table.png b/doc/src/hexdoc/_export/resources/assets/hexcasting/textures/gui/hexdoc/crafting_table.png new file mode 100644 index 0000000000000000000000000000000000000000..67f624422de83783f5eb335ea0a9350bcd50e917 GIT binary patch literal 471 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5S~!@2q{lB-Js>4n;u=vBoS#-wo>-L1;Fyx1 zl&avFo0y&&l$w}QS$Hzl2B=6hz$e7@*s){X-QEBH{|5?$hK8D(n*$j~U95V66njaK zUocP#BrE03>;|;S)zif>q~g|_yBGPK4J6nuXx;v8|7pqzp^Y<-9(`tge{P18e^8hF zQ@dsTXKK9|4a)YrrKvGYxb-*6JClWB9)qgR;Tikb4Vb4K02-gbTX5U=`rNB*2lAOp ztXF3_ohVGcI9X!^t4Q#4v}AaRwK2fhErYT|uW)EiWL)6F*3hf~a!Zc?l>I;FXff#ff+`njxg HN@xNAjMbDg literal 0 HcmV?d00001 diff --git a/doc/src/hexdoc/_templates/category.html.jinja b/doc/src/hexdoc/_templates/category.html.jinja index fd3e887e..7f83c2ff 100644 --- a/doc/src/hexdoc/_templates/category.html.jinja +++ b/doc/src/hexdoc/_templates/category.html.jinja @@ -1,8 +1,8 @@ -{% import "common/macros.html.jinja" as macros %} +{% import "macros/formatting.html.jinja" as formatting %}
- {% call macros.maybe_spoilered(category) %} - {{- macros.section_header(category, "h2", "category-title") }} + {% call formatting.maybe_spoilered(category) %} + {{- formatting.section_header(category, "h2", "category-title") }} {{ category.description|hexdoc_block }} {% endcall %} diff --git a/doc/src/hexdoc/_templates/components/table_of_contents.html.jinja b/doc/src/hexdoc/_templates/components/table_of_contents.html.jinja index 9074cad4..1b40bd29 100644 --- a/doc/src/hexdoc/_templates/components/table_of_contents.html.jinja +++ b/doc/src/hexdoc/_templates/components/table_of_contents.html.jinja @@ -1,4 +1,4 @@ -{% import "common/macros.html.jinja" as macros -%} +{% import "macros/formatting.html.jinja" as formatting -%} {# put the link target outside of toc-container so jump to top still works in sidebar mode #}
@@ -9,18 +9,18 @@ class="permalink toggle-link small" data-target="toc-category" title="{{ _('hexdoc.toc.toggle_all') }}" - >{{ macros.permalink("table-of-contents", "toc-permalink") }} + >{{ formatting.permalink("table-of-contents", "toc-permalink") }} {% for category in book.categories.values() if category.entries.values() %}
{# category #} - {{ macros.maybe_spoilered_link(category) }} + {{ formatting.maybe_spoilered_link(category) }} {# list of entries in the category #}
    {% for entry in category.entries.values() %} -
  • {{ macros.maybe_spoilered_link(entry) }}
  • +
  • {{ formatting.maybe_spoilered_link(entry) }}
  • {% endfor %}
diff --git a/doc/src/hexdoc/_templates/components/welcome.html.jinja b/doc/src/hexdoc/_templates/components/welcome.html.jinja index 7de09363..7e354a5f 100644 --- a/doc/src/hexdoc/_templates/components/welcome.html.jinja +++ b/doc/src/hexdoc/_templates/components/welcome.html.jinja @@ -13,4 +13,4 @@ {{ _f("hexdoc.welcome.old_version") }} - \ No newline at end of file + diff --git a/doc/src/hexdoc/_templates/entry.html.jinja b/doc/src/hexdoc/_templates/entry.html.jinja index 088eb538..a416a837 100644 --- a/doc/src/hexdoc/_templates/entry.html.jinja +++ b/doc/src/hexdoc/_templates/entry.html.jinja @@ -1,8 +1,8 @@ -{% import "common/macros.html.jinja" as macros -%} +{% import "macros/formatting.html.jinja" as formatting -%}
- {% call macros.maybe_spoilered(entry) %} - {{- macros.section_header(entry, "h3", "entry-title") }} + {% call formatting.maybe_spoilered(entry) %} + {{- formatting.section_header(entry, "h3", "entry-title") }} {% for page in entry.pages +%} {% include page.template %} diff --git a/doc/src/hexdoc/_templates/common/macros.html.jinja b/doc/src/hexdoc/_templates/macros/formatting.html.jinja similarity index 77% rename from doc/src/hexdoc/_templates/common/macros.html.jinja rename to doc/src/hexdoc/_templates/macros/formatting.html.jinja index 0bdf8b47..d1219c03 100644 --- a/doc/src/hexdoc/_templates/common/macros.html.jinja +++ b/doc/src/hexdoc/_templates/macros/formatting.html.jinja @@ -38,15 +38,3 @@ {{ caller() }} {% endif %} {%- endmacro %} - -{# shows the names of all the recipe results in a list of recipes #} -{% macro recipe_block(recipes, result_attribute, description, separator) -%} -
- {{ description }} {{ - recipes - |map(attribute="result." ~ result_attribute) - |map("hexdoc_wrap", "code") - |join(separator) - }}. -
-{%- endmacro %} diff --git a/doc/src/hexdoc/_templates/macros/recipes.html.jinja b/doc/src/hexdoc/_templates/macros/recipes.html.jinja new file mode 100644 index 00000000..fe085d8e --- /dev/null +++ b/doc/src/hexdoc/_templates/macros/recipes.html.jinja @@ -0,0 +1,64 @@ +{# show the names of all the recipe results in a list of recipes #} +{% macro generic(recipes, result_attribute, description, separator) -%} +
+ {{ description }} {{ + recipes + |map(attribute="result."~result_attribute) + |map("hexdoc_wrap", "code") + |join(separator) + }}. +
+{%- endmacro %} + +{# display a single item, with a badge if the count is greater than 1 #} +{% macro render_item(item, count=1, always_show_count=false) -%} +
+ Image of {{ item.name }} + {% if always_show_count or count > 1 %} +
{{ count }}
+ {% endif %} +
+{%- endmacro %} + +{% macro render_ingredient(ingredient) -%} + {% if ingredient is sequence %} + {# TODO: properly handle multi-option ingredients #} + {{ render_ingredient(ingredient[0]) }} + {% elif ingredient is none %} + {# air #} +
+ {% elif ingredient.type|string == "hexcasting:mod_conditional" %} + {{ render_ingredient(ingredient.default) }} + {% else %} + {{ render_item(ingredient.item) }} + {% endif %} +{%- endmacro %} + +{# render a crafting table for each recipe #} +{% macro crafting_table(recipes) -%} + {% for recipe in recipes %} + {% if loop.index0 > 0 %}
{% endif %} +
+ + {{ _('hexdoc.recipe.show').format(recipe.result.item.name) }} + {{ _('hexdoc.recipe.hide').format(recipe.result.item.name) }} + +
+ Crafting table + +
+ {% for item in recipe.ingredients %} + {{ render_ingredient(item) }} + {% endfor %} +
+ +
+ {{ render_item(recipe.result.item, recipe.result.count) }} +
+
+
+ {% endfor %} +{%- endmacro %} \ No newline at end of file diff --git a/doc/src/hexdoc/_templates/main.css.jinja b/doc/src/hexdoc/_templates/main.css.jinja index c1010c91..4eb8a7b6 100644 --- a/doc/src/hexdoc/_templates/main.css.jinja +++ b/doc/src/hexdoc/_templates/main.css.jinja @@ -2,7 +2,7 @@ summary { display: list-item; } -details.spell-collapsible { +.details-collapsible { display: inline-block; border: 1px solid #aaa; border-radius: 4px; @@ -10,31 +10,89 @@ details.spell-collapsible { margin-bottom: 0.5em; } -summary.collapse-spell { +.collapse-details { font-weight: bold; margin: -0.5em -0.5em 0; padding: 0.5em; } -details.spell-collapsible[open] { +.details-collapsible[open] { padding: 0.5em; } -details[open] summary.collapse-spell { +.details-collapsible[open] .collapse-details { border-bottom: 1px solid #aaa; margin-bottom: 0.5em; } -details .collapse-spell::before { +.details-collapsible .collapse-details.collapse-spell::before { content: "{{ _('hexdoc.pattern.show') }}"; } -details[open] .collapse-spell::before { +.details-collapsible[open] .collapse-details.collapse-spell::before { content: "{{ _('hexdoc.pattern.hide') }}"; } -blockquote.crafting-info { - font-size: inherit; +/* if the collapsible is closed, hide the "hide recipe" message */ +.details-collapsible:not([open]) > .collapse-details > .collapse-recipe-hide { + display: none; + visibility: hidden; +} + +/* if the collapsible is open, hide the "show recipe" message */ +.details-collapsible[open] > .collapse-details > .collapse-recipe-show { + display: none; + visibility: hidden; +} + +/* we do a bit of crafting (mostly stolen from https://computercraft.info/wiki) */ + +.crafting-table { + position: relative; + width: 256px; + height: 132px; + + background-color: #c6c6c6; + border-radius: 16px; +} + +.crafting-table > img { /* background */ + image-rendering: pixelated; + width: 100%; + height: 100%; +} + +.crafting-table-grid { + position: absolute; + top: 14px; + left: 14px; + + display: grid; + grid: repeat(3, 32px) / repeat(3, 32px); + gap: 4px; +} + +.crafting-table-result { + position: absolute; + top: 50px; + left: 202px; +} + +.crafting-table-result .badge { + position: absolute; + background-color: #292929; + bottom: -4px; + right: -4px; +} + +:is(.crafting-table-grid, .crafting-table-result) :is(img, span) { + image-rendering: pixelated; + width: 32px; + height: 32px; +} + +.tooltip { + white-space: nowrap; } a.permalink { diff --git a/doc/src/hexdoc/_templates/main.js.jinja b/doc/src/hexdoc/_templates/main.js.jinja index 335434f7..5b4f5657 100644 --- a/doc/src/hexdoc/_templates/main.js.jinja +++ b/doc/src/hexdoc/_templates/main.js.jinja @@ -297,8 +297,7 @@ function hookSpoiler(elem) { } // these are filled by Jinja -const ROOT_URL = "{{ props.url }}"; -const PAGE_URL = "{{ page_url }}"; +const BOOK_URL = "{{ props.url }}"; const VERSION = "{{ version }}"; const LANG = "{{ lang }}"; @@ -325,7 +324,7 @@ function versionDropdownItem(sitemap, version) { path = defaultPath; } - return dropdownItem(version, ROOT_URL + path); + return dropdownItem(version, BOOK_URL + path); } function versionDropdownItems(sitemap, versions) { @@ -390,7 +389,7 @@ function addDropdowns(sitemap) { const langs = Object.keys(langPaths).sort(); document.getElementById("lang-dropdown").append( - ...langs.map((lang) => dropdownItem(lang, ROOT_URL + langPaths[lang])), + ...langs.map((lang) => dropdownItem(lang, BOOK_URL + langPaths[lang])), ); // return sitemap for chaining, i guess @@ -399,12 +398,12 @@ function addDropdowns(sitemap) { document.addEventListener("DOMContentLoaded", () => { // fetch the sitemap from the root and use it to generate the navbar - fetch(`${ROOT_URL}/meta/sitemap.json`) + fetch(`${BOOK_URL}/meta/sitemap.json`) .then(r => r.json()) .then(addDropdowns) .catch(e => console.error(e)) - document.querySelectorAll("details.spell-collapsible").forEach(hookLoad); + document.querySelectorAll(".details-collapsible").forEach(hookLoad); document.querySelectorAll("a.toggle-link").forEach(hookToggle); document.querySelectorAll(".spoilered").forEach(hookSpoiler); @@ -417,3 +416,7 @@ document.addEventListener("DOMContentLoaded", () => { } requestAnimationFrame((t) => tick(t, t)); }); + +$(function () { + $('[data-toggle="tooltip"]').tooltip() +}) diff --git a/doc/src/hexdoc/_templates/pages/hexcasting/brainsweep.html.jinja b/doc/src/hexdoc/_templates/pages/hexcasting/brainsweep.html.jinja index b904673c..52da5122 100644 --- a/doc/src/hexdoc/_templates/pages/hexcasting/brainsweep.html.jinja +++ b/doc/src/hexdoc/_templates/pages/hexcasting/brainsweep.html.jinja @@ -1,8 +1,8 @@ {% extends "pages/patchouli/page.html.jinja" %} -{% include "common/macros.html.jinja" with context %} +{% import "macros/recipes.html.jinja" as recipe_macros with context %} {% block body %} - {{ macros.recipe_block( + {{ recipe_macros.generic( [page.recipe], "name", _("hexdoc.pages.hexcasting.brainsweep.description"), diff --git a/doc/src/hexdoc/_templates/pages/hexcasting/crafting_multi.html.jinja b/doc/src/hexdoc/_templates/pages/hexcasting/crafting_multi.html.jinja index ea88fafc..d2c130a0 100644 --- a/doc/src/hexdoc/_templates/pages/hexcasting/crafting_multi.html.jinja +++ b/doc/src/hexdoc/_templates/pages/hexcasting/crafting_multi.html.jinja @@ -1,12 +1,7 @@ {% extends "pages/patchouli/text.html.jinja" %} -{% include "common/macros.html.jinja" with context %} +{% import "macros/recipes.html.jinja" as recipe_macros with context %} {% block inner_body %} - {{ macros.recipe_block( - page.recipes, - "item", - _("hexdoc.pages.hexcasting.crafting_multi.description"), - _("hexdoc.pages.hexcasting.crafting_multi.separator"), - ) }} + {{ recipe_macros.crafting_table(page.recipes) }} {{ super() }} {% endblock inner_body %} diff --git a/doc/src/hexdoc/_templates/pages/hexcasting/manual_pattern.html.jinja b/doc/src/hexdoc/_templates/pages/hexcasting/manual_pattern.html.jinja index 688e3031..cea97460 100644 --- a/doc/src/hexdoc/_templates/pages/hexcasting/manual_pattern.html.jinja +++ b/doc/src/hexdoc/_templates/pages/hexcasting/manual_pattern.html.jinja @@ -1,8 +1,8 @@ {% extends "pages/patchouli/text.html.jinja" %} {% block inner_body %} -
- +
+ {% for pattern in page.patterns %} {% for image in page.images %} - + {% endfor %}

{{ super() }} diff --git a/doc/src/hexdoc/_templates/pages/patchouli/text.html.jinja b/doc/src/hexdoc/_templates/pages/patchouli/text.html.jinja index 9dbf66a5..bafd3bc8 100644 --- a/doc/src/hexdoc/_templates/pages/patchouli/text.html.jinja +++ b/doc/src/hexdoc/_templates/pages/patchouli/text.html.jinja @@ -1,5 +1,5 @@ {% extends "pages/patchouli/page.html.jinja" %} -{% import "common/macros.html.jinja" as macros %} +{% import "macros/formatting.html.jinja" as macros %} {% block body %} {% if page.title is not none %} diff --git a/doc/src/hexdoc/cli/main.py b/doc/src/hexdoc/cli/main.py index dea50562..b3f898e0 100644 --- a/doc/src/hexdoc/cli/main.py +++ b/doc/src/hexdoc/cli/main.py @@ -26,7 +26,7 @@ RequiredPathOption = Annotated[Path, typer.Option()] UpdateLatestOption = Annotated[bool, typer.Option(envvar="UPDATE_LATEST")] ReleaseOption = Annotated[bool, typer.Option(envvar="RELEASE")] -app = typer.Typer() +app = typer.Typer(pretty_exceptions_enable=False) @app.command() @@ -158,11 +158,10 @@ def serve( ): book_path = dst.resolve().relative_to(Path.cwd()) - base_url = f"http://localhost:{port}" - book_url = f"{base_url}/{book_path.as_posix()}" + book_url = f"/{book_path.as_posix()}" os.environ |= { - "DEBUG_GITHUBUSERCONTENT": base_url, + "DEBUG_GITHUBUSERCONTENT": "", "GITHUB_PAGES_URL": book_url, } @@ -186,7 +185,9 @@ def serve( release=release, ) - print(f"Serving web book at {book_url} (press ctrl+c to exit)\n") + print( + f"Serving web book at http://localhost:{port}{book_url} (press ctrl+c to exit)\n" + ) with HTTPServer(("", port), SimpleHTTPRequestHandler) as httpd: httpd.serve_forever() diff --git a/doc/src/hexdoc/cli/render.py b/doc/src/hexdoc/cli/render.py index bd69fa7a..7be7d298 100644 --- a/doc/src/hexdoc/cli/render.py +++ b/doc/src/hexdoc/cli/render.py @@ -21,7 +21,7 @@ from hexdoc.utils.jinja_extensions import ( IncludeRawExtension, hexdoc_block, hexdoc_localize, - hexdoc_texture_url, + hexdoc_texture, hexdoc_wrap, ) from hexdoc.utils.path import write_to_path @@ -50,7 +50,7 @@ def create_jinja_env(props: Properties): "hexdoc_block": hexdoc_block, "hexdoc_wrap": hexdoc_wrap, "hexdoc_localize": hexdoc_localize, - "hexdoc_texture_url": hexdoc_texture_url, + "hexdoc_texture": hexdoc_texture, } return env diff --git a/doc/src/hexdoc/minecraft/__init__.py b/doc/src/hexdoc/minecraft/__init__.py index 5ef67898..bd72e5b0 100644 --- a/doc/src/hexdoc/minecraft/__init__.py +++ b/doc/src/hexdoc/minecraft/__init__.py @@ -4,8 +4,10 @@ __all__ = [ "LocalizedStr", "Recipe", "Tag", + "RenderedItemStack", ] +from .assets import RenderedItemStack from .i18n import I18n, LocalizedItem, LocalizedStr from .recipe import Recipe from .tags import Tag diff --git a/doc/src/hexdoc/minecraft/assets.py b/doc/src/hexdoc/minecraft/assets.py new file mode 100644 index 00000000..0bd82c43 --- /dev/null +++ b/doc/src/hexdoc/minecraft/assets.py @@ -0,0 +1,82 @@ +import logging +from typing import Self + +import requests +from pydantic import Field, ValidationInfo, model_validator +from pydantic.dataclasses import dataclass + +from hexdoc.minecraft.i18n import I18nContext, LocalizedStr +from hexdoc.utils import ItemStack, ResourceLocation +from hexdoc.utils.deserialize import cast_or_raise +from hexdoc.utils.model import DEFAULT_CONFIG +from hexdoc.utils.resource_loader import resolve_texture_from_metadata + +# 16x16 hashtag icon for tags +TAG_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAC4jAAAuIwF4pT92AAAANUlEQVQ4y2NgGJRAXV39v7q6+n9cfGTARKllFBvAiOxMUjTevHmTkSouGPhAHA0DWnmBrgAANLIZgSXEQxIAAAAASUVORK5CYII=" + +MISSING_TEXTURE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAJElEQVQoz2NkwAF+MPzAKs7EQCIY1UAMYMQV3hwMHKOhRD8NAPogBA/DVsDEAAAAAElFTkSuQmCC" + + +class MinecraftAssetsContext(I18nContext): + minecraft_textures: dict[ResourceLocation, str | None] = Field(default_factory=dict) + + @model_validator(mode="after") + def _fetch_minecraft_textures(self) -> Self: + asset_props = self.props.minecraft_assets + url = ( + "https://raw.githubusercontent.com/PrismarineJS/minecraft-assets" + f"/{asset_props.ref}/data/{asset_props.version}/texture_content.json" + ) + + logging.getLogger(__name__).info(f"Fetch textures from {url}") + resp = requests.get(url) + resp.raise_for_status() + + textures_list: list[dict[{"name": str, "texture": str | None}]] = resp.json() + + for item in textures_list: + id = ResourceLocation("minecraft", item["name"]) + # items are first in the list (i think), so prioritize them + if id not in self.minecraft_textures: + self.minecraft_textures[id] = item["texture"] + + return self + + +@dataclass(config=DEFAULT_CONFIG, frozen=True, repr=False, kw_only=True) +class RenderedItemStack(ItemStack): + name: LocalizedStr | None = None + texture: str | None = None + + @model_validator(mode="after") + def _add_name_and_texture(self, info: ValidationInfo): + """Loads the recipe from json if the actual value is a resource location str.""" + if not info.context: + return self + context = cast_or_raise(info.context, MinecraftAssetsContext) + + object.__setattr__(self, "name", context.i18n.localize_item(self)) + object.__setattr__(self, "texture", self._get_texture(context)) + + return self + + def _get_texture(self, context: MinecraftAssetsContext): + if self.namespace == "minecraft": + return context.minecraft_textures[self.id] + else: + # check if there's an override to apply + id = context.props.textures.override.get(self.id, self.id) + + try: + return resolve_texture_from_metadata( + context.loader.mod_metadata, + id.with_path(f"textures/item/{id.path}.png"), + id.with_path(f"textures/block/{id.path}.png"), + ) + + except KeyError as e: + for missing_id in context.props.textures.missing: + if id.match(missing_id): + logging.getLogger(__name__).warn(f"No texture for {id}") + return MISSING_TEXTURE + raise KeyError(f"No texture for {id}") from e diff --git a/doc/src/hexdoc/minecraft/i18n.py b/doc/src/hexdoc/minecraft/i18n.py index b73b232e..b85f5722 100644 --- a/doc/src/hexdoc/minecraft/i18n.py +++ b/doc/src/hexdoc/minecraft/i18n.py @@ -20,7 +20,7 @@ from hexdoc.utils.resource_loader import LoaderContext @total_ordering -class LocalizedStr(HexdocModel): +class LocalizedStr(HexdocModel, frozen=True): """Represents a string which has been localized.""" key: str @@ -83,7 +83,7 @@ class LocalizedStr(HexdocModel): return self.value < other -class LocalizedItem(LocalizedStr): +class LocalizedItem(LocalizedStr, frozen=True): @classmethod def _localize(cls, i18n: I18n, key: str) -> Self: return i18n.localize_item(key) @@ -253,6 +253,10 @@ class I18n(HexdocModel): def localize_key(self, key: str) -> LocalizedStr: return self.localize(f"key.{key}") + def localize_tag(self, tag: ResourceLocation): + localized = self.localize(f"tag.{tag.namespace}.{tag.path}") + return LocalizedStr(key=localized.key, value=f"Tag: {localized.value}") + class I18nContext(LoaderContext): i18n: I18n diff --git a/doc/src/hexdoc/minecraft/recipe/__init__.py b/doc/src/hexdoc/minecraft/recipe/__init__.py index 8bc89582..60524cbc 100644 --- a/doc/src/hexdoc/minecraft/recipe/__init__.py +++ b/doc/src/hexdoc/minecraft/recipe/__init__.py @@ -10,16 +10,12 @@ __all__ = [ "ItemResult", ] -from .abstract_recipes import Recipe +from .abstract_recipes import CraftingRecipe, Recipe from .ingredients import ( ItemIngredient, ItemIngredientOrList, + ItemResult, MinecraftItemIdIngredient, MinecraftItemTagIngredient, ) -from .recipes import ( - CraftingRecipe, - CraftingShapedRecipe, - CraftingShapelessRecipe, - ItemResult, -) +from .recipes import CraftingShapedRecipe, CraftingShapelessRecipe diff --git a/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py b/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py index fdb4f9ba..7b79d04f 100644 --- a/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py +++ b/doc/src/hexdoc/minecraft/recipe/abstract_recipes.py @@ -1,12 +1,15 @@ import logging -from typing import Any +from typing import Any, Self from pydantic import ValidationInfo, model_validator +from pydantic.functional_validators import ModelWrapValidatorHandler from hexdoc.utils import ResourceLocation, TypeTaggedUnion from hexdoc.utils.deserialize import cast_or_raise from hexdoc.utils.resource_loader import LoaderContext +from .ingredients import ItemResult + class Recipe(TypeTaggedUnion, type=None): id: ResourceLocation @@ -14,23 +17,34 @@ class Recipe(TypeTaggedUnion, type=None): group: str | None = None category: str | None = None - @model_validator(mode="before") - def _pre_root(cls, values: Any, info: ValidationInfo): + # use wrap validator so we load the file *before* resolving the tagged union + @model_validator(mode="wrap") + @classmethod + def _pre_root( + cls, + value: Any, + handler: ModelWrapValidatorHandler[Self], + info: ValidationInfo, + ) -> Self: """Loads the recipe from json if the actual value is a resource location str.""" if not info.context: - return values + return handler(value) context = cast_or_raise(info.context, LoaderContext) # if necessary, convert the id to a ResourceLocation - match values: + match value: case str(): - id = ResourceLocation.from_str(values) + id = ResourceLocation.from_str(value) case ResourceLocation(): - id = values + id = value case _: - return values + return handler(value) # load the recipe _, data = context.loader.load_resource("data", "recipes", id) logging.getLogger(__name__).debug(f"Load {cls} from {id}") - return data | {"id": id} + return handler(data | {"id": id}) + + +class CraftingRecipe(Recipe, type=None): + result: ItemResult diff --git a/doc/src/hexdoc/minecraft/recipe/ingredients.py b/doc/src/hexdoc/minecraft/recipe/ingredients.py index 1243ac3a..59912c49 100644 --- a/doc/src/hexdoc/minecraft/recipe/ingredients.py +++ b/doc/src/hexdoc/minecraft/recipe/ingredients.py @@ -1,4 +1,10 @@ -from hexdoc.utils import NoValue, ResourceLocation, TypeTaggedUnion +from pydantic import ValidationInfo, model_validator + +from hexdoc.minecraft import RenderedItemStack +from hexdoc.minecraft.assets import TAG_TEXTURE +from hexdoc.minecraft.i18n import I18nContext +from hexdoc.utils import HexdocModel, NoValue, ResourceLocation, TypeTaggedUnion +from hexdoc.utils.deserialize import cast_or_raise class ItemIngredient(TypeTaggedUnion, type=None): @@ -9,8 +15,29 @@ ItemIngredientOrList = ItemIngredient | list[ItemIngredient] class MinecraftItemIdIngredient(ItemIngredient, type=NoValue): - item: ResourceLocation + item: RenderedItemStack class MinecraftItemTagIngredient(ItemIngredient, type=NoValue): tag: ResourceLocation + item: RenderedItemStack | None = None + + @model_validator(mode="after") + def _add_item(self, info: ValidationInfo): + if not info.context: + return self + context = cast_or_raise(info.context, I18nContext) + + self.item = RenderedItemStack( + namespace=self.tag.namespace, + path=self.tag.path, + name=context.i18n.localize_tag(self.tag), + texture=TAG_TEXTURE, + ) + + return self + + +class ItemResult(HexdocModel): + item: RenderedItemStack + count: int = 1 diff --git a/doc/src/hexdoc/minecraft/recipe/recipes.py b/doc/src/hexdoc/minecraft/recipe/recipes.py index aa499299..ae5d26fc 100644 --- a/doc/src/hexdoc/minecraft/recipe/recipes.py +++ b/doc/src/hexdoc/minecraft/recipe/recipes.py @@ -1,24 +1,34 @@ +from typing import Iterator + from pydantic import field_validator -from hexdoc.utils import HexdocModel from hexdoc.utils.compat import HexVersion -from ..i18n import LocalizedItem -from .abstract_recipes import Recipe +from .abstract_recipes import CraftingRecipe from .ingredients import ItemIngredientOrList -class ItemResult(HexdocModel): - item: LocalizedItem - count: int | None = None +class CraftingShapelessRecipe(CraftingRecipe, type="minecraft:crafting_shapeless"): + ingredients: list[ItemIngredientOrList] -class CraftingShapedRecipe(Recipe, type="minecraft:crafting_shaped"): +class CraftingShapedRecipe(CraftingRecipe, type="minecraft:crafting_shaped"): key: dict[str, ItemIngredientOrList] pattern: list[str] - result: ItemResult show_notification: bool | None = None + @property + def ingredients(self) -> Iterator[ItemIngredientOrList | None]: + for row in self.pattern: + if len(row) > 3: + raise ValueError(f"Expected len(row) <= 3, got {len(row)}: `{row}`") + for item_key in row.ljust(3): + match item_key: + case " ": + yield None + case _: + yield self.key[item_key] + @field_validator("show_notification") @classmethod def _check_show_notification(cls, value: bool | None): @@ -28,11 +38,3 @@ class CraftingShapedRecipe(Recipe, type="minecraft:crafting_shaped"): case HexVersion.v0_10_x | HexVersion.v0_9_x: HexVersion.check(value is None, "show_notification") return value - - -class CraftingShapelessRecipe(Recipe, type="minecraft:crafting_shapeless"): - ingredients: list[ItemIngredientOrList] - result: ItemResult - - -CraftingRecipe = CraftingShapedRecipe | CraftingShapelessRecipe diff --git a/doc/src/hexdoc/patchouli/book_context.py b/doc/src/hexdoc/patchouli/book_context.py index 3f7eb9b8..8f99e680 100644 --- a/doc/src/hexdoc/patchouli/book_context.py +++ b/doc/src/hexdoc/patchouli/book_context.py @@ -3,13 +3,14 @@ from typing import Self from pydantic import Field, model_validator from hexdoc.minecraft import Tag +from hexdoc.minecraft.assets import MinecraftAssetsContext from hexdoc.plugin.manager import PluginManagerContext from hexdoc.utils import ResourceLocation from .text.formatting import FormattingContext -class BookContext(FormattingContext, PluginManagerContext): +class BookContext(FormattingContext, PluginManagerContext, MinecraftAssetsContext): spoilered_advancements: set[ResourceLocation] = Field(default_factory=set) @model_validator(mode="after") diff --git a/doc/src/hexdoc/utils/iterators.py b/doc/src/hexdoc/utils/iterators.py index 1bee4663..e66d1f22 100644 --- a/doc/src/hexdoc/utils/iterators.py +++ b/doc/src/hexdoc/utils/iterators.py @@ -5,7 +5,7 @@ _T = TypeVar("_T") _P = ParamSpec("_P") -def must_yield(f: Callable[_P, Iterator[_T]]) -> Callable[_P, Iterator[_T]]: +def must_yield_something(f: Callable[_P, Iterator[_T]]) -> Callable[_P, Iterator[_T]]: """Raises StopIteration if the wrapped iterator doesn't yield anything.""" @functools.wraps(f) diff --git a/doc/src/hexdoc/utils/jinja_extensions.py b/doc/src/hexdoc/utils/jinja_extensions.py index b2cd9add..d42898cc 100644 --- a/doc/src/hexdoc/utils/jinja_extensions.py +++ b/doc/src/hexdoc/utils/jinja_extensions.py @@ -5,6 +5,7 @@ from jinja2.ext import Extension from jinja2.parser import Parser from jinja2.runtime import Context from markupsafe import Markup +from pydantic import ConfigDict, validate_call from hexdoc.minecraft import I18n, LocalizedStr from hexdoc.patchouli import Book, FormatTree @@ -12,7 +13,7 @@ from hexdoc.patchouli.book import Book from hexdoc.patchouli.text import HTMLStream from hexdoc.patchouli.text.formatting import FormatTree from hexdoc.utils.resource import ResourceLocation -from hexdoc.utils.resource_loader import HexdocMetadata +from hexdoc.utils.resource_loader import HexdocMetadata, resolve_texture_from_metadata from . import Properties from .deserialize import cast_or_raise @@ -101,13 +102,11 @@ def hexdoc_localize( @pass_context -def hexdoc_texture_url(context: Context, id: ResourceLocation) -> str: +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def hexdoc_texture(context: Context, id: ResourceLocation) -> str: try: - metadata = cast_or_raise( - context["mod_metadata"], - dict[str, HexdocMetadata], - )[id.namespace] - return f"{metadata.asset_url}/{metadata.textures[id].as_posix()}" + mod_metadata = cast_or_raise(context["mod_metadata"], dict[str, HexdocMetadata]) + return resolve_texture_from_metadata(mod_metadata, id) except Exception as e: e.add_note(f"id:\n {id}") raise diff --git a/doc/src/hexdoc/utils/properties.py b/doc/src/hexdoc/utils/properties.py index 87adbe38..f2d7c9d8 100644 --- a/doc/src/hexdoc/utils/properties.py +++ b/doc/src/hexdoc/utils/properties.py @@ -87,6 +87,16 @@ class TemplateProps(StripHiddenModel): return values +class MinecraftAssetsProps(StripHiddenModel): + ref: str + version: str + + +class TexturesProps(StripHiddenModel): + missing: list[ResourceLocation] + override: dict[ResourceLocation, ResourceLocation] + + class Properties(StripHiddenModel): env: EnvironmentVariableProps @@ -104,6 +114,11 @@ class Properties(StripHiddenModel): entry_id_blacklist: set[ResourceLocation] = Field(default_factory=set) + minecraft_assets: MinecraftAssetsProps + + # FIXME: remove this and get the data from the actual model files + textures: TexturesProps + template: TemplateProps @classmethod diff --git a/doc/src/hexdoc/utils/resource.py b/doc/src/hexdoc/utils/resource.py index 9073ce4f..44154567 100644 --- a/doc/src/hexdoc/utils/resource.py +++ b/doc/src/hexdoc/utils/resource.py @@ -90,6 +90,10 @@ class BaseResourceLocation: def _ser_model(self) -> str: return str(self) + @property + def id(self) -> ResourceLocation: + return ResourceLocation(self.namespace, self.path) + def __repr__(self) -> str: return f"{self.namespace}:{self.path}" @@ -175,6 +179,9 @@ class ItemStack(BaseResourceLocation, regex=_make_regex(count=True, nbt=True)): count: int | None = None nbt: str | None = None + def __init_subclass__(cls, **kwargs: Any): + super().__init_subclass__(regex=cls._from_str_regex, **kwargs) + def i18n_key(self, root: str = "item") -> str: # TODO: is this how i18n works????? (apparently, because it's working) return f"{root}.{self.namespace}.{self.path.replace('/', '.')}" diff --git a/doc/src/hexdoc/utils/resource_loader.py b/doc/src/hexdoc/utils/resource_loader.py index 6e6fd267..5515d9d3 100644 --- a/doc/src/hexdoc/utils/resource_loader.py +++ b/doc/src/hexdoc/utils/resource_loader.py @@ -15,7 +15,7 @@ from pydantic.dataclasses import dataclass from hexdoc.plugin.manager import PluginManager from hexdoc.utils.deserialize import JSONDict, decode_json_dict -from hexdoc.utils.iterators import must_yield +from hexdoc.utils.iterators import must_yield_something from hexdoc.utils.model import DEFAULT_CONFIG, HexdocModel, ValidationContext from hexdoc.utils.path import strip_suffixes, write_to_path @@ -47,12 +47,6 @@ class HexdocMetadata(HexdocModel): def path(cls, modid: str) -> Path: return Path(f"{modid}.hexdoc.json") - def export(self, loader: ModResourceLoader): - loader.export( - self.path(loader.props.modid), - self.model_dump_json(by_alias=True, warnings=False), - ) - @dataclass(config=DEFAULT_CONFIG, kw_only=True) class ModResourceLoader: @@ -114,21 +108,23 @@ class ModResourceLoader: for path_resource_dir in stack.enter_context(resource_dir.load(pm)) ], ) + loader.mod_metadata |= loader.load_metadata(model_type=HexdocMetadata) # export this mod's metadata - loader.mod_metadata[props.modid] = metadata = HexdocMetadata( + metadata = HexdocMetadata( book_url=f"{props.url}/v/{version}", asset_url=props.env.githubusercontent, textures=loader._map_own_assets("textures", root=repo_root), sounds=loader._map_own_assets("sounds", root=repo_root), ) - metadata.export(loader) + loader.mod_metadata[props.modid] = metadata + loader.export( + metadata.path(loader.props.modid), + metadata.model_dump_json(by_alias=True, warnings=False), + ) yield loader - def __post_init__(self): - self.mod_metadata |= self.load_metadata(model_type=HexdocMetadata) - def _map_own_assets(self, folder: str, *, root: str | Path): return { id: path.resolve().relative_to(root) @@ -170,7 +166,7 @@ class ModResourceLoader: return metadata # TODO: maybe this should take lang as a variable? - @must_yield + @must_yield_something def load_book_assets( self, book_id: ResourceLocation, @@ -494,3 +490,17 @@ class LoaderContext(ValidationContext): @property def props(self): return self.loader.props + + +def resolve_texture_from_metadata( + mod_metadata: dict[str, HexdocMetadata], + id: ResourceLocation, + *fallbacks: ResourceLocation, +) -> str: + try: + metadata = mod_metadata[id.namespace] + return f"{metadata.asset_url}/{metadata.textures[id].as_posix()}" + except KeyError: + if not fallbacks: + raise + return resolve_texture_from_metadata(mod_metadata, fallbacks[0], *fallbacks[1:]) diff --git a/pyproject.toml b/pyproject.toml index 588f0ed6..069f51d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,15 +33,16 @@ classifiers = [ ] requires-python = ">=3.11" dependencies = [ - "typing_extensions>=4.6.1,<5", - "importlib_resources>=6.0.1,<7", - "more_itertools>=10.1.0,<11", - "pydantic>=2.3.0,<3,!=2.4.0", - "pydantic_settings>=2.0.3,<3", - "Jinja2>=3.1.2,<4", - "pyjson5>=1.6.3,<2", - "pluggy>=1.3.0,<2", - "typer[all]>=0.9.0,<1", + "typing_extensions~=4.6", + "importlib_resources~=6.0", + "more_itertools~=10.1", + "pydantic~=2.3,!=2.4.0", + "pydantic_settings~=2.0", + "Jinja2~=3.1", + "pyjson5~=1.6", + "pluggy~=1.3", + "typer[all]~=0.9.0", + "requests~=2.31", ] dynamic = ["version"]