All the pages, but the tests are failing

This commit is contained in:
object-Object 2023-07-19 09:26:40 -04:00
parent 7678b74b2e
commit 8c1bbc8179
29 changed files with 240 additions and 142 deletions

View file

@ -1,4 +1,3 @@
from html import escape
from typing import Any
from jinja2 import nodes
@ -41,15 +40,25 @@ def hexdoc_minify(value: str) -> str:
def hexdoc_block(value: Any, allow_none: bool = False) -> str:
match value:
case LocalizedStr() | str():
# use Markup to tell Jinja not to escape this string for us
lines = str(value).splitlines()
return "<br />".join(escape(line) for line in lines)
return Markup("<br />".join(Markup.escape(line) for line in lines))
case FormatTree():
with HTMLStream() as out:
with value.style.element(out):
for child in value.children:
out.write(hexdoc_block(child))
return out.getvalue()
return Markup(out.getvalue())
case None if allow_none:
return ""
case _:
raise TypeError(value)
def hexdoc_wrap(value: str, *args: str):
tag, *attributes = args
if attributes:
attributes = " " + " ".join(attributes)
else:
attributes = ""
return f"<{tag}{attributes}>{value}</{tag}>"

View file

@ -120,6 +120,10 @@ class Properties(HexDocModel[Any]):
for stub in platform.pattern_stubs
]
def asset_url(self, asset: ResourceLocation, path: str = "assets") -> str:
base_url = self.base_asset_urls[asset.namespace]
return f"{base_url}/{path}/{asset.full_path}"
@field_validator("default_recipe_dir_index_")
def _check_default_recipe_dir(cls, value: int, info: FieldValidationInfo) -> int:
num_dirs = len(info.data["recipe_dirs"])

View file

@ -1,34 +1,31 @@
from abc import ABC, abstractmethod
from types import NoneType
from typing import Any, cast
from pydantic import Field, ValidationInfo, model_validator
from pydantic import ValidationInfo, model_validator
from hexcasting.pattern import RawPatternInfo
from minecraft.i18n import LocalizedStr
from minecraft.resource import ResourceLocation
from patchouli.page import PageWithTitle
from patchouli.page import PageWithText
from .hex_book import AnyHexContext, HexContext
# TODO: make anchor required (breaks because of Greater Sentinel)
class PageWithPattern(PageWithTitle[AnyHexContext], ABC, type=None):
title_: None = Field(default=None, include=True)
op_id: ResourceLocation | None = None
header: LocalizedStr | None = None
class PageWithPattern(PageWithText[AnyHexContext], type=None):
header: LocalizedStr
patterns: list[RawPatternInfo]
input: str | None = None
output: str | None = None
hex_size: int | None = None
# must be after op_id, so just put it last
patterns_: RawPatternInfo | list[RawPatternInfo] = Field(
alias="patterns", include=True
)
@property
@abstractmethod
def name(self) -> LocalizedStr:
...
@model_validator(mode="before")
def _pre_root_patterns(cls, values: dict[str, Any]):
# patterns may be a list or a single pattern, so make sure we always get a list
patterns = values.get("patterns")
if isinstance(patterns, (NoneType, list)):
return values
return values | {"patterns": [patterns]}
@property
def args(self) -> str | None:
@ -41,39 +38,19 @@ class PageWithPattern(PageWithTitle[AnyHexContext], ABC, type=None):
@property
def title(self) -> str:
suffix = f" ({self.args})" if self.args else ""
return self.name.value + suffix
@property
def patterns(self) -> list[RawPatternInfo]:
if isinstance(self.patterns_, list):
return self.patterns_
return [self.patterns_]
return f"{self.header}{suffix}"
class PageWithOpPattern(PageWithPattern[AnyHexContext], type=None):
name_: LocalizedStr = Field(include=True)
op_id: ResourceLocation
header: None = None
@property
def name(self) -> LocalizedStr:
return self.name_
@model_validator(mode="before")
def _check_name(cls, values: dict[str, Any], info: ValidationInfo):
def _pre_root_header(cls, values: dict[str, Any], info: ValidationInfo):
context = cast(HexContext, info.context)
if not context or (op_id := values.get("op_id")) is None:
if not context:
return values
name = context["i18n"].localize_pattern(op_id)
return values | {"name_": name}
class PageWithRawPattern(PageWithPattern[AnyHexContext], type=None):
op_id: None = None
header: LocalizedStr
@property
def name(self) -> LocalizedStr:
return self.header
# use the pattern name as the header
return values | {
"header": context["i18n"].localize_pattern(values["op_id"]),
}

View file

@ -6,9 +6,10 @@ from hexcasting.pattern import RawPatternInfo
from minecraft.i18n import LocalizedStr
from minecraft.recipe import CraftingRecipe
from minecraft.resource import ResourceLocation
from patchouli.page import PageWithCraftingRecipes, PageWithText
from patchouli.page import PageWithText
from patchouli.page.abstract_pages import PageWithTitle
from .abstract_hex_pages import PageWithOpPattern, PageWithRawPattern
from .abstract_hex_pages import PageWithOpPattern, PageWithPattern
from .hex_book import HexContext
from .hex_recipes import BrainsweepRecipe
@ -17,52 +18,49 @@ class LookupPatternPage(
PageWithOpPattern[HexContext],
type="hexcasting:pattern",
):
patterns_: list[RawPatternInfo]
@model_validator(mode="before")
def _check_patterns(cls, data: dict[str, Any], info: ValidationInfo):
def _pre_root_lookup(cls, values: dict[str, Any], info: ValidationInfo):
context = cast(HexContext, info.context)
if not context:
return data
return values
# look up the pattern from the op id
op_id = ResourceLocation.from_str(data["op_id"])
op_id = ResourceLocation.from_str(values["op_id"])
pattern = context["patterns"][op_id]
return data | {"patterns_": [pattern], "op_id": op_id}
return values | {
"op_id": op_id,
"patterns": [pattern],
}
class ManualOpPatternPage(
PageWithOpPattern[HexContext],
type="hexcasting:manual_pattern",
template_name="PageWithPattern",
):
pass
class ManualRawPatternPage(
PageWithRawPattern[HexContext],
PageWithPattern[HexContext],
type="hexcasting:manual_pattern",
template_name="PageWithPattern",
):
pass
class ManualPatternNosigPage(
PageWithRawPattern[HexContext],
PageWithPattern[HexContext],
type="hexcasting:manual_pattern_nosig",
template_name="PageWithPattern",
):
input: None = None
output: None = None
class CraftingMultiPage(
PageWithCraftingRecipes[HexContext],
type="hexcasting:crafting_multi",
):
heading: LocalizedStr # ...heading?
recipes_: list[CraftingRecipe] = Field(alias="recipes", include=True)
@property
def recipes(self) -> list[CraftingRecipe]:
return self.recipes_
class CraftingMultiPage(PageWithTitle[HexContext], type="hexcasting:crafting_multi"):
heading: LocalizedStr # TODO: should this be renamed to header?
recipes: list[CraftingRecipe]
class BrainsweepPage(PageWithText[HexContext], type="hexcasting:brainsweep"):

View file

@ -9,8 +9,13 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined
# from jinja2.sandbox import SandboxedEnvironment
from tap import Tap
from common.jinja_extensions import (
IncludeRawExtension,
hexdoc_block,
hexdoc_minify,
hexdoc_wrap,
)
from common.properties import Properties
from common.templates import IncludeRawExtension, hexdoc_block, hexdoc_minify
from hexcasting.hex_book import HexBook
if sys.version_info < (3, 11):
@ -44,8 +49,11 @@ def main(args: Args) -> None:
autoescape=False,
extensions=[IncludeRawExtension],
)
env.filters["hexdoc_minify"] = hexdoc_minify
env.filters["hexdoc_block"] = hexdoc_block
env.filters |= dict( # for some reason, pylance doesn't like the {} here
hexdoc_minify=hexdoc_minify,
hexdoc_block=hexdoc_block,
hexdoc_wrap=hexdoc_wrap,
)
# load and render template
template = env.get_template(props.template)
@ -53,8 +61,7 @@ def main(args: Args) -> None:
props.template_args
| {
"book": book,
"spoilers": props.spoilers,
"blacklist": props.blacklist,
"props": props,
}
)

View file

@ -32,7 +32,7 @@ class LocalizedStr(HexDocModel[I18nContext]):
return cls(key=key, value=key)
@classmethod
def skip_key(cls, value: str) -> Self:
def with_value(cls, value: str) -> Self:
"""Returns an instance of this class with an empty key."""
return cls(key="", value=value)

View file

@ -56,6 +56,10 @@ class BaseResourceLocation:
def _ser_model(self) -> str:
return str(self)
@property
def full_path(self) -> str:
return f"{self.namespace}/{self.path}"
def __repr__(self) -> str:
return f"{self.namespace}:{self.path}"

View file

@ -1,6 +1,5 @@
__all__ = [
"Page",
"PageWithCraftingRecipes",
"PageWithText",
"PageWithTitle",
"CraftingPage",
@ -16,7 +15,7 @@ __all__ = [
"TextPage",
]
from .abstract_pages import Page, PageWithCraftingRecipes, PageWithText, PageWithTitle
from .abstract_pages import Page, PageWithText, PageWithTitle
from .pages import (
CraftingPage,
EmptyPage,

View file

@ -1,12 +1,10 @@
from abc import ABC, abstractmethod
from typing import Any, Self
from typing import Any, ClassVar, Self
from pydantic import Field, model_validator
from pydantic import model_validator
from pydantic.functional_validators import ModelWrapValidatorHandler
from common.tagged_union import TypeTaggedUnion
from common.tagged_union import TagValue, TypeTaggedUnion
from minecraft.i18n import LocalizedStr
from minecraft.recipe import CraftingRecipe
from minecraft.resource import ResourceLocation
from ..context import AnyBookContext
@ -19,10 +17,32 @@ class Page(TypeTaggedUnion[AnyBookContext], group="hexdoc.Page", type=None):
See: https://vazkiimods.github.io/Patchouli/docs/patchouli-basics/page-types
"""
__template: ClassVar[str]
advancement: ResourceLocation | None = None
flag: str | None = None
anchor: str | None = None
def __init_subclass__(
cls,
*,
type: TagValue | None,
template: str | None = None,
template_name: str | None = None,
) -> None:
super().__init_subclass__(group=None, type=type)
# jinja template
match template, template_name:
case str(), None:
cls.__template = template
case None, str():
cls.__template = f"pages/{template_name}.html.jinja"
case None, None:
cls.__template = f"pages/{cls.__name__}.html.jinja"
case _:
raise ValueError("Must specify at most one of template, template_name")
@model_validator(mode="wrap")
@classmethod
def _pre_root(cls, value: str | Any, handler: ModelWrapValidatorHandler[Self]):
@ -30,21 +50,14 @@ class Page(TypeTaggedUnion[AnyBookContext], group="hexdoc.Page", type=None):
return handler({"type": "patchouli:text", "text": value})
return handler(value)
@property
def template(self) -> str:
return self.__template
class PageWithText(Page[AnyBookContext], type=None):
text: FormatTree | None = None
class PageWithTitle(PageWithText[AnyBookContext], type=None):
title_: LocalizedStr | None = Field(default=None, alias="title")
@property
def title(self) -> str | None:
return self.title_.value if self.title_ else None
class PageWithCraftingRecipes(PageWithText[AnyBookContext], ABC, type=None):
@property
@abstractmethod
def recipes(self) -> list[CraftingRecipe]:
...
title: LocalizedStr | None = None

View file

@ -1,17 +1,19 @@
from typing import Any
from pydantic import Field
from minecraft.i18n import LocalizedItem, LocalizedStr
from minecraft.recipe import CraftingRecipe
from minecraft.resource import Entity, ItemStack, ResourceLocation
from patchouli.context import BookContext
from ..text import FormatTree
from .abstract_pages import Page, PageWithCraftingRecipes, PageWithText, PageWithTitle
from .abstract_pages import Page, PageWithText, PageWithTitle
class TextPage(PageWithTitle[BookContext], type="patchouli:text"):
class TextPage(
PageWithTitle[BookContext],
type="patchouli:text",
template_name="PageWithTitle",
):
text: FormatTree
@ -20,22 +22,15 @@ class ImagePage(PageWithTitle[BookContext], type="patchouli:image"):
border: bool = False
class CraftingPage(
PageWithCraftingRecipes[BookContext],
type="patchouli:crafting",
):
class CraftingPage(PageWithTitle[BookContext], type="patchouli:crafting"):
recipe: CraftingRecipe
recipe2: CraftingRecipe | None = None
@property
def recipes(self) -> list[CraftingRecipe]:
recipes = [self.recipe]
if self.recipe2:
recipes.append(self.recipe2)
return recipes
return [r for r in [self.recipe, self.recipe2] if r is not None]
# TODO: this should probably inherit PageWithRecipes too
class SmeltingPage(PageWithTitle[BookContext], type="patchouli:smelting"):
recipe: ItemStack
recipe2: ItemStack | None = None
@ -75,19 +70,13 @@ class LinkPage(TextPage, type="patchouli:link"):
class RelationsPage(PageWithTitle[BookContext], type="patchouli:relations"):
entries: list[ResourceLocation]
title_: LocalizedStr = Field(
default=LocalizedStr.skip_key("Related Chapters"),
alias="title",
)
title: LocalizedStr = LocalizedStr.with_value("Related Chapters")
class QuestPage(PageWithTitle[BookContext], type="patchouli:quest"):
trigger: ResourceLocation | None = None
title_: LocalizedStr = Field(
default=LocalizedStr.skip_key("Objective"),
alias="title",
)
title: LocalizedStr = LocalizedStr.with_value("Objective")
class EmptyPage(Page[BookContext], type="patchouli:empty"):
class EmptyPage(Page[BookContext], type="patchouli:empty", template_name="Page"):
draw_filler: bool = True

View file

@ -4,7 +4,7 @@
<h1 class="book-title">
{{ book.name }}
</h1>
{{ book.landing_text | hexdoc_block }}
{{ book.landing_text|hexdoc_block }}
</header>
{% endif %}
<nav>
@ -12,7 +12,7 @@
</nav>
<main class="book-body">
{% for category in book.categories.values() %}
{% include "category.html.jinja" %}
{% endfor %}
{% include "category.html.jinja" -%}
{% endfor -%}
</main>
</div>

View file

@ -2,11 +2,11 @@
<section id="{{ category.id.path }}">
{% call macros.maybe_spoilered(category) %}
{{ macros.header(category, "category-title") }}
{{ category.description | hexdoc_block }}
{{ macros.section_header(category, "h2", "category-title") }}
{{ category.description|hexdoc_block }}
{% endcall %}
{% for entry in category.entries %}
{% if entry.id not in blacklist %}
{% if entry.id not in props.blacklist %}
{% include "entry.html.jinja" %}
{% endif %}
{% endfor %}

View file

@ -2,9 +2,9 @@
<div id="{{ entry.id.path }}">
{% call macros.maybe_spoilered(entry) %}
{{ macros.header(entry, "entry-title") }}
{{ macros.section_header(entry, "h3", "entry-title") }}
{% for page in entry.pages %}
{% include "pages/" ~ page.__class__.__name__ ~ ".html.jinja" %}
{% include page.template %}
{% endfor %}
{% endcall %}
</div>
</div>

View file

@ -26,10 +26,21 @@
</a>
{%- endmacro %}
{% macro header(value, class_name) %}
<h3 class="{{ class_name }} page-header">
{{ value.name | hexdoc_block }}
{% macro section_header(value, header_tag, class_name) %}
<{{ header_tag }} class="{{ class_name }} page-header">
{{ value.name|hexdoc_block }}
{{ jump_to_top() }}
{{ permalink(value.id.path) }}
</h3>
</{{ header_tag }}>
{%- endmacro %}
{% macro recipe_block(recipes, result_attribute, description, separator) %}
<blockquote class="crafting-info">
Depicted in the book: {{ description }} {{
recipes
|map(attribute="result." ~ result_attribute)
|map("hexdoc_wrap", "code")
|join(separator)
}}.
</blockquote>
{%- endmacro %}

View file

@ -126,7 +126,9 @@
</noscript>
<script>
{# TODO: kinda hacky #}
{% include_raw "main.js" %}
{% filter indent(6) %}
{% include_raw "main.js" %}
{% endfilter %}
</script>

View file

@ -0,0 +1,7 @@
{% extends "pages/PageWithText.html.jinja" %}
{% include "macros.html.jinja" %}
{% block body %}
{{ macros.recipe_block([page.recipe], "name", "A mind-flaying recipe producing the", "") }}
{{ super() }}
{% endblock body %}

View file

@ -0,0 +1,7 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% include "macros.html.jinja" %}
{% block inner_body %}
{{ macros.recipe_block(page.recipes, "item", "Several crafting recipes, for the", ", ") }}
{{ super() }}
{% endblock inner_body %}

View file

@ -0,0 +1,7 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% include "macros.html.jinja" %}
{% block inner_body %}
{{ macros.recipe_block(page.recipes, "item", "The crafting recipe for the", " and ") }}
{{ super() }}
{% endblock inner_body %}

View file

@ -0,0 +1,10 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% block inner_body %}
<p class="img-wrapper">
{% for image in page.images %}
<img src="{{ props.asset_url(image) }}"></img>
{% endfor %}
</p>
{{ super() }}
{% endblock inner_body %}

View file

@ -0,0 +1,8 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% block inner_body %}
{{ super() }}
<h4 class="linkout">
<a href="{{ page.url }}">{{ page.link_text }}</a>
</h4>
{% endblock inner_body %}

View file

@ -0,0 +1,3 @@
{% extends "pages/PageWithPattern.html.jinja" %}
{% block title_attrs %} class="pattern-title"{% endblock %}

12
doc/templates/pages/Page.html.jinja vendored Normal file
View file

@ -0,0 +1,12 @@
{% if page.anchor %}
{# set a variable so children can use this value #}
{% set page_anchor_id = entry.id.path ~ '@' ~ page.anchor %}
<div id="{{ page_anchor_id }}">
{# not required because EmptyPage uses this template directly #}
{% block body scoped %}{% endblock %}
</div>
{% else %}
{# can't define a block twice in the same template, so just reference it with self #}
{{ self.body() }}
{% endif %}
<br />

View file

@ -0,0 +1,13 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% block inner_body %}
<details class="spell-collapsible">
<summary class="collapse-spell"></summary>
{% for pattern in page.patterns %}
<canvas class="spell-viz" width=216 height=216 data-string="{{ pattern.signature }}" data-start="{{ pattern.startdir.name.lower() }}" data-per-world="{{ pattern.is_per_world }}">
Your browser does not support visualizing patterns. Pattern code: {{ pattern.signature }}
</canvas>
{% endfor %}
</details>
{{ super() }}
{% endblock inner_body %}

View file

@ -0,0 +1,5 @@
{% extends "pages/Page.html.jinja" %}
{% block body %}
{{ page.text|hexdoc_block(true) }}
{% endblock body %}

View file

@ -0,0 +1,19 @@
{% extends "pages/PageWithText.html.jinja" %}
{% import "macros.html.jinja" as macros %}
{% block body %}
{% if page.title is not none %}
{# we need title_attrs for PageWithPattern #}
<h4{% block title_attrs %}{% endblock %}>
{{ page.title }}
{# page_anchor_id is conditionally defined in Page #}
{% if page_anchor_id is defined %}
{{ macros.permalink(page_anchor_id) }}
{% endif %}
</h4>
{% endif %}
{# pretty sure this is the only good way to do this #}
{# and yes, winfy - this probably would have worked just fine in python too #}
{% set outer_body = super() %}
{% block inner_body scoped %}{{ outer_body }}{% endblock %}
{% endblock body %}

View file

@ -0,0 +1,8 @@
{% extends "pages/PageWithTitle.html.jinja" %}
{% block inner_body %}
<h4 class="spotlight-title page-header">
{{ page.item }}
</h4>
{{ super() }}
{% endblock inner_body %}

View file

@ -1,8 +0,0 @@
{% if page.anchor %}
<div id="{{ entry.id.path ~ '#' ~ page.anchor }}">
{% block body scoped required %}{% endblock %}
</div>
{% else %}
{% self.body() %}
{% endif %}
<br />

View file

@ -1,6 +0,0 @@
{% extends "pages/abstract/Page.html.jinja" %}
{% block body %}
{% if page.title is not none %}
{% endif %}
{% endblock %}

View file

@ -1,7 +1,7 @@
{% import "macros.html.jinja" as macros %}
<h2 id="table-of-contents" class="page-header">
Table of Contents
<a href="javascript:void(0)" class="permalink toggle-link small" data_target="toc-category" title="Toggle all">
<a href="javascript:void(0)" class="permalink toggle-link small" data-target="toc-category" title="Toggle all">
<i class="bi bi-list-nested"></i>
</a>
{{ macros.permalink("table-of-contents") }}