We do a bit of renaming

This commit is contained in:
object-Object 2023-07-10 00:56:40 -04:00
parent aeb737a4eb
commit 92f7770dc6
7 changed files with 155 additions and 159 deletions

View file

@ -3,6 +3,7 @@
modid = "hexcasting" modid = "hexcasting"
book_name = "thehexbook" book_name = "thehexbook"
template = "template.html" template = "template.html"
is_0_black = false
recipe_dirs = [ recipe_dirs = [
"{fabric.generated}/data/{modid}/recipes", "{fabric.generated}/data/{modid}/recipes",
"{forge.generated}/data/{modid}/recipes", "{forge.generated}/data/{modid}/recipes",
@ -13,6 +14,9 @@ default_recipe_dir = 0
# more on that later # more on that later
pattern_regex = {_Raw='HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?'} pattern_regex = {_Raw='HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?'}
[base_asset_urls]
hexcasting = "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources"
[i18n] [i18n]
lang = "en_us" lang = "en_us"
filename = "{lang}.json" filename = "{lang}.json"
@ -23,7 +27,6 @@ filename = "{lang}.json"
# platforms # platforms
# fabric and forge are optional
[common] [common]
src = "../Common/src" src = "../Common/src"

View file

@ -1,13 +1,25 @@
import re import re
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Annotated, Any, Self
from pydantic import Field, model_validator from pydantic import (
AfterValidator,
Field,
FieldValidationInfo,
HttpUrl,
field_validator,
)
from common.model import HexDocModel from common.model import HexDocModel
from common.toml_placeholders import load_toml from common.toml_placeholders import load_toml
from hexcasting.pattern import PatternStubFile from hexcasting.pattern import PatternStubFile
NoTrailingSlashHttpUrl = Annotated[
str,
HttpUrl,
AfterValidator(lambda u: str(u).rstrip("/")),
]
class PlatformProps(HexDocModel[Any]): class PlatformProps(HexDocModel[Any]):
resources: Path resources: Path
@ -27,15 +39,17 @@ class Properties(HexDocModel[Any]):
modid: str modid: str
book_name: str book_name: str
template: Path template: Path
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] recipe_dirs: list[Path]
default_recipe_dir_index_: int = Field(alias="default_recipe_dir") default_recipe_dir_index_: int = Field(alias="default_recipe_dir")
pattern_regex: re.Pattern[str] pattern_regex: re.Pattern[str]
is_0_black: bool = False base_asset_urls: dict[str, NoTrailingSlashHttpUrl]
"""If true, the style `$(0)` changes the text color to black; otherwise it resets """Mapping from modid to the url of that mod's `resources` directory on GitHub."""
the text color to the default."""
i18n: I18nProps i18n: I18nProps
@ -100,10 +114,11 @@ class Properties(HexDocModel[Any]):
for stub in platform.pattern_stubs for stub in platform.pattern_stubs
] ]
@model_validator(mode="after") @field_validator("default_recipe_dir_index_")
def _check_default_recipe_dir(self): def _check_default_recipe_dir(cls, value: int, info: FieldValidationInfo) -> int:
if self.default_recipe_dir_index_ >= len(self.recipe_dirs): num_dirs = len(info.data["recipe_dirs"])
if value >= num_dirs:
raise ValueError( raise ValueError(
f"default_recipe_dir must be a valid index of recipe_dirs (expected <={len(self.recipe_dirs)}, got {self.default_recipe_dir_index_})" f"default_recipe_dir must be a valid index of recipe_dirs (expected <={num_dirs - 1}, got {value})"
) )
return self return value

View file

@ -11,7 +11,7 @@ from patchouli.page import PageWithTitle
from .hex_book import AnyHexContext, HexContext from .hex_book import AnyHexContext, HexContext
# TODO: make anchor required # TODO: make anchor required (breaks because of Greater Sentinel)
class PageWithPattern(PageWithTitle[AnyHexContext], ABC, type=None): class PageWithPattern(PageWithTitle[AnyHexContext], ABC, type=None):
title_: None = Field(default=None, include=True) title_: None = Field(default=None, include=True)

View file

@ -2,6 +2,7 @@
import io import io
from typing import Any from typing import Any
from common.properties import Properties
from hexcasting import HexBook from hexcasting import HexBook
from hexcasting.abstract_hex_pages import PageWithPattern from hexcasting.abstract_hex_pages import PageWithPattern
from hexcasting.hex_pages import BrainsweepPage, CraftingMultiPage, LookupPatternPage from hexcasting.hex_pages import BrainsweepPage, CraftingMultiPage, LookupPatternPage
@ -17,13 +18,7 @@ from patchouli.page import (
SpotlightPage, SpotlightPage,
TextPage, TextPage,
) )
from patchouli.text.tags import Stream from patchouli.text.html import HTMLStream
# extra info :(
# TODO: properties.toml
repo_names = {
"hexcasting": "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources",
}
def entry_spoilered(root_info: HexBook, entry: Entry): def entry_spoilered(root_info: HexBook, entry: Entry):
@ -36,42 +31,42 @@ def category_spoilered(root_info: HexBook, category: Category):
return all(entry_spoilered(root_info, ent) for ent in category.entries) return all(entry_spoilered(root_info, ent) for ent in category.entries)
def write_block(out: Stream, block: FormatTree | str | LocalizedStr): def write_block(out: HTMLStream, block: FormatTree | str | LocalizedStr):
if isinstance(block, LocalizedStr): if isinstance(block, LocalizedStr):
block = str(block) block = str(block)
if isinstance(block, str): if isinstance(block, str):
first = False first = False
for line in block.split("\n"): for line in block.split("\n"):
if first: if first:
out.tag("br") out.self_closing_element("br")
first = True first = True
out.text(line) out.text(line)
return return
with block.style.tag(out): with block.style.element(out):
for child in block.children: for child in block.children:
write_block(out, child) write_block(out, child)
def anchor_toc(out: Stream): def anchor_toc(out: HTMLStream):
with out.pair_tag( with out.element(
"a", href="#table-of-contents", clazz="permalink small", title="Jump to top" "a", href="#table-of-contents", clazz="permalink small", title="Jump to top"
): ):
out.empty_pair_tag("i", clazz="bi bi-box-arrow-up") out.empty_element("i", clazz="bi bi-box-arrow-up")
def permalink(out: Stream, link: str): def permalink(out: HTMLStream, link: str):
with out.pair_tag("a", href=link, clazz="permalink small", title="Permalink"): with out.element("a", href=link, clazz="permalink small", title="Permalink"):
out.empty_pair_tag("i", clazz="bi bi-link-45deg") out.empty_element("i", clazz="bi bi-link-45deg")
def write_page(out: Stream, pageid: str, page: Page[Any]): def write_page(out: HTMLStream, pageid: str, page: Page[Any], props: Properties):
if anchor := page.anchor: if anchor := page.anchor:
anchor_id = pageid + "@" + anchor anchor_id = pageid + "@" + anchor
else: else:
anchor_id = None anchor_id = None
# TODO: put this in the page classes - this is just a stopgap to make the tests pass # TODO: put this in the page classes - this is just a stopgap to make the tests pass
with out.pair_tag_if(anchor_id, "div", id=anchor_id): with out.element_if(anchor_id, "div", id=anchor_id):
if isinstance(page, PageWithTitle) and page.title is not None: if isinstance(page, PageWithTitle) and page.title is not None:
# gross # gross
_kwargs = ( _kwargs = (
@ -79,7 +74,7 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
if isinstance(page, LookupPatternPage) if isinstance(page, LookupPatternPage)
else {} else {}
) )
with out.pair_tag("h4", **_kwargs): with out.element("h4", **_kwargs):
out.text(page.title) out.text(page.title)
if anchor_id: if anchor_id:
permalink(out, "#" + anchor_id) permalink(out, "#" + anchor_id)
@ -89,69 +84,69 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
pass pass
case LinkPage(): case LinkPage():
write_block(out, page.text) write_block(out, page.text)
with out.pair_tag("h4", clazz="linkout"): with out.element("h4", clazz="linkout"):
with out.pair_tag("a", href=page.url): with out.element("a", href=page.url):
out.text(page.link_text) out.text(page.link_text)
case TextPage(): case TextPage():
# LinkPage is a TextPage, so this needs to be below it # LinkPage is a TextPage, so this needs to be below it
write_block(out, page.text) write_block(out, page.text)
case SpotlightPage(): case SpotlightPage():
with out.pair_tag("h4", clazz="spotlight-title page-header"): with out.element("h4", clazz="spotlight-title page-header"):
out.text(page.item) out.text(page.item)
if page.text is not None: if page.text is not None:
write_block(out, page.text) write_block(out, page.text)
case CraftingPage(): case CraftingPage():
with out.pair_tag("blockquote", clazz="crafting-info"): with out.element("blockquote", clazz="crafting-info"):
out.text(f"Depicted in the book: The crafting recipe for the ") out.text(f"Depicted in the book: The crafting recipe for the ")
first = True first = True
for recipe in page.recipes: for recipe in page.recipes:
if not first: if not first:
out.text(" and ") out.text(" and ")
first = False first = False
with out.pair_tag("code"): with out.element("code"):
out.text(recipe.result.item) out.text(recipe.result.item)
out.text(".") out.text(".")
if page.text is not None: if page.text is not None:
write_block(out, page.text) write_block(out, page.text)
case ImagePage(): case ImagePage():
with out.pair_tag("p", clazz="img-wrapper"): with out.element("p", clazz="img-wrapper"):
for img in page.images: for img in page.images:
# TODO: make a thing for this # TODO: make a thing for this
out.empty_pair_tag( out.empty_element(
"img", "img",
src=f"{repo_names[img.namespace]}/assets/{img.namespace}/{img.path}", src=f"{props.base_asset_urls[img.namespace]}/assets/{img.namespace}/{img.path}",
) )
if page.text is not None: if page.text is not None:
write_block(out, page.text) write_block(out, page.text)
case CraftingMultiPage(): case CraftingMultiPage():
with out.pair_tag("blockquote", clazz="crafting-info"): with out.element("blockquote", clazz="crafting-info"):
out.text( out.text(
f"Depicted in the book: Several crafting recipes, for the " f"Depicted in the book: Several crafting recipes, for the "
) )
with out.pair_tag("code"): with out.element("code"):
out.text(page.recipes[0].result.item) out.text(page.recipes[0].result.item)
for i in page.recipes[1:]: for i in page.recipes[1:]:
out.text(", ") out.text(", ")
with out.pair_tag("code"): with out.element("code"):
out.text(i.result.item) out.text(i.result.item)
out.text(".") out.text(".")
if page.text is not None: if page.text is not None:
write_block(out, page.text) write_block(out, page.text)
case BrainsweepPage(): case BrainsweepPage():
with out.pair_tag("blockquote", clazz="crafting-info"): with out.element("blockquote", clazz="crafting-info"):
out.text( out.text(
f"Depicted in the book: A mind-flaying recipe producing the " f"Depicted in the book: A mind-flaying recipe producing the "
) )
with out.pair_tag("code"): with out.element("code"):
out.text(page.recipe.result.name) out.text(page.recipe.result.name)
out.text(".") out.text(".")
if page.text is not None: if page.text is not None:
write_block(out, page.text) write_block(out, page.text)
case PageWithPattern(): case PageWithPattern():
with out.pair_tag("details", clazz="spell-collapsible"): with out.element("details", clazz="spell-collapsible"):
out.empty_pair_tag("summary", clazz="collapse-spell") out.empty_element("summary", clazz="collapse-spell")
for pattern in page.patterns: for pattern in page.patterns:
with out.pair_tag( with out.element(
"canvas", "canvas",
clazz="spell-viz", clazz="spell-viz",
width=216, width=216,
@ -166,30 +161,30 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
) )
write_block(out, page.text) write_block(out, page.text)
case _: case _:
with out.pair_tag("p", clazz="todo-note"): with out.element("p", clazz="todo-note"):
out.text(f"TODO: Missing processor for type: {type(page)}") out.text(f"TODO: Missing processor for type: {type(page)}")
if isinstance(page, PageWithText): if isinstance(page, PageWithText):
write_block(out, page.text) write_block(out, page.text)
out.tag("br") out.self_closing_element("br")
def write_entry(out: Stream, book: HexBook, entry: Entry): def write_entry(out: HTMLStream, book: HexBook, entry: Entry):
with out.pair_tag("div", id=entry.id.path): with out.element("div", id=entry.id.path):
with out.pair_tag_if(entry_spoilered(book, entry), "div", clazz="spoilered"): with out.element_if(entry_spoilered(book, entry), "div", clazz="spoilered"):
with out.pair_tag("h3", clazz="entry-title page-header"): with out.element("h3", clazz="entry-title page-header"):
write_block(out, entry.name) write_block(out, entry.name)
anchor_toc(out) anchor_toc(out)
permalink(out, entry.id.href) permalink(out, entry.id.href)
for page in entry.pages: for page in entry.pages:
write_page(out, entry.id.path, page) write_page(out, entry.id.path, page, book.context["props"])
def write_category(out: Stream, book: HexBook, category: Category): def write_category(out: HTMLStream, book: HexBook, category: Category):
with out.pair_tag("section", id=category.id.path): with out.element("section", id=category.id.path):
with out.pair_tag_if( with out.element_if(
category_spoilered(book, category), "div", clazz="spoilered" category_spoilered(book, category), "div", clazz="spoilered"
): ):
with out.pair_tag("h2", clazz="category-title page-header"): with out.element("h2", clazz="category-title page-header"):
write_block(out, category.name) write_block(out, category.name)
anchor_toc(out) anchor_toc(out)
permalink(out, category.id.href) permalink(out, category.id.href)
@ -199,31 +194,31 @@ def write_category(out: Stream, book: HexBook, category: Category):
write_entry(out, book, entry) write_entry(out, book, entry)
def write_toc(out: Stream, book: HexBook): def write_toc(out: HTMLStream, book: HexBook):
with out.pair_tag("h2", id="table-of-contents", clazz="page-header"): with out.element("h2", id="table-of-contents", clazz="page-header"):
out.text("Table of Contents") out.text("Table of Contents")
with out.pair_tag( with out.element(
"a", "a",
href="javascript:void(0)", href="javascript:void(0)",
clazz="permalink toggle-link small", clazz="permalink toggle-link small",
data_target="toc-category", data_target="toc-category",
title="Toggle all", title="Toggle all",
): ):
out.empty_pair_tag("i", clazz="bi bi-list-nested") out.empty_element("i", clazz="bi bi-list-nested")
permalink(out, "#table-of-contents") permalink(out, "#table-of-contents")
for category in book.categories.values(): for category in book.categories.values():
with out.pair_tag("details", clazz="toc-category"): with out.element("details", clazz="toc-category"):
with out.pair_tag("summary"): with out.element("summary"):
with out.pair_tag( with out.element(
"a", "a",
href=category.id.href, href=category.id.href,
clazz="spoilered" if category_spoilered(book, category) else "", clazz="spoilered" if category_spoilered(book, category) else "",
): ):
out.text(category.name) out.text(category.name)
with out.pair_tag("ul"): with out.element("ul"):
for entry in category.entries: for entry in category.entries:
with out.pair_tag("li"): with out.element("li"):
with out.pair_tag( with out.element(
"a", "a",
href=entry.id.href, href=entry.id.href,
clazz="spoilered" if entry_spoilered(book, entry) else "", clazz="spoilered" if entry_spoilered(book, entry) else "",
@ -231,15 +226,15 @@ def write_toc(out: Stream, book: HexBook):
out.text(entry.name) out.text(entry.name)
def write_book(out: Stream, book: HexBook): def write_book(out: HTMLStream, book: HexBook):
with out.pair_tag("div", clazz="container"): with out.element("div", clazz="container"):
with out.pair_tag("header", clazz="jumbotron"): with out.element("header", clazz="jumbotron"):
with out.pair_tag("h1", clazz="book-title"): with out.element("h1", clazz="book-title"):
write_block(out, book.name) write_block(out, book.name)
write_block(out, book.landing_text) write_block(out, book.landing_text)
with out.pair_tag("nav"): with out.element("nav"):
write_toc(out, book) write_toc(out, book)
with out.pair_tag("main", clazz="book-body"): with out.element("main", clazz="book-body"):
for category in book.categories.values(): for category in book.categories.values():
write_category(out, book, category) write_category(out, book, category)
@ -258,7 +253,7 @@ def generate_docs(book: HexBook, template: str) -> str:
_, *spoilers = line.split() _, *spoilers = line.split()
book.context["spoilers"].update(spoilers) book.context["spoilers"].update(spoilers)
elif line == "#DUMP_BODY_HERE\n": elif line == "#DUMP_BODY_HERE\n":
write_book(Stream(output), book) write_book(HTMLStream(output), book)
print("", file=output) print("", file=output)
else: else:
print(line, end="", file=output) print(line, end="", file=output)

View file

@ -17,7 +17,7 @@ from common.properties import Properties
from common.types import TryGetEnum from common.types import TryGetEnum
from minecraft.i18n import I18nContext, LocalizedStr from minecraft.i18n import I18nContext, LocalizedStr
from .tags import PairTag, Stream from .html import HTMLElement, HTMLStream
DEFAULT_MACROS = { DEFAULT_MACROS = {
"$(obf)": "$(k)", "$(obf)": "$(k)",
@ -167,27 +167,27 @@ class Style(ABC, HexDocModel[Any], frozen=True):
raise ValueError(f"Unhandled style: {style_str}") raise ValueError(f"Unhandled style: {style_str}")
@abstractmethod @abstractmethod
def tag(self, out: Stream) -> PairTag | nullcontext[None]: def element(self, out: HTMLStream) -> HTMLElement | nullcontext[None]:
... ...
class CommandStyle(Style, frozen=True): class CommandStyle(Style, frozen=True):
type: CommandStyleType | BaseStyleType type: CommandStyleType | BaseStyleType
def tag(self, out: Stream) -> PairTag | nullcontext[None]: def element(self, out: HTMLStream) -> HTMLElement | nullcontext[None]:
match self.type: match self.type:
case CommandStyleType.obfuscated: case CommandStyleType.obfuscated:
return out.pair_tag("span", clazz="obfuscated") return out.element("span", clazz="obfuscated")
case CommandStyleType.bold: case CommandStyleType.bold:
return out.pair_tag("strong") return out.element("strong")
case CommandStyleType.strikethrough: case CommandStyleType.strikethrough:
return out.pair_tag("s") return out.element("s")
case CommandStyleType.underline: case CommandStyleType.underline:
return out.pair_tag("span", style="text-decoration: underline") return out.element("span", style="text-decoration: underline")
case CommandStyleType.italic: case CommandStyleType.italic:
return out.pair_tag("i") return out.element("i")
case SpecialStyleType.base: case SpecialStyleType.base:
return out.null_tag() return nullcontext()
class ParagraphStyle(Style, frozen=True): class ParagraphStyle(Style, frozen=True):
@ -212,8 +212,8 @@ class ParagraphStyle(Style, frozen=True):
def list_item(cls) -> Self: def list_item(cls) -> Self:
return cls(attributes={"clazz": "fake-li"}) return cls(attributes={"clazz": "fake-li"})
def tag(self, out: Stream) -> PairTag: def element(self, out: HTMLStream) -> HTMLElement:
return out.pair_tag("p", **self.attributes) return out.element("p", **self.attributes)
def _format_href(value: str) -> str: def _format_href(value: str) -> str:
@ -226,20 +226,20 @@ class FunctionStyle(Style, frozen=True):
type: FunctionStyleType | ColorStyleType type: FunctionStyleType | ColorStyleType
value: str value: str
def tag(self, out: Stream) -> PairTag: def element(self, out: HTMLStream) -> HTMLElement:
match self.type: match self.type:
case FunctionStyleType.link: case FunctionStyleType.link:
return out.pair_tag("a", href=_format_href(self.value)) return out.element("a", href=_format_href(self.value))
case FunctionStyleType.tooltip: case FunctionStyleType.tooltip:
return out.pair_tag("span", clazz="has-tooltip", title=self.value) return out.element("span", clazz="has-tooltip", title=self.value)
case FunctionStyleType.cmd_click: case FunctionStyleType.cmd_click:
return out.pair_tag( return out.element(
"span", "span",
clazz="has-cmd_click", clazz="has-cmd_click",
title=f"When clicked, would execute: {self.value}", title=f"When clicked, would execute: {self.value}",
) )
case SpecialStyleType.color: case SpecialStyleType.color:
return out.pair_tag("span", style=f"color: #{self.value}") return out.element("span", style=f"color: #{self.value}")
# intentionally not inheriting from Style, because this is basically an implementation # intentionally not inheriting from Style, because this is basically an implementation

View file

@ -0,0 +1,51 @@
# TODO: type
from contextlib import nullcontext
from dataclasses import dataclass
from html import escape
from typing import IO, Any
from minecraft.i18n import LocalizedStr
def attributes_to_str(attributes: dict[str, Any]):
return "".join(
f" {'class' if key == 'clazz' else key.replace('_', '-')}={repr(escape(str(value)))}"
for key, value in attributes.items()
)
@dataclass
class HTMLElement:
stream: IO[str]
name: str
attributes: dict[str, Any]
def __enter__(self) -> None:
self.stream.write(f"<{self.name}{attributes_to_str(self.attributes)}>")
def __exit__(self, *_: Any) -> None:
self.stream.write(f"</{self.name}>")
@dataclass
class HTMLStream:
stream: IO[str]
def self_closing_element(self, name: str, **kwargs: Any):
keywords = attributes_to_str(kwargs)
self.stream.write(f"<{name}{keywords} />")
return self
def element(self, name: str, **kwargs: Any):
return HTMLElement(self.stream, name, kwargs)
def element_if(self, cond: Any, name: str, **kwargs: Any):
return self.element(name, **kwargs) if cond else nullcontext()
def empty_element(self, name: str, **kwargs: Any):
with self.element(name, **kwargs):
pass
def text(self, txt: str | LocalizedStr):
self.stream.write(escape(str(txt)))
return self

View file

@ -1,68 +0,0 @@
# TODO: type
from contextlib import nullcontext
from dataclasses import InitVar, dataclass
from html import escape
from typing import IO, Any
from minecraft.i18n import LocalizedStr
def tag_args(kwargs: dict[str, Any]):
return "".join(
f" {'class' if key == 'clazz' else key.replace('_', '-')}={repr(escape(str(value)))}"
for key, value in kwargs.items()
)
@dataclass
class PairTag:
stream: IO[str]
name: str
args: InitVar[dict[str, Any]]
def __post_init__(self, args: dict[str, Any]):
self.args_str = tag_args(args)
def __enter__(self):
# TODO: self.stream.write??????????
print(f"<{self.name}{self.args_str}>", file=self.stream, end="")
def __exit__(self, _1: Any, _2: Any, _3: Any):
print(f"</{self.name}>", file=self.stream, end="")
class Empty:
def __enter__(self):
pass
def __exit__(self, _1: Any, _2: Any, _3: Any):
pass
class Stream:
__slots__ = ["stream"]
def __init__(self, stream: IO[str]):
self.stream = stream
def tag(self, name: str, **kwargs: Any):
keywords = tag_args(kwargs)
print(f"<{name}{keywords} />", file=self.stream, end="")
return self
def pair_tag(self, name: str, **kwargs: Any):
return PairTag(self.stream, name, kwargs)
def pair_tag_if(self, cond: Any, name: str, **kwargs: Any):
return self.pair_tag(name, **kwargs) if cond else Empty()
def empty_pair_tag(self, name: str, **kwargs: Any):
with self.pair_tag(name, **kwargs):
pass
def null_tag(self):
return nullcontext()
def text(self, txt: str | LocalizedStr):
print(escape(str(txt)), file=self.stream, end="")
return self