Update docgen to 0.11, mostly
This commit is contained in:
parent
b91b37b70a
commit
3e2afd13a3
29 changed files with 299 additions and 186 deletions
20
.github/workflows/build_docs.yml
vendored
20
.github/workflows/build_docs.yml
vendored
|
@ -1,8 +1,9 @@
|
|||
name: Build the Python doc-gen
|
||||
name: Build the web book
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_docs:
|
||||
|
@ -11,19 +12,30 @@ jobs:
|
|||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install docgen
|
||||
run: pip install ./doc
|
||||
|
||||
- name: Generate file
|
||||
run: doc/collate_data.py Common/src/main/resources hexcasting thehexbook doc/template.html index.html.uncommitted
|
||||
run: hexdoc doc/properties.toml -o index.html.uncommitted
|
||||
|
||||
- name: Check out gh-pages
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
ref: gh-pages
|
||||
|
||||
- name: Overwrite file and commmit
|
||||
run: |
|
||||
mv index.html.uncommitted index.html
|
||||
git config user.name "Documentation Generation Bot"
|
||||
git config user.name "HexDoc Bot"
|
||||
git config user.email "noreply@github.com"
|
||||
git add index.html
|
||||
git diff-index --quiet HEAD || git commit -m "Update docs at index.html from $GITHUB_REF"
|
||||
git diff-index --quiet HEAD || git commit -m "Update web book from $GITHUB_REF"
|
||||
|
||||
- name: Upload changes
|
||||
run: git push
|
||||
|
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"python.analysis.exclude": ["**"],
|
||||
"python.analysis.indexing": false,
|
||||
}
|
|
@ -2,6 +2,5 @@
|
|||
"name": "hexcasting.category.greatwork",
|
||||
"description": "hexcasting.category.greatwork.desc",
|
||||
"icon": "minecraft:music_disc_11",
|
||||
"sortnum": 3,
|
||||
"entry_color": "54398a"
|
||||
"sortnum": 3
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
},
|
||||
{
|
||||
"type": "patchouli:crafting",
|
||||
"recipe": "hexcasting:empty_directrix",
|
||||
"recipe": "hexcasting:directrix/empty",
|
||||
"text": "hexcasting.page.directrix.empty_directrix"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hexcasting.entry.fanciful_staves",
|
||||
"category": "hexcasting:greatwork",
|
||||
"icon": "hexcasting:mindsplice_staff",
|
||||
"icon": "hexcasting:staff/mindsplice",
|
||||
"sortnum": 6,
|
||||
"advancement": "hexcasting:enlightenment",
|
||||
"entry_color": "54398a",
|
||||
|
@ -12,8 +12,8 @@
|
|||
},
|
||||
{
|
||||
"type": "patchouli:crafting",
|
||||
"recipe": "hexcasting:quenched_staff",
|
||||
"recipe2": "hexcasting:mindsplice_staff"
|
||||
"recipe": "hexcasting:staff/quenched",
|
||||
"recipe2": "hexcasting:staff/mindsplice"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
{
|
||||
"type": "patchouli:crafting",
|
||||
"recipe": "hexcasting:empty_impetus",
|
||||
"recipe": "hexcasting:impetus/empty",
|
||||
"text": "hexcasting.page.impetus.empty_impetus"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,8 +15,7 @@
|
|||
},
|
||||
{
|
||||
"type": "patchouli:crafting",
|
||||
"recipe": "hexcasting:edified_planks",
|
||||
"recipe2": "hexcasting:edified_wood"
|
||||
"recipe": "hexcasting:edified_planks"
|
||||
},
|
||||
{
|
||||
"type": "patchouli:crafting",
|
||||
|
|
|
@ -38,8 +38,8 @@
|
|||
"hexcasting:dye_colorizer_red",
|
||||
"hexcasting:dye_colorizer_white",
|
||||
"hexcasting:dye_colorizer_yellow",
|
||||
"hexcasting:empty_directrix",
|
||||
"hexcasting:empty_impetus",
|
||||
"hexcasting:directrix/empty",
|
||||
"hexcasting:impetus/empty",
|
||||
"hexcasting:focus",
|
||||
"hexcasting:jeweler_hammer",
|
||||
"hexcasting:lens",
|
||||
|
|
4
doc/.vscode/HexMod_doc.code-workspace
vendored
4
doc/.vscode/HexMod_doc.code-workspace
vendored
|
@ -9,5 +9,7 @@
|
|||
"path": "../.."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
"settings": {
|
||||
"python.languageServer": "Pylance",
|
||||
}
|
||||
}
|
|
@ -4,25 +4,21 @@ modid = "hexcasting"
|
|||
book_name = "thehexbook"
|
||||
is_0_black = false
|
||||
|
||||
recipe_dirs = [
|
||||
"{fabric.generated}/data/{modid}/recipes",
|
||||
"{forge.generated}/data/{modid}/recipes",
|
||||
]
|
||||
default_recipe_dir = 0
|
||||
|
||||
# NOTE: _Raw means "don't apply variable interpolation to this value"
|
||||
pattern_regex = {_Raw='HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?'}
|
||||
pattern_regex = {_Raw='make\(\s*"(?P<name>[a-zA-Z0-9_\/]+)",\s*(?:new )?(?:ActionRegistryEntry|OperationAction)\(\s*HexPattern\.fromAngles\(\s*"(?P<signature>[aqweds]+)",\s*HexDir.(?P<startdir>\w+)\)'}
|
||||
|
||||
template = "main.html.jinja"
|
||||
template_dirs = []
|
||||
template_packages = []
|
||||
|
||||
spoilers = [
|
||||
spoilered_advancements = [
|
||||
"hexcasting:opened_eyes",
|
||||
"hexcasting:y_u_no_cast_angy",
|
||||
"hexcasting:enlightenment",
|
||||
"hexcasting:lore/*",
|
||||
]
|
||||
blacklist = []
|
||||
entry_id_blacklist = []
|
||||
|
||||
|
||||
[template_args]
|
||||
title = "Hex Book"
|
||||
|
@ -33,42 +29,56 @@ icon_href = "icon.png"
|
|||
is_bleeding_edge = true
|
||||
show_landing_text = true
|
||||
|
||||
|
||||
[base_asset_urls]
|
||||
hexcasting = "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources"
|
||||
|
||||
|
||||
[i18n]
|
||||
default_lang = "en_us"
|
||||
filename = "{default_lang}.json"
|
||||
[i18n.extra]
|
||||
"item.minecraft.amethyst_shard" = "Amethyst Shard"
|
||||
"item.minecraft.budding_amethyst" = "Budding Amethyst"
|
||||
"block.hexcasting.slate" = "Blank Slate"
|
||||
filename = "{default_lang}.flatten.json5"
|
||||
|
||||
[i18n.extra]
|
||||
"item.minecraft.amethyst_shard" = "Amethyst Shard"
|
||||
"item.minecraft.budding_amethyst" = "Budding Amethyst"
|
||||
"block.hexcasting.slate" = "Blank Slate"
|
||||
|
||||
[i18n.keys]
|
||||
use = "Right Click"
|
||||
sneak = "Left Shift"
|
||||
jump = "Space"
|
||||
|
||||
|
||||
# platforms
|
||||
|
||||
[common]
|
||||
src = "../Common/src"
|
||||
# NOTE: {...} is variable interpolation from the current table
|
||||
package = "{src}/main/java/at/petrak/hexcasting"
|
||||
resources = "{src}/main/resources"
|
||||
generated = "{src}/generated/resources"
|
||||
pattern_stubs = [
|
||||
"{package}/common/casting/RegisterPatterns.java",
|
||||
"{package}/interop/pehkui/PehkuiInterop.java",
|
||||
]
|
||||
|
||||
[[common.pattern_stubs]]
|
||||
path = "{^package}/common/lib/hex/HexActions.java"
|
||||
regex = "{^^pattern_regex}"
|
||||
|
||||
|
||||
[fabric]
|
||||
src = "../Fabric/src"
|
||||
package = "{src}/main/java/at/petrak/hexcasting/fabric"
|
||||
resources = "{src}/main/resources"
|
||||
generated = "{src}/generated/resources"
|
||||
pattern_stubs = [
|
||||
"{package}/interop/gravity/GravityApiInterop.java",
|
||||
]
|
||||
recipes = "{generated}/data/{^modid}/recipes"
|
||||
tags = "{generated}/data/{^modid}/tags"
|
||||
|
||||
[[fabric.pattern_stubs]]
|
||||
path = "{^package}/FabricHexInitializer.kt"
|
||||
regex = "{^^pattern_regex}"
|
||||
|
||||
|
||||
[forge]
|
||||
src = "../Forge/src"
|
||||
package = "{src}/main/java/at/petrak/hexcasting/forge"
|
||||
resources = "{src}/main/resources"
|
||||
generated = "{src}/generated/resources"
|
||||
recipes = "{generated}/data/{^modid}/recipes"
|
||||
tags = "{generated}/data/{^modid}/tags"
|
||||
|
|
|
@ -14,9 +14,9 @@ readme = "README.md"
|
|||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"typing_extensions~=4.7.0",
|
||||
"typed-argument-parser~=1.8.0",
|
||||
"pydantic~=2.1.1",
|
||||
"Jinja2~=3.1.2",
|
||||
"pyjson5~=1.6.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
@ -47,6 +47,9 @@ hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
|
|||
hexdoc-minecraft = "hexdoc.minecraft.recipe.ingredients"
|
||||
hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
|
||||
|
||||
[project.entry-points."hexdoc.BrainsweepeeIngredient"]
|
||||
hexdoc-hexcasting = "hexdoc.hexcasting.hex_recipes"
|
||||
|
||||
|
||||
[tool.hatch.build]
|
||||
packages = ["src/hexdoc"]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
{{ category.description|hexdoc_block }}
|
||||
{% endcall %}
|
||||
|
||||
{% for entry in category.entries if entry.id not in props.blacklist +%}
|
||||
{% for entry in category.entries if entry.id not in props.entry_id_blacklist +%}
|
||||
{% include "entry.html.jinja" %}
|
||||
{% endfor +%}
|
||||
</section>
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
from pathlib import Path
|
||||
import logging
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from hexdoc.patchouli import AnyBookContext, Book, BookContext
|
||||
from hexdoc.utils import AnyContext, Properties, ResourceLocation
|
||||
from hexdoc.utils.properties import PatternStubProps
|
||||
|
||||
from .pattern import Direction, PatternInfo
|
||||
|
||||
|
@ -25,6 +26,7 @@ class HexBookType(
|
|||
# load patterns
|
||||
patterns = dict[ResourceLocation, PatternInfo]()
|
||||
signatures = dict[str, PatternInfo]() # just for duplicate checking
|
||||
|
||||
for stub in props.pattern_stubs:
|
||||
# for each stub, load all the patterns in the file
|
||||
for pattern in cls.load_patterns(stub, props):
|
||||
|
@ -35,9 +37,12 @@ class HexBookType(
|
|||
raise ValueError(
|
||||
f"Duplicate pattern {pattern.id}\n{pattern}\n{duplicate}"
|
||||
)
|
||||
|
||||
patterns[pattern.id] = pattern
|
||||
signatures[pattern.signature] = pattern
|
||||
|
||||
logging.getLogger(__name__).debug(f"Patterns: {patterns.keys()}")
|
||||
|
||||
# build new context
|
||||
return data, {
|
||||
**context,
|
||||
|
@ -45,16 +50,16 @@ class HexBookType(
|
|||
}
|
||||
|
||||
@classmethod
|
||||
def load_patterns(cls, path: Path, props: Properties):
|
||||
def load_patterns(cls, stub: PatternStubProps, props: Properties):
|
||||
# TODO: add Gradle task to generate json with this data. this is dumb and fragile.
|
||||
stub_text = path.read_text("utf-8")
|
||||
for match in props.pattern_regex.finditer(stub_text):
|
||||
signature, startdir, name, is_per_world = match.groups()
|
||||
stub_text = stub.path.read_text("utf-8")
|
||||
for match in stub.regex.finditer(stub_text):
|
||||
groups = match.groupdict()
|
||||
yield PatternInfo(
|
||||
startdir=Direction[startdir],
|
||||
signature=signature,
|
||||
is_per_world=bool(is_per_world),
|
||||
id=ResourceLocation(props.modid, name),
|
||||
startdir=Direction[groups["startdir"]],
|
||||
signature=groups["signature"],
|
||||
# is_per_world=bool(is_per_world), # FIXME: idfk how to do this now
|
||||
id=ResourceLocation(props.modid, groups["name"]),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -6,21 +6,39 @@ from hexdoc.minecraft.recipe import (
|
|||
MinecraftItemIdIngredient,
|
||||
MinecraftItemTagIngredient,
|
||||
)
|
||||
from hexdoc.utils import HexDocModel, ResourceLocation
|
||||
from hexdoc.utils import HexDocModel, ResourceLocation, TypeTaggedUnion
|
||||
from hexdoc.utils.model import AnyContext
|
||||
|
||||
from .hex_book import HexContext
|
||||
|
||||
# ingredients
|
||||
|
||||
|
||||
class VillagerIngredient(HexDocModel[HexContext]): # lol, lmao
|
||||
class BrainsweepeeIngredient(
|
||||
TypeTaggedUnion[AnyContext],
|
||||
group="hexdoc.BrainsweepeeIngredient",
|
||||
type=None,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
# lol, lmao
|
||||
class VillagerIngredient(BrainsweepeeIngredient[HexContext], type="villager"):
|
||||
minLevel: int
|
||||
profession: ResourceLocation | None = None
|
||||
biome: ResourceLocation | None = None
|
||||
|
||||
|
||||
class EntityTypeIngredient(BrainsweepeeIngredient[HexContext], type="entity_type"):
|
||||
entityType: ResourceLocation
|
||||
|
||||
|
||||
class EntityTagIngredient(BrainsweepeeIngredient[HexContext], type="entity_tag"):
|
||||
tag: ResourceLocation
|
||||
|
||||
|
||||
class BlockStateIngredient(HexDocModel[HexContext]):
|
||||
# TODO: StateIngredient should also be a TypeTaggedUnion, probably
|
||||
# TODO: tagged union
|
||||
type: Literal["block"]
|
||||
block: ResourceLocation
|
||||
|
||||
|
@ -53,5 +71,6 @@ class BlockState(HexDocModel[HexContext]):
|
|||
|
||||
class BrainsweepRecipe(Recipe[HexContext], type="hexcasting:brainsweep"):
|
||||
blockIn: BlockStateIngredient
|
||||
villagerIn: VillagerIngredient
|
||||
cost: int
|
||||
entityIn: BrainsweepeeIngredient[HexContext]
|
||||
result: BlockState
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# because Tap.add_argument isn't typed, for some reason
|
||||
# pyright: reportUnknownMemberType=false
|
||||
|
||||
import logging
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Self, Sequence
|
||||
|
||||
from jinja2 import (
|
||||
ChoiceLoader,
|
||||
|
@ -11,35 +12,53 @@ from jinja2 import (
|
|||
StrictUndefined,
|
||||
)
|
||||
|
||||
# from jinja2.sandbox import SandboxedEnvironment
|
||||
from tap import Tap
|
||||
|
||||
from hexdoc.hexcasting import HexBook
|
||||
from hexdoc.utils import Properties
|
||||
|
||||
from .jinja_extensions import IncludeRawExtension, hexdoc_block, hexdoc_wrap
|
||||
|
||||
# TODO: enable
|
||||
# from jinja2.sandbox import SandboxedEnvironment
|
||||
|
||||
|
||||
def strip_empty_lines(text: str) -> str:
|
||||
return "\n".join(s for s in text.splitlines() if s.strip())
|
||||
|
||||
|
||||
# CLI arguments
|
||||
class Args(Tap):
|
||||
@dataclass
|
||||
class Args:
|
||||
"""example: main.py properties.toml -o out.html"""
|
||||
|
||||
properties_file: Path
|
||||
output_file: Path | None
|
||||
verbose: bool
|
||||
|
||||
def configure(self):
|
||||
self.add_argument("properties_file")
|
||||
self.add_argument("-o", "--output_file", required=False)
|
||||
@classmethod
|
||||
def parse_args(cls, args: Sequence[str] | None = None) -> Self:
|
||||
parser = ArgumentParser()
|
||||
|
||||
parser.add_argument("properties_file", type=Path)
|
||||
parser.add_argument("--output_file", "-o", type=Path)
|
||||
parser.add_argument("--verbose", "-v", action="store_true")
|
||||
|
||||
return cls(**vars(parser.parse_args(args)))
|
||||
|
||||
|
||||
def main(args: Args | None = None) -> None:
|
||||
# allow passing Args for test cases, but parse by default
|
||||
if args is None:
|
||||
args = Args().parse_args()
|
||||
args = Args.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
style="{",
|
||||
format="[{levelname}][{name}] {message}",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
logger.debug("Log level set to DEBUG")
|
||||
|
||||
# load the properties and book
|
||||
props = Properties.load(args.properties_file)
|
||||
|
@ -60,7 +79,7 @@ def main(args: Args | None = None) -> None:
|
|||
autoescape=True,
|
||||
extensions=[IncludeRawExtension],
|
||||
)
|
||||
env.filters |= dict( # for some reason, pylance doesn't like the {} here
|
||||
env.filters |= dict( # pyright: ignore[reportUnknownMemberType]
|
||||
hexdoc_block=hexdoc_block,
|
||||
hexdoc_wrap=hexdoc_wrap,
|
||||
)
|
||||
|
|
|
@ -16,7 +16,7 @@ from hexdoc.utils import (
|
|||
Properties,
|
||||
ResourceLocation,
|
||||
)
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_json_dict
|
||||
from hexdoc.utils.deserialize import isinstance_or_raise, load_and_flatten_json_dict
|
||||
|
||||
|
||||
class I18nContext(TypedDict):
|
||||
|
@ -53,7 +53,6 @@ class LocalizedStr(HexDocModel[I18nContext]):
|
|||
if not isinstance(value, str):
|
||||
return handler(value)
|
||||
|
||||
# this is nasty, but we need to use cast to get type checking for context
|
||||
context = cast(I18nContext, info.context)
|
||||
return cls._localize(context["i18n"], value)
|
||||
|
||||
|
@ -113,24 +112,16 @@ class I18n:
|
|||
# TODO: load ALL of the i18n files, return dict[str, _Lookup] | None
|
||||
# or maybe dict[(str, str), LocalizedStr]
|
||||
# we could also use that to ensure all i18n files have the same set of keys
|
||||
lang_dir = props.resources_dir / "assets" / props.modid / "lang"
|
||||
path = lang_dir / props.i18n.filename
|
||||
raw_lookup = load_json_dict(path) | (props.i18n.extra or {})
|
||||
path = props.assets_dir / "lang" / props.i18n.filename
|
||||
raw_lookup = load_and_flatten_json_dict(path) | props.i18n.extra
|
||||
|
||||
# validate and insert
|
||||
self.lookup = {}
|
||||
for key, raw_value in raw_lookup.items():
|
||||
assert isinstance_or_raise(raw_value, str)
|
||||
self.lookup[key] = LocalizedStr(
|
||||
key=key,
|
||||
value=raw_value.replace("%%", "%"),
|
||||
)
|
||||
self.lookup = {
|
||||
key: LocalizedStr(key=key, value=value.replace("%%", "%"))
|
||||
for key, value in raw_lookup.items()
|
||||
}
|
||||
|
||||
def localize(
|
||||
self,
|
||||
*keys: str,
|
||||
default: str | None = None,
|
||||
) -> LocalizedStr:
|
||||
def localize(self, *keys: str, default: str | None = None) -> LocalizedStr:
|
||||
"""Looks up the given string in the lang table if i18n is enabled. Otherwise,
|
||||
returns the original key.
|
||||
|
||||
|
@ -170,8 +161,8 @@ class I18n:
|
|||
# prefer the book-specific translation if it exists
|
||||
# TODO: should this use op_id.namespace anywhere?
|
||||
return self.localize(
|
||||
f"hexcasting.spell.book.{op_id}",
|
||||
f"hexcasting.spell.{op_id}",
|
||||
f"hexcasting.action.book.{op_id}",
|
||||
f"hexcasting.action.{op_id}",
|
||||
)
|
||||
|
||||
def localize_item(self, item: ItemStack | str) -> LocalizedItem:
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import logging
|
||||
from typing import Any, Self, cast
|
||||
|
||||
from pydantic import ValidationInfo, model_validator
|
||||
|
@ -10,6 +11,7 @@ class Recipe(TypeTaggedUnion[AnyPropsContext], group="hexdoc.Recipe", type=None)
|
|||
id: ResourceLocation
|
||||
|
||||
group: str | None = None
|
||||
category: str | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
def _pre_root(
|
||||
|
@ -30,15 +32,17 @@ class Recipe(TypeTaggedUnion[AnyPropsContext], group="hexdoc.Recipe", type=None)
|
|||
|
||||
# load the recipe
|
||||
context = cast(AnyPropsContext, info.context)
|
||||
for recipe_dir in context["props"].recipe_dirs:
|
||||
# TODO: should this use id.namespace somewhere?
|
||||
path = recipe_dir / f"{id.path}.json"
|
||||
if recipe_dir == context["props"].default_recipe_dir:
|
||||
# only load from one file
|
||||
values = load_json_dict(path) | {"id": id}
|
||||
elif not path.exists():
|
||||
# this is to ensure the recipe at least exists on all platforms
|
||||
# because we've had issues with that before (eg. Hexal's Mote Nexus)
|
||||
raise ValueError(f"Recipe {id} missing from path {path}")
|
||||
|
||||
return values
|
||||
# TODO: this is ugly and not super great for eg. hexbound
|
||||
forge_path = context["props"].forge.recipes / f"{id.path}.json"
|
||||
fabric_path = context["props"].fabric.recipes / f"{id.path}.json"
|
||||
|
||||
# this is to ensure the recipe at least exists on all platforms
|
||||
# because we've had issues with that before (eg. Hexal's Mote Nexus)
|
||||
if not forge_path.exists():
|
||||
raise ValueError(f"Recipe {id} missing from path {forge_path}")
|
||||
|
||||
logging.getLogger(__name__).debug(
|
||||
f"Load {cls}\n id: {id}\n path: {fabric_path}"
|
||||
)
|
||||
return load_json_dict(fabric_path) | {"id": id}
|
||||
|
|
|
@ -16,9 +16,10 @@ class CraftingShapedRecipe(
|
|||
Recipe[Any],
|
||||
type="minecraft:crafting_shaped",
|
||||
):
|
||||
pattern: list[str]
|
||||
key: dict[str, ItemIngredientOrList[Any]]
|
||||
pattern: list[str]
|
||||
result: ItemResult
|
||||
show_notification: bool
|
||||
|
||||
|
||||
class CraftingShapelessRecipe(
|
||||
|
|
|
@ -40,6 +40,7 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
# required
|
||||
name: LocalizedStr
|
||||
landing_text: FormatTree
|
||||
use_resource_pack: Literal[True]
|
||||
|
||||
# optional
|
||||
book_texture: ResourceLocation = ResLoc("patchouli", "textures/gui/book_brown.png")
|
||||
|
@ -60,7 +61,7 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
show_progress: bool = True
|
||||
version: str | int = 0
|
||||
subtitle: LocalizedStr | None = None
|
||||
creative_tab: str = "misc" # TODO: this was changed in 1.19.3+, and again in 1.20
|
||||
creative_tab: str | None = None
|
||||
advancements_tab: str | None = None
|
||||
dont_generate_book: bool = False
|
||||
custom_book_item: ItemStack | None = None
|
||||
|
@ -70,14 +71,11 @@ class Book(Generic[AnyContext, AnyBookContext], HexDocModel[AnyBookContext]):
|
|||
macros: dict[str, str] = Field(default_factory=dict)
|
||||
pause_game: bool = False
|
||||
text_overflow_mode: Literal["overflow", "resize", "truncate"] | None = None
|
||||
extend: ResourceLocation | None = None
|
||||
allow_extensions: bool = True
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, props: Properties) -> tuple[dict[str, Any], BookContext]:
|
||||
# read the raw dict from the json file
|
||||
path = props.book_dir / "book.json"
|
||||
data = load_json_dict(path)
|
||||
data = load_json_dict(props.book_path)
|
||||
|
||||
# set up the deserialization context object
|
||||
assert isinstance_or_raise(data["i18n"], bool)
|
||||
|
|
|
@ -52,5 +52,8 @@ class Entry(BookFileModel[BookContext, BookContext], Sortable):
|
|||
if not context or self.advancement is None:
|
||||
return self
|
||||
|
||||
self.is_spoiler = self.advancement in context["props"].spoilers
|
||||
self.is_spoiler = any(
|
||||
self.advancement.match(spoiler)
|
||||
for spoiler in context["props"].spoilered_advancements
|
||||
)
|
||||
return self
|
||||
|
|
|
@ -14,7 +14,7 @@ from pydantic.functional_validators import ModelWrapValidatorHandler
|
|||
|
||||
from hexdoc.minecraft import LocalizedStr
|
||||
from hexdoc.minecraft.i18n import I18nContext
|
||||
from hexdoc.utils import DEFAULT_CONFIG, HexDocModel, PropsContext
|
||||
from hexdoc.utils import DEFAULT_CONFIG, HexDocModel, Properties, PropsContext
|
||||
from hexdoc.utils.types import TryGetEnum
|
||||
|
||||
from .html import HTMLElement, HTMLStream
|
||||
|
@ -61,11 +61,6 @@ _COLORS = {
|
|||
"f": "fff",
|
||||
}
|
||||
|
||||
_KEYS = {
|
||||
"use": "Right Click",
|
||||
"sneak": "Left Shift",
|
||||
}
|
||||
|
||||
|
||||
# Higgledy piggledy
|
||||
# Old fuck Alwinfy said,
|
||||
|
@ -116,7 +111,7 @@ class Style(ABC, HexDocModel[Any], frozen=True):
|
|||
type: CommandStyleType | FunctionStyleType | SpecialStyleType
|
||||
|
||||
@staticmethod
|
||||
def parse(style_str: str, is_0_black: bool) -> Style | _CloseTag | str:
|
||||
def parse(style_str: str, props: Properties) -> Style | _CloseTag | str:
|
||||
# direct text replacements
|
||||
if style_str in _REPLACEMENTS:
|
||||
return _REPLACEMENTS[style_str]
|
||||
|
@ -130,7 +125,7 @@ class Style(ABC, HexDocModel[Any], frozen=True):
|
|||
return CommandStyle(type=style_type)
|
||||
|
||||
# reset color, but only if 0 is considered reset instead of black
|
||||
if not is_0_black and style_str == "0":
|
||||
if not props.is_0_black and style_str == "0":
|
||||
return _CloseTag(type=SpecialStyleType.color)
|
||||
|
||||
# preset colors
|
||||
|
@ -146,9 +141,8 @@ class Style(ABC, HexDocModel[Any], frozen=True):
|
|||
name, value = style_str.split(":", 1)
|
||||
|
||||
# keys
|
||||
if name == "k":
|
||||
if value in _KEYS:
|
||||
return _KEYS[value]
|
||||
if name == "k" and (key := props.i18n.keys.get(value)):
|
||||
return key
|
||||
|
||||
# all the other functions
|
||||
if style_type := FunctionStyleType.get(name):
|
||||
|
@ -265,7 +259,7 @@ class FormatTree:
|
|||
children: list[FormatTree | str] # this can't be Self, it breaks Pydantic
|
||||
|
||||
@classmethod
|
||||
def format(cls, string: str, macros: dict[str, str], is_0_black: bool) -> Self:
|
||||
def format(cls, string: str, macros: dict[str, str], props: Properties) -> Self:
|
||||
# resolve macros
|
||||
# TODO: use ahocorasick? this feels inefficient
|
||||
old_string = None
|
||||
|
@ -286,7 +280,7 @@ class FormatTree:
|
|||
text_since_prev_style.append(leading_text)
|
||||
last_end = match.end()
|
||||
|
||||
match Style.parse(match[1], is_0_black):
|
||||
match Style.parse(match[1], props):
|
||||
case str(replacement):
|
||||
# str means "use this instead of the original value"
|
||||
text_since_prev_style.append(replacement)
|
||||
|
@ -347,4 +341,4 @@ class FormatTree:
|
|||
|
||||
if isinstance(value, str):
|
||||
value = context["i18n"].localize(value)
|
||||
return cls.format(value.value, context["macros"], context["props"].is_0_black)
|
||||
return cls.format(value.value, context["macros"], context["props"])
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
__all__ = [
|
||||
"HexDocModel",
|
||||
"FrozenHexDocModel",
|
||||
"HexDocFileModel",
|
||||
"InternallyTaggedUnion",
|
||||
"Color",
|
||||
|
@ -19,13 +18,7 @@ __all__ = [
|
|||
"TypeTaggedUnion",
|
||||
]
|
||||
|
||||
from .model import (
|
||||
DEFAULT_CONFIG,
|
||||
AnyContext,
|
||||
FrozenHexDocModel,
|
||||
HexDocFileModel,
|
||||
HexDocModel,
|
||||
)
|
||||
from .model import DEFAULT_CONFIG, AnyContext, HexDocFileModel, HexDocModel
|
||||
from .properties import AnyPropsContext, Properties, PropsContext
|
||||
from .resource import Entity, ItemStack, ResLoc, ResourceLocation
|
||||
from .tagged_union import (
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeGuard, TypeVar, get_origin
|
||||
|
||||
import pyjson5
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T_cov = TypeVar("_T_cov", covariant=True)
|
||||
|
||||
_DEFAULT_MESSAGE = "Expected any of {expected}, got {actual}: {value}"
|
||||
|
||||
|
@ -39,6 +42,50 @@ JSONValue = JSONDict | list["JSONValue"] | str | int | float | bool | None
|
|||
|
||||
|
||||
def load_json_dict(path: Path) -> JSONDict:
|
||||
data: JSONValue = json.loads(path.read_text("utf-8"))
|
||||
data = pyjson5.decode(path.read_text("utf-8"))
|
||||
assert isinstance_or_raise(data, dict)
|
||||
return data
|
||||
|
||||
|
||||
# implement pkpcpbp's flattening in python
|
||||
# https://github.com/gamma-delta/PKPCPBP/blob/786194a590f/src/main/java/at/petrak/pkpcpbp/filters/JsonUtil.java
|
||||
def load_and_flatten_json_dict(path: Path) -> dict[str, str]:
|
||||
# load file, replace `\<LF> foobar` with `\<LF>foobar`
|
||||
json_str = re.sub(r"\\\n\s*", "\\\n", path.read_text("utf-8"))
|
||||
|
||||
# decode json5 and flatten
|
||||
data = pyjson5.decode(json_str)
|
||||
assert isinstance_or_raise(data, JSONDict)
|
||||
|
||||
return _flatten_inner(data, "")
|
||||
|
||||
|
||||
def _flatten_inner(obj: JSONDict, prefix: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
|
||||
for key_stub, value in obj.items():
|
||||
if not prefix:
|
||||
key = key_stub
|
||||
elif not key_stub:
|
||||
key = prefix
|
||||
elif prefix[-1] in ":_-/":
|
||||
key = prefix + key_stub
|
||||
else:
|
||||
key = f"{prefix}.{key_stub}"
|
||||
|
||||
match value:
|
||||
case dict():
|
||||
_update_disallow_duplicates(out, _flatten_inner(value, key))
|
||||
case str():
|
||||
_update_disallow_duplicates(out, {key: value})
|
||||
case _:
|
||||
raise TypeError(value)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _update_disallow_duplicates(base: dict[str, _T_cov], new: dict[str, _T_cov]):
|
||||
for key, value in new.items():
|
||||
if key in base:
|
||||
raise ValueError(f"Duplicate key {key}\nold=`{base[key]}`\nnew=`{value}`")
|
||||
base[key] = value
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar, dataclass_transform
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from pydantic.config import ConfigDict
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .deserialize import load_json_dict
|
||||
|
@ -47,14 +49,14 @@ class HexDocModel(Generic[AnyContext], BaseModel):
|
|||
...
|
||||
|
||||
|
||||
@dataclass_transform(frozen_default=True)
|
||||
class FrozenHexDocModel(Generic[AnyContext], HexDocModel[AnyContext]):
|
||||
model_config = DEFAULT_CONFIG | {"frozen": True}
|
||||
|
||||
|
||||
@dataclass_transform()
|
||||
class HexDocFileModel(HexDocModel[AnyContext]):
|
||||
@classmethod
|
||||
def load(cls, path: Path, context: AnyContext) -> Self:
|
||||
logging.getLogger(__name__).debug(f"Load {cls}\n path: {path}")
|
||||
data = load_json_dict(path) | {"__path": path}
|
||||
return cls.model_validate(data, context=context)
|
||||
try:
|
||||
return cls.model_validate(data, context=context)
|
||||
except Exception as e:
|
||||
e.add_note(f"File: {path}")
|
||||
raise
|
||||
|
|
|
@ -4,18 +4,12 @@ import re
|
|||
from pathlib import Path
|
||||
from typing import Annotated, Any, Self, TypeVar
|
||||
|
||||
from pydantic import (
|
||||
AfterValidator,
|
||||
Field,
|
||||
FieldValidationInfo,
|
||||
HttpUrl,
|
||||
field_validator,
|
||||
)
|
||||
from pydantic import AfterValidator, Field, HttpUrl
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from .model import HexDocModel
|
||||
from .resource import ResourceLocation
|
||||
from .toml_placeholders import load_toml
|
||||
from .toml_placeholders import load_toml_with_placeholders
|
||||
|
||||
NoTrailingSlashHttpUrl = Annotated[
|
||||
str,
|
||||
|
@ -24,38 +18,43 @@ NoTrailingSlashHttpUrl = Annotated[
|
|||
]
|
||||
|
||||
|
||||
class PlatformProps(HexDocModel[Any]):
|
||||
resources: Path
|
||||
generated: Path
|
||||
class PatternStubProps(HexDocModel[Any], extra="ignore"):
|
||||
path: Path
|
||||
regex: re.Pattern[str]
|
||||
|
||||
|
||||
class XplatProps(HexDocModel[Any], extra="ignore"):
|
||||
src: Path
|
||||
package: Path
|
||||
pattern_stubs: list[Path] | None = None
|
||||
pattern_stubs: list[PatternStubProps] | None = None
|
||||
resources: Path
|
||||
|
||||
|
||||
class I18nProps(HexDocModel[Any]):
|
||||
class PlatformProps(XplatProps):
|
||||
recipes: Path
|
||||
tags: Path
|
||||
|
||||
|
||||
class I18nProps(HexDocModel[Any], extra="ignore"):
|
||||
default_lang: str
|
||||
filename: str
|
||||
extra: dict[str, str] | None = None
|
||||
extra: dict[str, str] = Field(default_factory=dict)
|
||||
keys: dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class Properties(HexDocModel[Any]):
|
||||
class Properties(HexDocModel[Any], extra="ignore"):
|
||||
modid: str
|
||||
book_name: str
|
||||
is_0_black: bool
|
||||
"""If true, the style `$(0)` changes the text color to black; otherwise it resets
|
||||
the text color to the default."""
|
||||
|
||||
recipe_dirs: list[Path]
|
||||
default_recipe_dir_index_: int = Field(alias="default_recipe_dir")
|
||||
|
||||
pattern_regex: re.Pattern[str]
|
||||
|
||||
template: str
|
||||
template_dirs: list[Path]
|
||||
template_packages: list[tuple[str, Path]]
|
||||
|
||||
spoilers: set[ResourceLocation]
|
||||
blacklist: set[ResourceLocation]
|
||||
spoilered_advancements: set[ResourceLocation]
|
||||
entry_id_blacklist: set[ResourceLocation]
|
||||
|
||||
template_args: dict[str, Any]
|
||||
|
||||
|
@ -64,13 +63,13 @@ class Properties(HexDocModel[Any]):
|
|||
|
||||
i18n: I18nProps
|
||||
|
||||
common: PlatformProps
|
||||
common: XplatProps
|
||||
fabric: PlatformProps # TODO: some way to make these optional for addons
|
||||
forge: PlatformProps
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> Self:
|
||||
return cls.model_validate(load_toml(path))
|
||||
return cls.model_validate(load_toml_with_placeholders(path))
|
||||
|
||||
@property
|
||||
def resources_dir(self):
|
||||
|
@ -81,30 +80,37 @@ class Properties(HexDocModel[Any]):
|
|||
return self.i18n.default_lang
|
||||
|
||||
@property
|
||||
def book_dir(self) -> Path:
|
||||
"""eg. `resources/data/hexcasting/patchouli_books/thehexbook`"""
|
||||
def book_path(self) -> Path:
|
||||
"""eg. `resources/data/hexcasting/patchouli_books/thehexbook/book.json`"""
|
||||
return (
|
||||
self.resources_dir
|
||||
/ "data"
|
||||
/ self.modid
|
||||
/ "patchouli_books"
|
||||
/ self.book_name
|
||||
/ "book.json"
|
||||
)
|
||||
|
||||
@property
|
||||
def assets_dir(self) -> Path:
|
||||
"""eg. `resources/assets/hexcasting`"""
|
||||
return self.resources_dir / "assets" / self.modid
|
||||
|
||||
@property
|
||||
def book_assets_dir(self) -> Path:
|
||||
"""eg. `resources/assets/hexcasting/patchouli_books/thehexbook`"""
|
||||
return self.assets_dir / "patchouli_books" / self.book_name
|
||||
|
||||
@property
|
||||
def categories_dir(self) -> Path:
|
||||
return self.book_dir / self.lang / "categories"
|
||||
return self.book_assets_dir / self.lang / "categories"
|
||||
|
||||
@property
|
||||
def entries_dir(self) -> Path:
|
||||
return self.book_dir / self.lang / "entries"
|
||||
return self.book_assets_dir / self.lang / "entries"
|
||||
|
||||
@property
|
||||
def default_recipe_dir(self) -> Path:
|
||||
return self.recipe_dirs[self.default_recipe_dir_index_]
|
||||
|
||||
@property
|
||||
def platforms(self) -> list[PlatformProps]:
|
||||
def platforms(self) -> list[XplatProps]:
|
||||
platforms = [self.common]
|
||||
if self.fabric:
|
||||
platforms.append(self.fabric)
|
||||
|
@ -113,7 +119,7 @@ class Properties(HexDocModel[Any]):
|
|||
return platforms
|
||||
|
||||
@property
|
||||
def pattern_stubs(self) -> list[Path]:
|
||||
def pattern_stubs(self):
|
||||
return [
|
||||
stub
|
||||
for platform in self.platforms
|
||||
|
@ -125,15 +131,6 @@ class Properties(HexDocModel[Any]):
|
|||
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"])
|
||||
if value >= num_dirs:
|
||||
raise ValueError(
|
||||
f"default_recipe_dir must be a valid index of recipe_dirs (expected <={num_dirs - 1}, got {value})"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class PropsContext(TypedDict):
|
||||
props: Properties
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
# basically, just leave it here
|
||||
|
||||
import re
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Self
|
||||
|
||||
|
@ -16,7 +17,7 @@ from .model import DEFAULT_CONFIG
|
|||
|
||||
|
||||
def _make_regex(count: bool = False, nbt: bool = False) -> re.Pattern[str]:
|
||||
pattern = r"(?:(?P<namespace>[0-9a-z_\-.]+):)?(?P<path>[0-9a-z_\-./]+)"
|
||||
pattern = r"(?:(?P<namespace>[0-9a-z_\-.]+):)?(?P<path>[0-9a-z_\-./*]+)"
|
||||
if count:
|
||||
pattern += r"(?:#(?P<count>[0-9]+))?"
|
||||
if nbt:
|
||||
|
@ -58,7 +59,11 @@ class BaseResourceLocation:
|
|||
def _default_namespace(cls, value: str | None) -> str:
|
||||
if value is None:
|
||||
return "minecraft"
|
||||
return value
|
||||
return value.lower()
|
||||
|
||||
@field_validator("path")
|
||||
def _lower_path(cls, value: str):
|
||||
return value.lower()
|
||||
|
||||
@model_serializer
|
||||
def _ser_model(self) -> str:
|
||||
|
@ -85,6 +90,9 @@ class ResourceLocation(BaseResourceLocation, regex=_make_regex()):
|
|||
def href(self) -> str:
|
||||
return f"#{self.path}"
|
||||
|
||||
def match(self, pattern: Self) -> bool:
|
||||
return fnmatch(str(self), str(pattern))
|
||||
|
||||
|
||||
# pure unadulterated laziness
|
||||
ResLoc = ResourceLocation
|
||||
|
@ -101,7 +109,8 @@ class ItemStack(BaseResourceLocation, regex=_make_regex(count=True, nbt=True)):
|
|||
nbt: str | None = None
|
||||
|
||||
def i18n_key(self, root: str = "item") -> str:
|
||||
return f"{root}.{self.namespace}.{self.path}"
|
||||
# TODO: is this how i18n works?????
|
||||
return f"{root}.{self.namespace}.{self.path.replace('/', '.')}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
s = super().__repr__()
|
||||
|
|
|
@ -114,7 +114,7 @@ def _fill_placeholders(
|
|||
_handle_child(data, stack, expanded, key, child, table.__setitem__)
|
||||
|
||||
|
||||
def load_toml(path: Path) -> TOMLDict:
|
||||
def load_toml_with_placeholders(path: Path) -> TOMLDict:
|
||||
data = tomllib.loads(path.read_text("utf-8"))
|
||||
fill_placeholders(data)
|
||||
return data
|
||||
|
|
|
@ -7,14 +7,11 @@ from syrupy.assertion import SnapshotAssertion
|
|||
|
||||
from hexdoc.hexdoc import Args, main
|
||||
|
||||
_RUN = ["hexdoc"]
|
||||
_ARGV = ["properties.toml", "-o"]
|
||||
|
||||
|
||||
def test_file(tmp_path: Path, snapshot: SnapshotAssertion):
|
||||
# generate output docs html file and assert it hasn't changed vs. the snapshot
|
||||
out_path = tmp_path / "out.html"
|
||||
main(Args().parse_args(_ARGV + [out_path.as_posix()]))
|
||||
main(Args.parse_args(["properties.toml", "-o", out_path.as_posix()]))
|
||||
assert out_path.read_text("utf-8") == snapshot
|
||||
|
||||
|
||||
|
@ -22,7 +19,7 @@ def test_cmd(tmp_path: Path, snapshot: SnapshotAssertion):
|
|||
# as above, but running the command we actually want to be using
|
||||
out_path = tmp_path / "out.html"
|
||||
subprocess.run(
|
||||
_RUN + _ARGV + [out_path.as_posix()],
|
||||
["hexdoc", "properties.toml", "-o", out_path.as_posix()],
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
|
@ -30,5 +27,5 @@ def test_cmd(tmp_path: Path, snapshot: SnapshotAssertion):
|
|||
|
||||
|
||||
def test_stdout(capsys: pytest.CaptureFixture[str], snapshot: SnapshotAssertion):
|
||||
main(Args().parse_args(["properties.toml"]))
|
||||
main(Args.parse_args(["properties.toml"]))
|
||||
assert capsys.readouterr() == snapshot
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
# pyright: reportPrivateUsage=false
|
||||
from argparse import Namespace
|
||||
from typing import cast
|
||||
|
||||
from hexdoc.patchouli.text import DEFAULT_MACROS, FormatTree
|
||||
from hexdoc.patchouli.text.formatting import (
|
||||
CommandStyle,
|
||||
|
@ -7,14 +10,16 @@ from hexdoc.patchouli.text.formatting import (
|
|||
ParagraphStyle,
|
||||
SpecialStyleType,
|
||||
)
|
||||
from hexdoc.utils.properties import Properties
|
||||
|
||||
|
||||
def test_format_string():
|
||||
# arrange
|
||||
test_str = "Write the given iota to my $(l:patterns/readwrite#hexcasting:write/local)$(#490)local$().$(br)The $(l:patterns/readwrite#hexcasting:write/local)$(#490)local$() is a lot like a $(l:items/focus)$(#b0b)Focus$(). It's cleared when I stop casting a Hex, starts with $(l:casting/influences)$(#490)Null$() in it, and is preserved between casts of $(l:patterns/meta#hexcasting:for_each)$(#fc77be)Thoth's Gambit$(). "
|
||||
mock_props = Namespace(is_0_black=False, i18n=Namespace(keys={}))
|
||||
|
||||
# act
|
||||
tree = FormatTree.format(test_str, DEFAULT_MACROS, is_0_black=False)
|
||||
tree = FormatTree.format(test_str, DEFAULT_MACROS, cast(Properties, mock_props))
|
||||
|
||||
# assert
|
||||
# TODO: possibly make this less lazy
|
||||
|
|
Loading…
Reference in a new issue