We do a bit of renaming
This commit is contained in:
parent
aeb737a4eb
commit
92f7770dc6
7 changed files with 155 additions and 159 deletions
|
@ -3,6 +3,7 @@
|
|||
modid = "hexcasting"
|
||||
book_name = "thehexbook"
|
||||
template = "template.html"
|
||||
is_0_black = false
|
||||
recipe_dirs = [
|
||||
"{fabric.generated}/data/{modid}/recipes",
|
||||
"{forge.generated}/data/{modid}/recipes",
|
||||
|
@ -13,6 +14,9 @@ default_recipe_dir = 0
|
|||
# more on that later
|
||||
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]
|
||||
lang = "en_us"
|
||||
filename = "{lang}.json"
|
||||
|
@ -23,7 +27,6 @@ filename = "{lang}.json"
|
|||
|
||||
|
||||
# platforms
|
||||
# fabric and forge are optional
|
||||
|
||||
[common]
|
||||
src = "../Common/src"
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import re
|
||||
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.toml_placeholders import load_toml
|
||||
from hexcasting.pattern import PatternStubFile
|
||||
|
||||
NoTrailingSlashHttpUrl = Annotated[
|
||||
str,
|
||||
HttpUrl,
|
||||
AfterValidator(lambda u: str(u).rstrip("/")),
|
||||
]
|
||||
|
||||
|
||||
class PlatformProps(HexDocModel[Any]):
|
||||
resources: Path
|
||||
|
@ -27,15 +39,17 @@ class Properties(HexDocModel[Any]):
|
|||
modid: str
|
||||
book_name: str
|
||||
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]
|
||||
default_recipe_dir_index_: int = Field(alias="default_recipe_dir")
|
||||
|
||||
pattern_regex: re.Pattern[str]
|
||||
|
||||
is_0_black: bool = False
|
||||
"""If true, the style `$(0)` changes the text color to black; otherwise it resets
|
||||
the text color to the default."""
|
||||
base_asset_urls: dict[str, NoTrailingSlashHttpUrl]
|
||||
"""Mapping from modid to the url of that mod's `resources` directory on GitHub."""
|
||||
|
||||
i18n: I18nProps
|
||||
|
||||
|
@ -100,10 +114,11 @@ class Properties(HexDocModel[Any]):
|
|||
for stub in platform.pattern_stubs
|
||||
]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_default_recipe_dir(self):
|
||||
if self.default_recipe_dir_index_ >= len(self.recipe_dirs):
|
||||
@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 <={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
|
||||
|
|
|
@ -11,7 +11,7 @@ from patchouli.page import PageWithTitle
|
|||
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):
|
||||
title_: None = Field(default=None, include=True)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import io
|
||||
from typing import Any
|
||||
|
||||
from common.properties import Properties
|
||||
from hexcasting import HexBook
|
||||
from hexcasting.abstract_hex_pages import PageWithPattern
|
||||
from hexcasting.hex_pages import BrainsweepPage, CraftingMultiPage, LookupPatternPage
|
||||
|
@ -17,13 +18,7 @@ from patchouli.page import (
|
|||
SpotlightPage,
|
||||
TextPage,
|
||||
)
|
||||
from patchouli.text.tags import Stream
|
||||
|
||||
# extra info :(
|
||||
# TODO: properties.toml
|
||||
repo_names = {
|
||||
"hexcasting": "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources",
|
||||
}
|
||||
from patchouli.text.html import HTMLStream
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def write_block(out: Stream, block: FormatTree | str | LocalizedStr):
|
||||
def write_block(out: HTMLStream, block: FormatTree | str | LocalizedStr):
|
||||
if isinstance(block, LocalizedStr):
|
||||
block = str(block)
|
||||
if isinstance(block, str):
|
||||
first = False
|
||||
for line in block.split("\n"):
|
||||
if first:
|
||||
out.tag("br")
|
||||
out.self_closing_element("br")
|
||||
first = True
|
||||
out.text(line)
|
||||
return
|
||||
with block.style.tag(out):
|
||||
with block.style.element(out):
|
||||
for child in block.children:
|
||||
write_block(out, child)
|
||||
|
||||
|
||||
def anchor_toc(out: Stream):
|
||||
with out.pair_tag(
|
||||
def anchor_toc(out: HTMLStream):
|
||||
with out.element(
|
||||
"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):
|
||||
with out.pair_tag("a", href=link, clazz="permalink small", title="Permalink"):
|
||||
out.empty_pair_tag("i", clazz="bi bi-link-45deg")
|
||||
def permalink(out: HTMLStream, link: str):
|
||||
with out.element("a", href=link, clazz="permalink small", title="Permalink"):
|
||||
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:
|
||||
anchor_id = pageid + "@" + anchor
|
||||
else:
|
||||
anchor_id = None
|
||||
|
||||
# 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:
|
||||
# gross
|
||||
_kwargs = (
|
||||
|
@ -79,7 +74,7 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
|
|||
if isinstance(page, LookupPatternPage)
|
||||
else {}
|
||||
)
|
||||
with out.pair_tag("h4", **_kwargs):
|
||||
with out.element("h4", **_kwargs):
|
||||
out.text(page.title)
|
||||
if anchor_id:
|
||||
permalink(out, "#" + anchor_id)
|
||||
|
@ -89,69 +84,69 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
|
|||
pass
|
||||
case LinkPage():
|
||||
write_block(out, page.text)
|
||||
with out.pair_tag("h4", clazz="linkout"):
|
||||
with out.pair_tag("a", href=page.url):
|
||||
with out.element("h4", clazz="linkout"):
|
||||
with out.element("a", href=page.url):
|
||||
out.text(page.link_text)
|
||||
case TextPage():
|
||||
# LinkPage is a TextPage, so this needs to be below it
|
||||
write_block(out, page.text)
|
||||
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)
|
||||
if page.text is not None:
|
||||
write_block(out, page.text)
|
||||
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 ")
|
||||
first = True
|
||||
for recipe in page.recipes:
|
||||
if not first:
|
||||
out.text(" and ")
|
||||
first = False
|
||||
with out.pair_tag("code"):
|
||||
with out.element("code"):
|
||||
out.text(recipe.result.item)
|
||||
out.text(".")
|
||||
if page.text is not None:
|
||||
write_block(out, page.text)
|
||||
case ImagePage():
|
||||
with out.pair_tag("p", clazz="img-wrapper"):
|
||||
with out.element("p", clazz="img-wrapper"):
|
||||
for img in page.images:
|
||||
# TODO: make a thing for this
|
||||
out.empty_pair_tag(
|
||||
out.empty_element(
|
||||
"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:
|
||||
write_block(out, page.text)
|
||||
case CraftingMultiPage():
|
||||
with out.pair_tag("blockquote", clazz="crafting-info"):
|
||||
with out.element("blockquote", clazz="crafting-info"):
|
||||
out.text(
|
||||
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)
|
||||
for i in page.recipes[1:]:
|
||||
out.text(", ")
|
||||
with out.pair_tag("code"):
|
||||
with out.element("code"):
|
||||
out.text(i.result.item)
|
||||
out.text(".")
|
||||
if page.text is not None:
|
||||
write_block(out, page.text)
|
||||
case BrainsweepPage():
|
||||
with out.pair_tag("blockquote", clazz="crafting-info"):
|
||||
with out.element("blockquote", clazz="crafting-info"):
|
||||
out.text(
|
||||
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(".")
|
||||
if page.text is not None:
|
||||
write_block(out, page.text)
|
||||
case PageWithPattern():
|
||||
with out.pair_tag("details", clazz="spell-collapsible"):
|
||||
out.empty_pair_tag("summary", clazz="collapse-spell")
|
||||
with out.element("details", clazz="spell-collapsible"):
|
||||
out.empty_element("summary", clazz="collapse-spell")
|
||||
for pattern in page.patterns:
|
||||
with out.pair_tag(
|
||||
with out.element(
|
||||
"canvas",
|
||||
clazz="spell-viz",
|
||||
width=216,
|
||||
|
@ -166,30 +161,30 @@ def write_page(out: Stream, pageid: str, page: Page[Any]):
|
|||
)
|
||||
write_block(out, page.text)
|
||||
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)}")
|
||||
if isinstance(page, PageWithText):
|
||||
write_block(out, page.text)
|
||||
out.tag("br")
|
||||
out.self_closing_element("br")
|
||||
|
||||
|
||||
def write_entry(out: Stream, book: HexBook, entry: Entry):
|
||||
with out.pair_tag("div", id=entry.id.path):
|
||||
with out.pair_tag_if(entry_spoilered(book, entry), "div", clazz="spoilered"):
|
||||
with out.pair_tag("h3", clazz="entry-title page-header"):
|
||||
def write_entry(out: HTMLStream, book: HexBook, entry: Entry):
|
||||
with out.element("div", id=entry.id.path):
|
||||
with out.element_if(entry_spoilered(book, entry), "div", clazz="spoilered"):
|
||||
with out.element("h3", clazz="entry-title page-header"):
|
||||
write_block(out, entry.name)
|
||||
anchor_toc(out)
|
||||
permalink(out, entry.id.href)
|
||||
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):
|
||||
with out.pair_tag("section", id=category.id.path):
|
||||
with out.pair_tag_if(
|
||||
def write_category(out: HTMLStream, book: HexBook, category: Category):
|
||||
with out.element("section", id=category.id.path):
|
||||
with out.element_if(
|
||||
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)
|
||||
anchor_toc(out)
|
||||
permalink(out, category.id.href)
|
||||
|
@ -199,31 +194,31 @@ def write_category(out: Stream, book: HexBook, category: Category):
|
|||
write_entry(out, book, entry)
|
||||
|
||||
|
||||
def write_toc(out: Stream, book: HexBook):
|
||||
with out.pair_tag("h2", id="table-of-contents", clazz="page-header"):
|
||||
def write_toc(out: HTMLStream, book: HexBook):
|
||||
with out.element("h2", id="table-of-contents", clazz="page-header"):
|
||||
out.text("Table of Contents")
|
||||
with out.pair_tag(
|
||||
with out.element(
|
||||
"a",
|
||||
href="javascript:void(0)",
|
||||
clazz="permalink toggle-link small",
|
||||
data_target="toc-category",
|
||||
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")
|
||||
for category in book.categories.values():
|
||||
with out.pair_tag("details", clazz="toc-category"):
|
||||
with out.pair_tag("summary"):
|
||||
with out.pair_tag(
|
||||
with out.element("details", clazz="toc-category"):
|
||||
with out.element("summary"):
|
||||
with out.element(
|
||||
"a",
|
||||
href=category.id.href,
|
||||
clazz="spoilered" if category_spoilered(book, category) else "",
|
||||
):
|
||||
out.text(category.name)
|
||||
with out.pair_tag("ul"):
|
||||
with out.element("ul"):
|
||||
for entry in category.entries:
|
||||
with out.pair_tag("li"):
|
||||
with out.pair_tag(
|
||||
with out.element("li"):
|
||||
with out.element(
|
||||
"a",
|
||||
href=entry.id.href,
|
||||
clazz="spoilered" if entry_spoilered(book, entry) else "",
|
||||
|
@ -231,15 +226,15 @@ def write_toc(out: Stream, book: HexBook):
|
|||
out.text(entry.name)
|
||||
|
||||
|
||||
def write_book(out: Stream, book: HexBook):
|
||||
with out.pair_tag("div", clazz="container"):
|
||||
with out.pair_tag("header", clazz="jumbotron"):
|
||||
with out.pair_tag("h1", clazz="book-title"):
|
||||
def write_book(out: HTMLStream, book: HexBook):
|
||||
with out.element("div", clazz="container"):
|
||||
with out.element("header", clazz="jumbotron"):
|
||||
with out.element("h1", clazz="book-title"):
|
||||
write_block(out, book.name)
|
||||
write_block(out, book.landing_text)
|
||||
with out.pair_tag("nav"):
|
||||
with out.element("nav"):
|
||||
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():
|
||||
write_category(out, book, category)
|
||||
|
||||
|
@ -258,7 +253,7 @@ def generate_docs(book: HexBook, template: str) -> str:
|
|||
_, *spoilers = line.split()
|
||||
book.context["spoilers"].update(spoilers)
|
||||
elif line == "#DUMP_BODY_HERE\n":
|
||||
write_book(Stream(output), book)
|
||||
write_book(HTMLStream(output), book)
|
||||
print("", file=output)
|
||||
else:
|
||||
print(line, end="", file=output)
|
||||
|
|
|
@ -17,7 +17,7 @@ from common.properties import Properties
|
|||
from common.types import TryGetEnum
|
||||
from minecraft.i18n import I18nContext, LocalizedStr
|
||||
|
||||
from .tags import PairTag, Stream
|
||||
from .html import HTMLElement, HTMLStream
|
||||
|
||||
DEFAULT_MACROS = {
|
||||
"$(obf)": "$(k)",
|
||||
|
@ -167,27 +167,27 @@ class Style(ABC, HexDocModel[Any], frozen=True):
|
|||
raise ValueError(f"Unhandled style: {style_str}")
|
||||
|
||||
@abstractmethod
|
||||
def tag(self, out: Stream) -> PairTag | nullcontext[None]:
|
||||
def element(self, out: HTMLStream) -> HTMLElement | nullcontext[None]:
|
||||
...
|
||||
|
||||
|
||||
class CommandStyle(Style, frozen=True):
|
||||
type: CommandStyleType | BaseStyleType
|
||||
|
||||
def tag(self, out: Stream) -> PairTag | nullcontext[None]:
|
||||
def element(self, out: HTMLStream) -> HTMLElement | nullcontext[None]:
|
||||
match self.type:
|
||||
case CommandStyleType.obfuscated:
|
||||
return out.pair_tag("span", clazz="obfuscated")
|
||||
return out.element("span", clazz="obfuscated")
|
||||
case CommandStyleType.bold:
|
||||
return out.pair_tag("strong")
|
||||
return out.element("strong")
|
||||
case CommandStyleType.strikethrough:
|
||||
return out.pair_tag("s")
|
||||
return out.element("s")
|
||||
case CommandStyleType.underline:
|
||||
return out.pair_tag("span", style="text-decoration: underline")
|
||||
return out.element("span", style="text-decoration: underline")
|
||||
case CommandStyleType.italic:
|
||||
return out.pair_tag("i")
|
||||
return out.element("i")
|
||||
case SpecialStyleType.base:
|
||||
return out.null_tag()
|
||||
return nullcontext()
|
||||
|
||||
|
||||
class ParagraphStyle(Style, frozen=True):
|
||||
|
@ -212,8 +212,8 @@ class ParagraphStyle(Style, frozen=True):
|
|||
def list_item(cls) -> Self:
|
||||
return cls(attributes={"clazz": "fake-li"})
|
||||
|
||||
def tag(self, out: Stream) -> PairTag:
|
||||
return out.pair_tag("p", **self.attributes)
|
||||
def element(self, out: HTMLStream) -> HTMLElement:
|
||||
return out.element("p", **self.attributes)
|
||||
|
||||
|
||||
def _format_href(value: str) -> str:
|
||||
|
@ -226,20 +226,20 @@ class FunctionStyle(Style, frozen=True):
|
|||
type: FunctionStyleType | ColorStyleType
|
||||
value: str
|
||||
|
||||
def tag(self, out: Stream) -> PairTag:
|
||||
def element(self, out: HTMLStream) -> HTMLElement:
|
||||
match self.type:
|
||||
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:
|
||||
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:
|
||||
return out.pair_tag(
|
||||
return out.element(
|
||||
"span",
|
||||
clazz="has-cmd_click",
|
||||
title=f"When clicked, would execute: {self.value}",
|
||||
)
|
||||
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
|
||||
|
|
51
doc/src/patchouli/text/html.py
Normal file
51
doc/src/patchouli/text/html.py
Normal 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
|
|
@ -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
|
Loading…
Reference in a new issue