Add WIP crafting table rendering
This commit is contained in:
parent
aa7d927e28
commit
0f6050fcca
35 changed files with 440 additions and 148 deletions
9
.vscode/launch.json
vendored
9
.vscode/launch.json
vendored
|
@ -16,9 +16,14 @@
|
||||||
"name": "Python: Generate Docs",
|
"name": "Python: Generate Docs",
|
||||||
"type": "python",
|
"type": "python",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "hexdoc.scripts.hexdoc",
|
"module": "hexdoc.cli.main",
|
||||||
"args": [
|
"args": [
|
||||||
"doc/properties.toml", "-o", "out", "--lang", "en_us",
|
"render",
|
||||||
|
"doc/properties.toml",
|
||||||
|
"_site/src/docs",
|
||||||
|
"--lang",
|
||||||
|
"en_us",
|
||||||
|
"--allow-missing",
|
||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"justMyCode": false
|
"justMyCode": false
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"watch": ["doc/src/hexdoc/_templates"],
|
"watch": ["doc/src/hexdoc/_templates", "doc/properties.toml"],
|
||||||
"ext": "jinja,html,css,js",
|
"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"
|
"exec": "hexdoc serve doc/properties.toml --src _site/src/docs --dst _site/dst/docs --lang en_us --allow-missing --release"
|
||||||
}
|
}
|
|
@ -26,6 +26,35 @@ regex = "{^_pattern_regex}"
|
||||||
path = "{^_fabric.package}/FabricHexInitializer.kt"
|
path = "{^_fabric.package}/FabricHexInitializer.kt"
|
||||||
regex = "{^_pattern_regex}"
|
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]
|
[template]
|
||||||
static_dir = "static"
|
static_dir = "static"
|
||||||
|
|
|
@ -38,23 +38,16 @@
|
||||||
not_supported: "Your browser does not support visualizing patterns. Pattern code: {}",
|
not_supported: "Your browser does not support visualizing patterns. Pattern code: {}",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
recipe: {
|
||||||
|
show: "Click to show recipe: {}",
|
||||||
|
hide: "Click to hide recipe: {}",
|
||||||
|
},
|
||||||
|
|
||||||
pages: {
|
pages: {
|
||||||
patchouli: {
|
|
||||||
crafting: {
|
|
||||||
description: "Depicted in the book: The crafting recipe for the",
|
|
||||||
separator: " and ",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
hexcasting: {
|
hexcasting: {
|
||||||
crafting_multi: {
|
|
||||||
description: "Depicted in the book: Several crafting recipes, for the",
|
|
||||||
separator: ", ",
|
|
||||||
},
|
|
||||||
|
|
||||||
brainsweep: {
|
brainsweep: {
|
||||||
description: "Depicted in the book: A mind-flaying recipe producing the",
|
description: "Depicted in the book: A mind-flaying recipe producing the",
|
||||||
separator: "", // this value intentionally left blank
|
separator: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 471 B |
|
@ -1,8 +1,8 @@
|
||||||
{% import "common/macros.html.jinja" as macros %}
|
{% import "macros/formatting.html.jinja" as formatting %}
|
||||||
|
|
||||||
<section id="{{ category.id.path }}">
|
<section id="{{ category.id.path }}">
|
||||||
{% call macros.maybe_spoilered(category) %}
|
{% call formatting.maybe_spoilered(category) %}
|
||||||
{{- macros.section_header(category, "h2", "category-title") }}
|
{{- formatting.section_header(category, "h2", "category-title") }}
|
||||||
{{ category.description|hexdoc_block }}
|
{{ category.description|hexdoc_block }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|
||||||
|
|
|
@ -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 #}
|
{# put the link target outside of toc-container so jump to top still works in sidebar mode #}
|
||||||
<div id="table-of-contents"></div>
|
<div id="table-of-contents"></div>
|
||||||
|
@ -9,18 +9,18 @@
|
||||||
class="permalink toggle-link small"
|
class="permalink toggle-link small"
|
||||||
data-target="toc-category"
|
data-target="toc-category"
|
||||||
title="{{ _('hexdoc.toc.toggle_all') }}"
|
title="{{ _('hexdoc.toc.toggle_all') }}"
|
||||||
><i class="bi bi-list-nested"></i></a>{{ macros.permalink("table-of-contents", "toc-permalink") }}</span>
|
><i class="bi bi-list-nested"></i></a>{{ formatting.permalink("table-of-contents", "toc-permalink") }}</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{% for category in book.categories.values() if category.entries.values() %}
|
{% for category in book.categories.values() if category.entries.values() %}
|
||||||
<details class="toc-category">
|
<details class="toc-category">
|
||||||
{# category #}
|
{# category #}
|
||||||
<summary>{{ macros.maybe_spoilered_link(category) }}</summary>
|
<summary>{{ formatting.maybe_spoilered_link(category) }}</summary>
|
||||||
|
|
||||||
{# list of entries in the category #}
|
{# list of entries in the category #}
|
||||||
<ul>
|
<ul>
|
||||||
{% for entry in category.entries.values() %}
|
{% for entry in category.entries.values() %}
|
||||||
<li>{{ macros.maybe_spoilered_link(entry) }}</li>
|
<li>{{ formatting.maybe_spoilered_link(entry) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% import "common/macros.html.jinja" as macros -%}
|
{% import "macros/formatting.html.jinja" as formatting -%}
|
||||||
|
|
||||||
<div id="{{ entry.id.path }}">
|
<div id="{{ entry.id.path }}">
|
||||||
{% call macros.maybe_spoilered(entry) %}
|
{% call formatting.maybe_spoilered(entry) %}
|
||||||
{{- macros.section_header(entry, "h3", "entry-title") }}
|
{{- formatting.section_header(entry, "h3", "entry-title") }}
|
||||||
|
|
||||||
{% for page in entry.pages +%}
|
{% for page in entry.pages +%}
|
||||||
{% include page.template %}
|
{% include page.template %}
|
||||||
|
|
|
@ -38,15 +38,3 @@
|
||||||
{{ caller() }}
|
{{ caller() }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{# shows the names of all the recipe results in a list of recipes #}
|
|
||||||
{% macro recipe_block(recipes, result_attribute, description, separator) -%}
|
|
||||||
<blockquote class="crafting-info">
|
|
||||||
{{ description }} {{
|
|
||||||
recipes
|
|
||||||
|map(attribute="result." ~ result_attribute)
|
|
||||||
|map("hexdoc_wrap", "code")
|
|
||||||
|join(separator)
|
|
||||||
}}.
|
|
||||||
</blockquote>
|
|
||||||
{%- endmacro %}
|
|
64
doc/src/hexdoc/_templates/macros/recipes.html.jinja
Normal file
64
doc/src/hexdoc/_templates/macros/recipes.html.jinja
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{# show the names of all the recipe results in a list of recipes #}
|
||||||
|
{% macro generic(recipes, result_attribute, description, separator) -%}
|
||||||
|
<blockquote class="crafting-info">
|
||||||
|
{{ description }} {{
|
||||||
|
recipes
|
||||||
|
|map(attribute="result."~result_attribute)
|
||||||
|
|map("hexdoc_wrap", "code")
|
||||||
|
|join(separator)
|
||||||
|
}}.
|
||||||
|
</blockquote>
|
||||||
|
{%- 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) -%}
|
||||||
|
<div data-toggle="tooltip" title="{{ item.name }}">
|
||||||
|
<img loading="lazy" alt="Image of {{ item.name }}" src="{{ item.texture }}">
|
||||||
|
{% if always_show_count or count > 1 %}
|
||||||
|
<div class="badge">{{ count }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro render_ingredient(ingredient) -%}
|
||||||
|
{% if ingredient is sequence %}
|
||||||
|
{# TODO: properly handle multi-option ingredients #}
|
||||||
|
{{ render_ingredient(ingredient[0]) }}
|
||||||
|
{% elif ingredient is none %}
|
||||||
|
{# air #}
|
||||||
|
<div></div>
|
||||||
|
{% 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 %}<br />{% endif %}
|
||||||
|
<details class="details-collapsible">
|
||||||
|
<summary class="collapse-details">
|
||||||
|
<span class="collapse-recipe-show">{{ _('hexdoc.recipe.show').format(recipe.result.item.name) }}</span>
|
||||||
|
<span class="collapse-recipe-hide">{{ _('hexdoc.recipe.hide').format(recipe.result.item.name) }}</span>
|
||||||
|
</summary>
|
||||||
|
<div class="crafting-table">
|
||||||
|
<img
|
||||||
|
alt="Crafting table"
|
||||||
|
src="{{ 'hexcasting:textures/gui/hexdoc/crafting_table.png'|hexdoc_texture }}"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div class="crafting-table-grid">
|
||||||
|
{% for item in recipe.ingredients %}
|
||||||
|
{{ render_ingredient(item) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="crafting-table-result">
|
||||||
|
{{ render_item(recipe.result.item, recipe.result.count) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
{%- endmacro %}
|
|
@ -2,7 +2,7 @@ summary {
|
||||||
display: list-item;
|
display: list-item;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.spell-collapsible {
|
.details-collapsible {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
@ -10,31 +10,89 @@ details.spell-collapsible {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.collapse-spell {
|
.collapse-details {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: -0.5em -0.5em 0;
|
margin: -0.5em -0.5em 0;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
details.spell-collapsible[open] {
|
.details-collapsible[open] {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] summary.collapse-spell {
|
.details-collapsible[open] .collapse-details {
|
||||||
border-bottom: 1px solid #aaa;
|
border-bottom: 1px solid #aaa;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
details .collapse-spell::before {
|
.details-collapsible .collapse-details.collapse-spell::before {
|
||||||
content: "{{ _('hexdoc.pattern.show') }}";
|
content: "{{ _('hexdoc.pattern.show') }}";
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] .collapse-spell::before {
|
.details-collapsible[open] .collapse-details.collapse-spell::before {
|
||||||
content: "{{ _('hexdoc.pattern.hide') }}";
|
content: "{{ _('hexdoc.pattern.hide') }}";
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote.crafting-info {
|
/* if the collapsible is closed, hide the "hide recipe" message */
|
||||||
font-size: inherit;
|
.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 {
|
a.permalink {
|
||||||
|
|
|
@ -297,8 +297,7 @@ function hookSpoiler(elem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// these are filled by Jinja
|
// these are filled by Jinja
|
||||||
const ROOT_URL = "{{ props.url }}";
|
const BOOK_URL = "{{ props.url }}";
|
||||||
const PAGE_URL = "{{ page_url }}";
|
|
||||||
|
|
||||||
const VERSION = "{{ version }}";
|
const VERSION = "{{ version }}";
|
||||||
const LANG = "{{ lang }}";
|
const LANG = "{{ lang }}";
|
||||||
|
@ -325,7 +324,7 @@ function versionDropdownItem(sitemap, version) {
|
||||||
path = defaultPath;
|
path = defaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dropdownItem(version, ROOT_URL + path);
|
return dropdownItem(version, BOOK_URL + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function versionDropdownItems(sitemap, versions) {
|
function versionDropdownItems(sitemap, versions) {
|
||||||
|
@ -390,7 +389,7 @@ function addDropdowns(sitemap) {
|
||||||
const langs = Object.keys(langPaths).sort();
|
const langs = Object.keys(langPaths).sort();
|
||||||
|
|
||||||
document.getElementById("lang-dropdown").append(
|
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
|
// return sitemap for chaining, i guess
|
||||||
|
@ -399,12 +398,12 @@ function addDropdowns(sitemap) {
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
// fetch the sitemap from the root and use it to generate the navbar
|
// 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(r => r.json())
|
||||||
.then(addDropdowns)
|
.then(addDropdowns)
|
||||||
.catch(e => console.error(e))
|
.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("a.toggle-link").forEach(hookToggle);
|
||||||
document.querySelectorAll(".spoilered").forEach(hookSpoiler);
|
document.querySelectorAll(".spoilered").forEach(hookSpoiler);
|
||||||
|
|
||||||
|
@ -417,3 +416,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||||
}
|
}
|
||||||
requestAnimationFrame((t) => tick(t, t));
|
requestAnimationFrame((t) => tick(t, t));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "pages/patchouli/page.html.jinja" %}
|
{% 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 %}
|
{% block body %}
|
||||||
{{ macros.recipe_block(
|
{{ recipe_macros.generic(
|
||||||
[page.recipe],
|
[page.recipe],
|
||||||
"name",
|
"name",
|
||||||
_("hexdoc.pages.hexcasting.brainsweep.description"),
|
_("hexdoc.pages.hexcasting.brainsweep.description"),
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
{% extends "pages/patchouli/text.html.jinja" %}
|
{% 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 %}
|
{% block inner_body %}
|
||||||
{{ macros.recipe_block(
|
{{ recipe_macros.crafting_table(page.recipes) }}
|
||||||
page.recipes,
|
|
||||||
"item",
|
|
||||||
_("hexdoc.pages.hexcasting.crafting_multi.description"),
|
|
||||||
_("hexdoc.pages.hexcasting.crafting_multi.separator"),
|
|
||||||
) }}
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% endblock inner_body %}
|
{% endblock inner_body %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "pages/patchouli/text.html.jinja" %}
|
{% extends "pages/patchouli/text.html.jinja" %}
|
||||||
|
|
||||||
{% block inner_body %}
|
{% block inner_body %}
|
||||||
<details class="spell-collapsible">
|
<details class="details-collapsible">
|
||||||
<summary class="collapse-spell"></summary>
|
<summary class="collapse-details collapse-spell"></summary>
|
||||||
{% for pattern in page.patterns %}
|
{% for pattern in page.patterns %}
|
||||||
<canvas
|
<canvas
|
||||||
class="spell-viz"
|
class="spell-viz"
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
{% extends "pages/patchouli/text.html.jinja" %}
|
{% extends "pages/patchouli/text.html.jinja" %}
|
||||||
{% include "common/macros.html.jinja" %}
|
{% import "macros/recipes.html.jinja" as recipe_macros with context %}
|
||||||
|
|
||||||
{% block inner_body %}
|
{% block inner_body %}
|
||||||
{{ macros.recipe_block(
|
{{ recipe_macros.crafting_table(page.recipes) }}
|
||||||
page.recipes,
|
|
||||||
"item",
|
|
||||||
_("hexdoc.pages.patchouli.crafting.description"),
|
|
||||||
_("hexdoc.pages.patchouli.crafting.separator"),
|
|
||||||
) }}
|
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
{% endblock inner_body %}
|
{% endblock inner_body %}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block inner_body %}
|
{% block inner_body %}
|
||||||
<p class="img-wrapper">
|
<p class="img-wrapper">
|
||||||
{% for image in page.images %}
|
{% for image in page.images %}
|
||||||
<img src="{{ image|hexdoc_texture_url }}"></img>
|
<img src="{{ image|hexdoc_texture }}"></img>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "pages/patchouli/page.html.jinja" %}
|
{% extends "pages/patchouli/page.html.jinja" %}
|
||||||
{% import "common/macros.html.jinja" as macros %}
|
{% import "macros/formatting.html.jinja" as macros %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if page.title is not none %}
|
{% if page.title is not none %}
|
||||||
|
|
|
@ -26,7 +26,7 @@ RequiredPathOption = Annotated[Path, typer.Option()]
|
||||||
UpdateLatestOption = Annotated[bool, typer.Option(envvar="UPDATE_LATEST")]
|
UpdateLatestOption = Annotated[bool, typer.Option(envvar="UPDATE_LATEST")]
|
||||||
ReleaseOption = Annotated[bool, typer.Option(envvar="RELEASE")]
|
ReleaseOption = Annotated[bool, typer.Option(envvar="RELEASE")]
|
||||||
|
|
||||||
app = typer.Typer()
|
app = typer.Typer(pretty_exceptions_enable=False)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
@ -158,11 +158,10 @@ def serve(
|
||||||
):
|
):
|
||||||
book_path = dst.resolve().relative_to(Path.cwd())
|
book_path = dst.resolve().relative_to(Path.cwd())
|
||||||
|
|
||||||
base_url = f"http://localhost:{port}"
|
book_url = f"/{book_path.as_posix()}"
|
||||||
book_url = f"{base_url}/{book_path.as_posix()}"
|
|
||||||
|
|
||||||
os.environ |= {
|
os.environ |= {
|
||||||
"DEBUG_GITHUBUSERCONTENT": base_url,
|
"DEBUG_GITHUBUSERCONTENT": "",
|
||||||
"GITHUB_PAGES_URL": book_url,
|
"GITHUB_PAGES_URL": book_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +185,9 @@ def serve(
|
||||||
release=release,
|
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:
|
with HTTPServer(("", port), SimpleHTTPRequestHandler) as httpd:
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ from hexdoc.utils.jinja_extensions import (
|
||||||
IncludeRawExtension,
|
IncludeRawExtension,
|
||||||
hexdoc_block,
|
hexdoc_block,
|
||||||
hexdoc_localize,
|
hexdoc_localize,
|
||||||
hexdoc_texture_url,
|
hexdoc_texture,
|
||||||
hexdoc_wrap,
|
hexdoc_wrap,
|
||||||
)
|
)
|
||||||
from hexdoc.utils.path import write_to_path
|
from hexdoc.utils.path import write_to_path
|
||||||
|
@ -50,7 +50,7 @@ def create_jinja_env(props: Properties):
|
||||||
"hexdoc_block": hexdoc_block,
|
"hexdoc_block": hexdoc_block,
|
||||||
"hexdoc_wrap": hexdoc_wrap,
|
"hexdoc_wrap": hexdoc_wrap,
|
||||||
"hexdoc_localize": hexdoc_localize,
|
"hexdoc_localize": hexdoc_localize,
|
||||||
"hexdoc_texture_url": hexdoc_texture_url,
|
"hexdoc_texture": hexdoc_texture,
|
||||||
}
|
}
|
||||||
|
|
||||||
return env
|
return env
|
||||||
|
|
|
@ -4,8 +4,10 @@ __all__ = [
|
||||||
"LocalizedStr",
|
"LocalizedStr",
|
||||||
"Recipe",
|
"Recipe",
|
||||||
"Tag",
|
"Tag",
|
||||||
|
"RenderedItemStack",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from .assets import RenderedItemStack
|
||||||
from .i18n import I18n, LocalizedItem, LocalizedStr
|
from .i18n import I18n, LocalizedItem, LocalizedStr
|
||||||
from .recipe import Recipe
|
from .recipe import Recipe
|
||||||
from .tags import Tag
|
from .tags import Tag
|
||||||
|
|
82
doc/src/hexdoc/minecraft/assets.py
Normal file
82
doc/src/hexdoc/minecraft/assets.py
Normal file
|
@ -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
|
|
@ -20,7 +20,7 @@ from hexdoc.utils.resource_loader import LoaderContext
|
||||||
|
|
||||||
|
|
||||||
@total_ordering
|
@total_ordering
|
||||||
class LocalizedStr(HexdocModel):
|
class LocalizedStr(HexdocModel, frozen=True):
|
||||||
"""Represents a string which has been localized."""
|
"""Represents a string which has been localized."""
|
||||||
|
|
||||||
key: str
|
key: str
|
||||||
|
@ -83,7 +83,7 @@ class LocalizedStr(HexdocModel):
|
||||||
return self.value < other
|
return self.value < other
|
||||||
|
|
||||||
|
|
||||||
class LocalizedItem(LocalizedStr):
|
class LocalizedItem(LocalizedStr, frozen=True):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _localize(cls, i18n: I18n, key: str) -> Self:
|
def _localize(cls, i18n: I18n, key: str) -> Self:
|
||||||
return i18n.localize_item(key)
|
return i18n.localize_item(key)
|
||||||
|
@ -253,6 +253,10 @@ class I18n(HexdocModel):
|
||||||
def localize_key(self, key: str) -> LocalizedStr:
|
def localize_key(self, key: str) -> LocalizedStr:
|
||||||
return self.localize(f"key.{key}")
|
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):
|
class I18nContext(LoaderContext):
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
|
|
|
@ -10,16 +10,12 @@ __all__ = [
|
||||||
"ItemResult",
|
"ItemResult",
|
||||||
]
|
]
|
||||||
|
|
||||||
from .abstract_recipes import Recipe
|
from .abstract_recipes import CraftingRecipe, Recipe
|
||||||
from .ingredients import (
|
from .ingredients import (
|
||||||
ItemIngredient,
|
ItemIngredient,
|
||||||
ItemIngredientOrList,
|
ItemIngredientOrList,
|
||||||
|
ItemResult,
|
||||||
MinecraftItemIdIngredient,
|
MinecraftItemIdIngredient,
|
||||||
MinecraftItemTagIngredient,
|
MinecraftItemTagIngredient,
|
||||||
)
|
)
|
||||||
from .recipes import (
|
from .recipes import CraftingShapedRecipe, CraftingShapelessRecipe
|
||||||
CraftingRecipe,
|
|
||||||
CraftingShapedRecipe,
|
|
||||||
CraftingShapelessRecipe,
|
|
||||||
ItemResult,
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Self
|
||||||
|
|
||||||
from pydantic import ValidationInfo, model_validator
|
from pydantic import ValidationInfo, model_validator
|
||||||
|
from pydantic.functional_validators import ModelWrapValidatorHandler
|
||||||
|
|
||||||
from hexdoc.utils import ResourceLocation, TypeTaggedUnion
|
from hexdoc.utils import ResourceLocation, TypeTaggedUnion
|
||||||
from hexdoc.utils.deserialize import cast_or_raise
|
from hexdoc.utils.deserialize import cast_or_raise
|
||||||
from hexdoc.utils.resource_loader import LoaderContext
|
from hexdoc.utils.resource_loader import LoaderContext
|
||||||
|
|
||||||
|
from .ingredients import ItemResult
|
||||||
|
|
||||||
|
|
||||||
class Recipe(TypeTaggedUnion, type=None):
|
class Recipe(TypeTaggedUnion, type=None):
|
||||||
id: ResourceLocation
|
id: ResourceLocation
|
||||||
|
@ -14,23 +17,34 @@ class Recipe(TypeTaggedUnion, type=None):
|
||||||
group: str | None = None
|
group: str | None = None
|
||||||
category: str | None = None
|
category: str | None = None
|
||||||
|
|
||||||
@model_validator(mode="before")
|
# use wrap validator so we load the file *before* resolving the tagged union
|
||||||
def _pre_root(cls, values: Any, info: ValidationInfo):
|
@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."""
|
"""Loads the recipe from json if the actual value is a resource location str."""
|
||||||
if not info.context:
|
if not info.context:
|
||||||
return values
|
return handler(value)
|
||||||
context = cast_or_raise(info.context, LoaderContext)
|
context = cast_or_raise(info.context, LoaderContext)
|
||||||
|
|
||||||
# if necessary, convert the id to a ResourceLocation
|
# if necessary, convert the id to a ResourceLocation
|
||||||
match values:
|
match value:
|
||||||
case str():
|
case str():
|
||||||
id = ResourceLocation.from_str(values)
|
id = ResourceLocation.from_str(value)
|
||||||
case ResourceLocation():
|
case ResourceLocation():
|
||||||
id = values
|
id = value
|
||||||
case _:
|
case _:
|
||||||
return values
|
return handler(value)
|
||||||
|
|
||||||
# load the recipe
|
# load the recipe
|
||||||
_, data = context.loader.load_resource("data", "recipes", id)
|
_, data = context.loader.load_resource("data", "recipes", id)
|
||||||
logging.getLogger(__name__).debug(f"Load {cls} from {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
|
||||||
|
|
|
@ -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):
|
class ItemIngredient(TypeTaggedUnion, type=None):
|
||||||
|
@ -9,8 +15,29 @@ ItemIngredientOrList = ItemIngredient | list[ItemIngredient]
|
||||||
|
|
||||||
|
|
||||||
class MinecraftItemIdIngredient(ItemIngredient, type=NoValue):
|
class MinecraftItemIdIngredient(ItemIngredient, type=NoValue):
|
||||||
item: ResourceLocation
|
item: RenderedItemStack
|
||||||
|
|
||||||
|
|
||||||
class MinecraftItemTagIngredient(ItemIngredient, type=NoValue):
|
class MinecraftItemTagIngredient(ItemIngredient, type=NoValue):
|
||||||
tag: ResourceLocation
|
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
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator
|
||||||
|
|
||||||
from hexdoc.utils import HexdocModel
|
|
||||||
from hexdoc.utils.compat import HexVersion
|
from hexdoc.utils.compat import HexVersion
|
||||||
|
|
||||||
from ..i18n import LocalizedItem
|
from .abstract_recipes import CraftingRecipe
|
||||||
from .abstract_recipes import Recipe
|
|
||||||
from .ingredients import ItemIngredientOrList
|
from .ingredients import ItemIngredientOrList
|
||||||
|
|
||||||
|
|
||||||
class ItemResult(HexdocModel):
|
class CraftingShapelessRecipe(CraftingRecipe, type="minecraft:crafting_shapeless"):
|
||||||
item: LocalizedItem
|
ingredients: list[ItemIngredientOrList]
|
||||||
count: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class CraftingShapedRecipe(Recipe, type="minecraft:crafting_shaped"):
|
class CraftingShapedRecipe(CraftingRecipe, type="minecraft:crafting_shaped"):
|
||||||
key: dict[str, ItemIngredientOrList]
|
key: dict[str, ItemIngredientOrList]
|
||||||
pattern: list[str]
|
pattern: list[str]
|
||||||
result: ItemResult
|
|
||||||
show_notification: bool | None = None
|
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")
|
@field_validator("show_notification")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_show_notification(cls, value: bool | None):
|
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:
|
case HexVersion.v0_10_x | HexVersion.v0_9_x:
|
||||||
HexVersion.check(value is None, "show_notification")
|
HexVersion.check(value is None, "show_notification")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class CraftingShapelessRecipe(Recipe, type="minecraft:crafting_shapeless"):
|
|
||||||
ingredients: list[ItemIngredientOrList]
|
|
||||||
result: ItemResult
|
|
||||||
|
|
||||||
|
|
||||||
CraftingRecipe = CraftingShapedRecipe | CraftingShapelessRecipe
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ from typing import Self
|
||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator
|
||||||
|
|
||||||
from hexdoc.minecraft import Tag
|
from hexdoc.minecraft import Tag
|
||||||
|
from hexdoc.minecraft.assets import MinecraftAssetsContext
|
||||||
from hexdoc.plugin.manager import PluginManagerContext
|
from hexdoc.plugin.manager import PluginManagerContext
|
||||||
from hexdoc.utils import ResourceLocation
|
from hexdoc.utils import ResourceLocation
|
||||||
|
|
||||||
from .text.formatting import FormattingContext
|
from .text.formatting import FormattingContext
|
||||||
|
|
||||||
|
|
||||||
class BookContext(FormattingContext, PluginManagerContext):
|
class BookContext(FormattingContext, PluginManagerContext, MinecraftAssetsContext):
|
||||||
spoilered_advancements: set[ResourceLocation] = Field(default_factory=set)
|
spoilered_advancements: set[ResourceLocation] = Field(default_factory=set)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
|
|
|
@ -5,7 +5,7 @@ _T = TypeVar("_T")
|
||||||
_P = ParamSpec("_P")
|
_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."""
|
"""Raises StopIteration if the wrapped iterator doesn't yield anything."""
|
||||||
|
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
|
|
|
@ -5,6 +5,7 @@ from jinja2.ext import Extension
|
||||||
from jinja2.parser import Parser
|
from jinja2.parser import Parser
|
||||||
from jinja2.runtime import Context
|
from jinja2.runtime import Context
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
from pydantic import ConfigDict, validate_call
|
||||||
|
|
||||||
from hexdoc.minecraft import I18n, LocalizedStr
|
from hexdoc.minecraft import I18n, LocalizedStr
|
||||||
from hexdoc.patchouli import Book, FormatTree
|
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 import HTMLStream
|
||||||
from hexdoc.patchouli.text.formatting import FormatTree
|
from hexdoc.patchouli.text.formatting import FormatTree
|
||||||
from hexdoc.utils.resource import ResourceLocation
|
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 . import Properties
|
||||||
from .deserialize import cast_or_raise
|
from .deserialize import cast_or_raise
|
||||||
|
@ -101,13 +102,11 @@ def hexdoc_localize(
|
||||||
|
|
||||||
|
|
||||||
@pass_context
|
@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:
|
try:
|
||||||
metadata = cast_or_raise(
|
mod_metadata = cast_or_raise(context["mod_metadata"], dict[str, HexdocMetadata])
|
||||||
context["mod_metadata"],
|
return resolve_texture_from_metadata(mod_metadata, id)
|
||||||
dict[str, HexdocMetadata],
|
|
||||||
)[id.namespace]
|
|
||||||
return f"{metadata.asset_url}/{metadata.textures[id].as_posix()}"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
e.add_note(f"id:\n {id}")
|
e.add_note(f"id:\n {id}")
|
||||||
raise
|
raise
|
||||||
|
|
|
@ -87,6 +87,16 @@ class TemplateProps(StripHiddenModel):
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class MinecraftAssetsProps(StripHiddenModel):
|
||||||
|
ref: str
|
||||||
|
version: str
|
||||||
|
|
||||||
|
|
||||||
|
class TexturesProps(StripHiddenModel):
|
||||||
|
missing: list[ResourceLocation]
|
||||||
|
override: dict[ResourceLocation, ResourceLocation]
|
||||||
|
|
||||||
|
|
||||||
class Properties(StripHiddenModel):
|
class Properties(StripHiddenModel):
|
||||||
env: EnvironmentVariableProps
|
env: EnvironmentVariableProps
|
||||||
|
|
||||||
|
@ -104,6 +114,11 @@ class Properties(StripHiddenModel):
|
||||||
|
|
||||||
entry_id_blacklist: set[ResourceLocation] = Field(default_factory=set)
|
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
|
template: TemplateProps
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -90,6 +90,10 @@ class BaseResourceLocation:
|
||||||
def _ser_model(self) -> str:
|
def _ser_model(self) -> str:
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self) -> ResourceLocation:
|
||||||
|
return ResourceLocation(self.namespace, self.path)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.namespace}:{self.path}"
|
return f"{self.namespace}:{self.path}"
|
||||||
|
|
||||||
|
@ -175,6 +179,9 @@ class ItemStack(BaseResourceLocation, regex=_make_regex(count=True, nbt=True)):
|
||||||
count: int | None = None
|
count: int | None = None
|
||||||
nbt: str | 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:
|
def i18n_key(self, root: str = "item") -> str:
|
||||||
# TODO: is this how i18n works????? (apparently, because it's working)
|
# TODO: is this how i18n works????? (apparently, because it's working)
|
||||||
return f"{root}.{self.namespace}.{self.path.replace('/', '.')}"
|
return f"{root}.{self.namespace}.{self.path.replace('/', '.')}"
|
||||||
|
|
|
@ -15,7 +15,7 @@ from pydantic.dataclasses import dataclass
|
||||||
|
|
||||||
from hexdoc.plugin.manager import PluginManager
|
from hexdoc.plugin.manager import PluginManager
|
||||||
from hexdoc.utils.deserialize import JSONDict, decode_json_dict
|
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.model import DEFAULT_CONFIG, HexdocModel, ValidationContext
|
||||||
from hexdoc.utils.path import strip_suffixes, write_to_path
|
from hexdoc.utils.path import strip_suffixes, write_to_path
|
||||||
|
|
||||||
|
@ -47,12 +47,6 @@ class HexdocMetadata(HexdocModel):
|
||||||
def path(cls, modid: str) -> Path:
|
def path(cls, modid: str) -> Path:
|
||||||
return Path(f"{modid}.hexdoc.json")
|
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)
|
@dataclass(config=DEFAULT_CONFIG, kw_only=True)
|
||||||
class ModResourceLoader:
|
class ModResourceLoader:
|
||||||
|
@ -114,21 +108,23 @@ class ModResourceLoader:
|
||||||
for path_resource_dir in stack.enter_context(resource_dir.load(pm))
|
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
|
# export this mod's metadata
|
||||||
loader.mod_metadata[props.modid] = metadata = HexdocMetadata(
|
metadata = HexdocMetadata(
|
||||||
book_url=f"{props.url}/v/{version}",
|
book_url=f"{props.url}/v/{version}",
|
||||||
asset_url=props.env.githubusercontent,
|
asset_url=props.env.githubusercontent,
|
||||||
textures=loader._map_own_assets("textures", root=repo_root),
|
textures=loader._map_own_assets("textures", root=repo_root),
|
||||||
sounds=loader._map_own_assets("sounds", 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
|
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):
|
def _map_own_assets(self, folder: str, *, root: str | Path):
|
||||||
return {
|
return {
|
||||||
id: path.resolve().relative_to(root)
|
id: path.resolve().relative_to(root)
|
||||||
|
@ -170,7 +166,7 @@ class ModResourceLoader:
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
# TODO: maybe this should take lang as a variable?
|
# TODO: maybe this should take lang as a variable?
|
||||||
@must_yield
|
@must_yield_something
|
||||||
def load_book_assets(
|
def load_book_assets(
|
||||||
self,
|
self,
|
||||||
book_id: ResourceLocation,
|
book_id: ResourceLocation,
|
||||||
|
@ -494,3 +490,17 @@ class LoaderContext(ValidationContext):
|
||||||
@property
|
@property
|
||||||
def props(self):
|
def props(self):
|
||||||
return self.loader.props
|
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:])
|
||||||
|
|
|
@ -33,15 +33,16 @@ classifiers = [
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typing_extensions>=4.6.1,<5",
|
"typing_extensions~=4.6",
|
||||||
"importlib_resources>=6.0.1,<7",
|
"importlib_resources~=6.0",
|
||||||
"more_itertools>=10.1.0,<11",
|
"more_itertools~=10.1",
|
||||||
"pydantic>=2.3.0,<3,!=2.4.0",
|
"pydantic~=2.3,!=2.4.0",
|
||||||
"pydantic_settings>=2.0.3,<3",
|
"pydantic_settings~=2.0",
|
||||||
"Jinja2>=3.1.2,<4",
|
"Jinja2~=3.1",
|
||||||
"pyjson5>=1.6.3,<2",
|
"pyjson5~=1.6",
|
||||||
"pluggy>=1.3.0,<2",
|
"pluggy~=1.3",
|
||||||
"typer[all]>=0.9.0,<1",
|
"typer[all]~=0.9.0",
|
||||||
|
"requests~=2.31",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue