diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 000737ec5..0565e8158 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -1,29 +1,60 @@ -name: Build the Python doc-gen +name: Build the web book on: push: - branches: [main] + branches: '1.18' + workflow_dispatch: + inputs: + release: + description: Release this version + type: boolean + default: false + publish: + description: Package index to publish to + type: choice + options: + - none + - PyPI + +env: + PYPI_PACKAGE: hexdoc-hexcasting + +permissions: + contents: read jobs: - build_docs: - runs-on: ubuntu-latest + hexdoc: + uses: hexdoc-dev/hexdoc/.github/workflows/hexdoc.yml@main permissions: contents: write + pages: read + secrets: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + python-version: "3.11" + release: |- + ${{ + github.event_name != 'push' && inputs.release + || github.event_name == 'push' && startsWith(github.event.head_commit.message, '[Release]') + }} + + publish-pypi: + runs-on: ubuntu-latest + needs: hexdoc + if: |- + needs.hexdoc.outputs.release == 'true' && + (github.event_name == 'push' || inputs.publish == 'PyPI') + environment: + name: pypi + url: https://pypi.org/p/${{ env.PYPI_PACKAGE }} + permissions: + id-token: write steps: - - uses: actions/checkout@v3 - - name: Generate file - run: doc/collate_data.py Common/src/main/resources hexcasting thehexbook doc/template.html 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.email "noreply@github.com" - git add index.html - git diff-index --quiet HEAD || git commit -m "Update docs at index.html from $GITHUB_REF" - - name: Upload changes - run: git push + - name: Download package artifact + uses: actions/download-artifact@v3 + with: + name: hexdoc-build + path: dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 6c344479b..0b04d6694 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +# hexdoc +doc/**/_export/generated/ +/_site/ +/_checkout/ +__gradle_version__.py +.hexdoc/ + # eclipse bin *.launch @@ -21,9 +28,168 @@ build eclipse run +# MacOS moment +.DS_Store + # Files from Forge MDK forge*changelog.txt Session.vim plot/ + +# Python + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json b/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json index 7cf089590..cbeecbde2 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json +++ b/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json @@ -802,7 +802,7 @@ "_comment": "Продвинутые руны", "hexcasting.entry.itempicking": "Работа с предметами", - "hexcasting.page.itempicking.1": "Некоторые руны, такие как $(l:hexcasting:patterns/spells/blockworks#OpPlaceBlock)$(action)Поставить блок/$, потребляют дополнительные предметы из инвентаря. Руна ищёт предмет для использования.", + "hexcasting.page.itempicking.1": "Некоторые руны, такие как $(l:hexcasting:patterns/spells/blockworks#hexcasting:place_block)$(action)Поставить блок/$, потребляют дополнительные предметы из инвентаря. Руна ищёт предмет для использования.", "hexcasting.page.itempicking.2": "$(li)Сначала, руна ищёт первый подходящий предмет в хотбаре около $(italic)Посоха/$.$(li)Во вторых, руна использует $(italic)самый далёкий предмет из инвентаря/$.", "hexcasting.page.itempicking.3": "Так можно выбрать какой предмет какое заклинание будет использовать.", @@ -867,7 +867,7 @@ "hexcasting.page.flight.1": "The power of flight! I have wrestled Nature to its knees. But Nature is vengeful, and itches for me to break its contract so it may break my shins.", "hexcasting.page.flight.2": "The entity (which must be a player) will be endowed with flight. The first number is the number of seconds they may fly for, and the second number is the radius of the zone they may fly in. If the recipient exits that zone, or their timer runs out while midair, the gravity that they spurned will get its revenge. Painfully.$(br2)It costs approximately 1 $(item)Amethyst Dust/$ multiplied by the radius, per second of flight.", - "hexcasting.page.teleport.1": "Куда мощнее $(l:patterns/spells/basic#OpBlink)$(action)Блинка/$, это позволит мне телепортироваться на невероятные расстония, пускай ограничение и есть, но оно $(italic)куда больше/$.", + "hexcasting.page.teleport.1": "Куда мощнее $(l:patterns/spells/basic#hexcasting:blink)$(action)Блинка/$, это позволит мне телепортироваться на невероятные расстония, пускай ограничение и есть, но оно $(italic)куда больше/$.", "hexcasting.page.teleport.2": "The entity will be teleported by the given vector, which is an offset from its given position. No matter the distance, it always seems to cost about ten $(item)Charged Crystal/$s.$(br2)The transference is not perfect, and it seems when teleporting something as complex as a player, their inventory doesn't $(italic)quite/$ stay attached, and tends to splatter everywhere at the destination.", "hexcasting.entry.zeniths": "Zeniths", @@ -878,7 +878,7 @@ "hexcasting.page.zeniths.5": "Bestows haste. Base cost is one $(item)Amethyst Dust/$ per 3 seconds.", "hexcasting.page.zeniths.6": "Bestows strength. Base cost is one $(item)Amethyst Dust/$ per 3 seconds.", - "hexcasting.page.greater_sentinel.1": "Summon a greater version of my $(l:patterns/sentinels)$(thing)Sentinel/$. Costs about 2 $(item)Amethyst Dust/$s.", + "hexcasting.page.greater_sentinel.1": "Summon a greater version of my $(l:patterns/spells/sentinels)$(thing)Sentinel/$. Costs about 2 $(item)Amethyst Dust/$s.", "hexcasting.page.greater_sentinel.2": "The stronger sentinel acts like the normal one I can summon without the use of a Great Spell, if a little more visually interesting. However, the range in which my spells can work is extended to a small region around my greater sentinel, about 16 blocks. In other words, no matter where in the world I am, I can interact with things around my sentinel (the mysterious forces of chunkloading notwithstanding).", "hexcasting.page.make_battery.1": "Infuse a bottle with _media to form a $(item)Phial./$", diff --git a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/categories/greatwork.json b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/categories/greatwork.json index cee14c1a9..ed8cd6c26 100644 --- a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/categories/greatwork.json +++ b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/categories/greatwork.json @@ -2,6 +2,5 @@ "name": "hexcasting.entry.greatwork", "description": "hexcasting.entry.greatwork.desc", "icon": "minecraft:music_disc_11", - "sortnum": 3, - "entry_color": "54398a" + "sortnum": 3 } diff --git a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/greater_sentinel.json b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/greater_sentinel.json index f855cd80e..8fc890a46 100644 --- a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/greater_sentinel.json +++ b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/greater_sentinel.json @@ -9,6 +9,7 @@ { "type": "hexcasting:pattern", "op_id": "hexcasting:sentinel/create/great", + "anchor": "hexcasting:sentinel/create/great", "text": "hexcasting.page.greater_sentinel.1", "input": "vector", "output": "" diff --git a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/zeniths.json b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/zeniths.json index 3a295b323..b5899a67c 100644 --- a/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/zeniths.json +++ b/Common/src/main/resources/data/hexcasting/patchouli_books/thehexbook/en_us/entries/patterns/great_spells/zeniths.json @@ -3,7 +3,7 @@ "category": "hexcasting:patterns/great_spells", "icon": "minecraft:potion{Potion:'minecraft:regeneration'}", "advancement": "hexcasting:root", - "sort_num": 4, + "sortnum": 4, "read_by_default": true, "pages": [ { diff --git a/doc/LICENSE.txt b/doc/LICENSE.txt new file mode 100644 index 000000000..81a0f86dd --- /dev/null +++ b/doc/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 object-Object, Alwinfy, gamma-delta + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 000000000..609f80261 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,56 @@ +# hexdoc-hexcasting + +Python web book docgen and [hexdoc](https://pypi.org/project/hexdoc) plugin for Hex Casting. + +## Version scheme + +We use [hatch-gradle-version](https://pypi.org/project/hatch-gradle-version) to generate the version number based on whichever mod version the docgen was built with. + +The version is in this format: `mod-version.python-version.mod-pre.python-dev.python-post` + +For example: +* Mod version: `0.11.1-7` +* Python package version: `1.0.dev0` +* Full version: `0.11.1.1.0rc7.dev0` + +## Setup + +```sh +python3.11 -m venv venv + +.\venv\Scripts\activate # Windows +. venv/bin/activate.fish # fish +source venv/bin/activate # everything else + +# run from the repo root, not doc/ +pip install -e .[dev] +``` + +## Usage + +For local testing, create a file called `.env` in the repo root following this template: +```sh +GITHUB_REPOSITORY=gamma-delta/HexMod +GITHUB_SHA=main +GITHUB_PAGES_URL=https://gamma-delta.github.io/HexMod +``` + +Useful commands: +```sh +# show help +hexdoc -h + +# render and serve the web book in watch mode +nodemon --config doc/nodemon.json + +# render and serve the web book +hexdoc serve + +# export, render, and merge the web book +hexdoc export +hexdoc render +hexdoc merge + +# start the Python interpreter with some extra local variables +hexdoc repl +``` diff --git a/doc/collate_data.py b/doc/collate_data.py deleted file mode 100755 index 0bf422a13..000000000 --- a/doc/collate_data.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/env python3 -from sys import argv, stdout -from collections import namedtuple -from html import escape -import json # codec -import re # parsing -import os # listdir - -# TO USE: put in Hexcasting root dir, collate_data.py src/main/resources hexcasting thehexbook out.html - -# extra info :( -lang = "en_us" -repo_names = { - "hexcasting": "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources", -} -extra_i18n = { - "item.minecraft.amethyst_shard": "Amethyst Shard", - "item.minecraft.budding_amethyst": "Budding Amethyst", - "block.hexcasting.slate": "Blank Slate", -} - -default_macros = { - "$(obf)": "$(k)", - "$(bold)": "$(l)", - "$(strike)": "$(m)", - "$(italic)": "$(o)", - "$(italics)": "$(o)", - "$(list": "$(li", - "$(reset)": "$()", - "$(clear)": "$()", - "$(2br)": "$(br2)", - "$(p)": "$(br2)", - "/$": "$()", - "
": "$(br)", - "$(nocolor)": "$(0)", - "$(item)": "$(#b0b)", - "$(thing)": "$(#490)", -} - -colors = { - "0": None, - "1": "00a", - "2": "0a0", - "3": "0aa", - "4": "a00", - "5": "a0a", - "6": "fa0", - "7": "aaa", - "8": "555", - "9": "55f", - "a": "5f5", - "b": "5ff", - "c": "f55", - "d": "f5f", - "e": "ff5", - "f": "fff", -} -types = { - "k": "obf", - "l": "bold", - "m": "strikethrough", - "n": "underline", - "o": "italic", -} - -keys = { - "use": "Right Click", - "sneak": "Left Shift", -} - -bind1 = (lambda: None).__get__(0).__class__ - -def slurp(filename): - with open(filename, "r") as fh: - return json.load(fh) - -FormatTree = namedtuple("FormatTree", ["style", "children"]) -Style = namedtuple("Style", ["type", "value"]) - -def parse_style(sty): - if sty == "br": - return "\n", None - if sty == "br2": - return "", Style("para", {}) - if sty == "li": - return "", Style("para", {"clazz": "fake-li"}) - if sty[:2] == "k:": - return keys[sty[2:]], None - if sty[:2] == "l:": - return "", Style("link", sty[2:]) - if sty == "/l": - return "", Style("link", None) - if sty == "playername": - return "[Playername]", None - if sty[:2] == "t:": - return "", Style("tooltip", sty[2:]) - if sty == "/t": - return "", Style("tooltip", None) - if sty[:2] == "c:": - return "", Style("cmd_click", sty[2:]) - if sty == "/c": - return "", Style("cmd_click", None) - if sty == "r" or not sty: - return "", Style("base", None) - if sty in types: - return "", Style(types[sty], True) - if sty in colors: - return "", Style("color", colors[sty]) - if sty.startswith("#") and len(sty) in [4, 7]: - return "", Style("color", sty[1:]) - # TODO more style parse - raise ValueError("Unknown style: " + sty) - -def localize(i18n, string, default=None): - return i18n.get(string, default if default else string) if i18n else string - -format_re = re.compile(r"\$\(([^)]*)\)") -def format_string(root_data, string): - # resolve lang - string = localize(root_data["i18n"], string) - # resolve macros - old_string = None - while old_string != string: - old_string = string - for macro, replace in root_data["macros"].items(): - string = string.replace(macro, replace) - else: break - - # lex out parsed styles - text_nodes = [] - styles = [] - last_end = 0 - extra_text = "" - for mobj in re.finditer(format_re, string): - bonus_text, sty = parse_style(mobj.group(1)) - text = string[last_end:mobj.start()] + bonus_text - if sty: - styles.append(sty) - text_nodes.append(extra_text + text) - extra_text = "" - else: - extra_text += text - last_end = mobj.end() - text_nodes.append(extra_text + string[last_end:]) - first_node, *text_nodes = text_nodes - - # parse - style_stack = [FormatTree(Style("base", True), []), FormatTree(Style("para", {}), [first_node])] - for style, text in zip(styles, text_nodes): - tmp_stylestack = [] - if style.type == "base": - while style_stack[-1].style.type != "para": - last_node = style_stack.pop() - style_stack[-1].children.append(last_node) - elif any(tree.style.type == style.type for tree in style_stack): - while len(style_stack) >= 2: - last_node = style_stack.pop() - style_stack[-1].children.append(last_node) - if last_node.style.type == style.type: - break - tmp_stylestack.append(last_node.style) - for sty in tmp_stylestack: - style_stack.append(FormatTree(sty, [])) - if style.value is None: - if text: style_stack[-1].children.append(text) - else: - style_stack.append(FormatTree(style, [text] if text else [])) - while len(style_stack) >= 2: - last_node = style_stack.pop() - style_stack[-1].children.append(last_node) - - return style_stack[0] - -test_root = {"i18n": {}, "macros": default_macros, "resource_dir": "Common/src/main/resources", "modid": "hexcasting"} -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$(). " - -def localize_pattern(root_data, op_id): - return localize(root_data["i18n"], "hexcasting.spell.book." + op_id, - localize(root_data["i18n"], "hexcasting.spell." + op_id)) - - -def do_localize(root_data, obj, *names): - for name in names: - if name in obj: - obj[name] = localize(root_data["i18n"], obj[name]) - -def do_format(root_data, obj, *names): - for name in names: - if name in obj: - obj[name] = format_string(root_data, obj[name]) - -def identity(x): return x - -pattern_pat = re.compile(r'HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?') -pattern_stubs = [(None, "at/petrak/hexcasting/interop/pehkui/PehkuiInterop.java"), (None, "at/petrak/hexcasting/common/casting/RegisterPatterns.java"), ("Fabric", "at/petrak/hexcasting/fabric/interop/gravity/GravityApiInterop.java")] -def fetch_patterns(root_data): - registry = {} - for loader, stub in pattern_stubs: - filename = f"{root_data['resource_dir']}/../java/{stub}" - if loader: filename = filename.replace("Common", loader) - with open(filename, "r") as fh: - pattern_data = fh.read() - for mobj in re.finditer(pattern_pat, pattern_data): - string, start_angle, name, is_per_world = mobj.groups() - registry[root_data["modid"] + ":" + name] = (string, start_angle, bool(is_per_world)) - return registry - -def resolve_pattern(root_data, page): - if "pattern_reg" not in root_data: - root_data["pattern_reg"] = fetch_patterns(root_data) - page["op"] = [root_data["pattern_reg"][page["op_id"]]] - page["name"] = localize_pattern(root_data, page["op_id"]) - -def fixup_pattern(do_sig, root_data, page): - patterns = page["patterns"] - if "op_id" in page: - page["header"] = localize_pattern(root_data, page["op_id"]) - if not isinstance(patterns, list): patterns = [patterns] - if do_sig: - inp = page.get("input", None) or "" - oup = page.get("output", None) or "" - pipe = f"{inp} \u2192 {oup}".strip() - suffix = f" ({pipe})" if inp or oup else "" - page["header"] += suffix - page["op"] = [(p["signature"], p["startdir"], False) for p in patterns] - -def fetch_recipe(root_data, recipe): - modid, recipeid = recipe.split(":") - gen_resource_dir = root_data["resource_dir"].replace("/main/", "/generated/").replace("Common/", "Forge/") # TODO hack - recipe_path = f"{gen_resource_dir}/data/{modid}/recipes/{recipeid}.json" - return slurp(recipe_path) -def fetch_recipe_result(root_data, recipe): - return fetch_recipe(root_data, recipe)["result"]["item"] -def fetch_bswp_recipe_result(root_data, recipe): - return fetch_recipe(root_data, recipe)["result"]["name"] - -def localize_item(root_data, item): - # TODO hack - item = re.sub("{.*", "", item.replace(":", ".")) - block = "block." + item - block_l = localize(root_data["i18n"], block) - if block_l != block: return block_l - return localize(root_data["i18n"], "item." + item) - -page_types = { - "hexcasting:pattern": resolve_pattern, - "hexcasting:manual_pattern": bind1(fixup_pattern, True), - "hexcasting:manual_pattern_nosig": bind1(fixup_pattern, False), - "hexcasting:brainsweep": lambda rd, page: page.__setitem__("output_name", localize_item(rd, fetch_bswp_recipe_result(rd, page["recipe"]))), - "patchouli:link": lambda rd, page: do_localize(rd, page, "link_text"), - "patchouli:crafting": lambda rd, page: page.__setitem__("item_name", [localize_item(rd, fetch_recipe_result(rd, page[ty])) for ty in ("recipe", "recipe2") if ty in page]), - "hexcasting:crafting_multi": lambda rd, page: page.__setitem__("item_name", [localize_item(rd, fetch_recipe_result(rd, recipe)) for recipe in page["recipes"]]), - "patchouli:spotlight": lambda rd, page: page.__setitem__("item_name", localize_item(rd, page["item"])) -} - -def walk_dir(root_dir, prefix): - search_dir = root_dir + '/' + prefix - for fh in os.scandir(search_dir): - if fh.is_dir(): - yield from walk_dir(root_dir, prefix + fh.name + '/') - elif fh.name.endswith(".json"): - yield prefix + fh.name - -def parse_entry(root_data, entry_path, ent_name): - data = slurp(f"{entry_path}") - do_localize(root_data, data, "name") - for i, page in enumerate(data["pages"]): - if isinstance(page, str): - page = {"type": "patchouli:text", "text": page} - data["pages"][i] = page - - do_localize(root_data, page, "title", "header") - do_format(root_data, page, "text") - if page["type"] in page_types: - page_types[page["type"]](root_data, page) - data["id"] = ent_name - - return data - -def parse_category(root_data, base_dir, cat_name): - data = slurp(f"{base_dir}/categories/{cat_name}.json") - do_localize(root_data, data, "name") - do_format(root_data, data, "description") - - entry_dir = f"{base_dir}/entries/{cat_name}" - entries = [] - for filename in os.listdir(entry_dir): - if filename.endswith(".json"): - basename = filename[:-5] - entries.append(parse_entry(root_data, f"{entry_dir}/{filename}", cat_name + "/" + basename)) - entries.sort(key=lambda ent: (not ent.get("priority", False), ent.get("sortnum", 0), ent["name"])) - data["entries"] = entries - data["id"] = cat_name - - return data - -def parse_sortnum(cats, name): - if '/' in name: - ix = name.rindex('/') - return parse_sortnum(cats, name[:ix]) + (cats[name].get("sortnum", 0),) - return cats[name].get("sortnum", 0), - -def parse_book(root, mod_name, book_name): - base_dir = f"{root}/data/{mod_name}/patchouli_books/{book_name}" - root_info = slurp(f"{base_dir}/book.json") - - root_info["resource_dir"] = root - root_info["modid"] = mod_name - root_info.setdefault("macros", {}).update(default_macros) - if root_info.setdefault("i18n", {}): - root_info["i18n"] = slurp(f"{root}/assets/{mod_name}/lang/{lang}.json") - root_info["i18n"].update(extra_i18n) - - book_dir = f"{base_dir}/{lang}" - - categories = [] - for filename in walk_dir(f"{book_dir}/categories", ""): - basename = filename[:-5] - categories.append(parse_category(root_info, book_dir, basename)) - cats = {cat["id"]: cat for cat in categories} - categories.sort(key=lambda cat: (parse_sortnum(cats, cat["id"]), cat["name"])) - - do_localize(root_info, root_info, "name") - do_format(root_info, root_info, "landing_text") - root_info["categories"] = categories - root_info["blacklist"] = set() - root_info["spoilers"] = set() - - return root_info - -def tag_args(kwargs): - return "".join(f" {'class' if key == 'clazz' else key.replace('_', '-')}={repr(escape(str(value)))}" for key, value in kwargs.items()) - -class PairTag: - __slots__ = ["stream", "name", "kwargs"] - def __init__(self, stream, name, **kwargs): - self.stream = stream - self.name = name - self.kwargs = tag_args(kwargs) - def __enter__(self): - print(f"<{self.name}{self.kwargs}>", file=self.stream, end="") - def __exit__(self, _1, _2, _3): - print(f"", file=self.stream, end="") - -class Empty: - def __enter__(self): pass - def __exit__(self, _1, _2, _3): pass - -class Stream: - __slots__ = ["stream", "thunks"] - def __init__(self, stream): - self.stream = stream - self.thunks = [] - - def tag(self, name, **kwargs): - keywords = tag_args(kwargs) - print(f"<{name}{keywords} />", file=self.stream, end="") - return self - - def pair_tag(self, name, **kwargs): - return PairTag(self.stream, name, **kwargs) - - def pair_tag_if(self, cond, name, **kwargs): - return self.pair_tag(name, **kwargs) if cond else Empty() - - def empty_pair_tag(self, name, **kwargs): - with self.pair_tag(name, **kwargs): pass - - def text(self, txt): - print(escape(txt), file=self.stream, end="") - return self - -def get_format(out, ty, value): - if ty == "para": - return out.pair_tag("p", **value) - if ty == "color": - return out.pair_tag("span", style=f"color: #{value}") - if ty == "link": - link = value - if "://" not in link: - link = "#" + link.replace("#", "@") - return out.pair_tag("a", href=link) - if ty == "tooltip": - return out.pair_tag("span", clazz="has-tooltip", title=value) - if ty == "cmd_click": - return out.pair_tag("span", clazz="has-cmd_click", title="When clicked, would execute: "+value) - if ty == "obf": - return out.pair_tag("span", clazz="obfuscated") - if ty == "bold": - return out.pair_tag("strong") - if ty == "italic": - return out.pair_tag("i") - if ty == "strikethrough": - return out.pair_tag("s") - if ty == "underline": - return out.pair_tag("span", style="text-decoration: underline") - raise ValueError("Unknown format type: " + ty) - -def entry_spoilered(root_info, entry): - return entry.get("advancement", None) in root_info["spoilers"] - -def category_spoilered(root_info, category): - return all(entry_spoilered(root_info, ent) for ent in category["entries"]) - -def write_block(out, block): - if isinstance(block, str): - first = False - for line in block.split("\n"): - if first: - out.tag("br") - first = True - out.text(line) - return - sty_type = block.style.type - if sty_type == "base": - for child in block.children: write_block(out, child) - return - tag = get_format(out, sty_type, block.style.value) - with tag: - for child in block.children: - write_block(out, child) - -def anchor_toc(out): - with out.pair_tag("a", href="#table-of-contents", clazz="permalink small", title="Jump to top"): - out.empty_pair_tag("i", clazz="bi bi-box-arrow-up") - -def permalink(out, link): - with out.pair_tag("a", href=link, clazz="permalink small", title="Permalink"): - out.empty_pair_tag("i", clazz="bi bi-link-45deg") - -# TODO modularize -def write_page(out, pageid, page): - if "anchor" in page: - anchor_id = pageid + "@" + page["anchor"] - else: anchor_id = None - - with out.pair_tag_if(anchor_id, "div", id=anchor_id): - if "header" in page or "title" in page: - with out.pair_tag("h4"): - out.text(page.get("header", page.get("title", None))) - if anchor_id: - permalink(out, "#" + anchor_id) - - ty = page["type"] - if ty == "patchouli:text": - write_block(out, page["text"]) - elif ty == "patchouli:empty": pass - elif ty == "patchouli:link": - write_block(out, page["text"]) - with out.pair_tag("h4", clazz="linkout"): - with out.pair_tag("a", href=page["url"]): - out.text(page["link_text"]) - elif ty == "patchouli:spotlight": - with out.pair_tag("h4", clazz="spotlight-title page-header"): - out.text(page["item_name"]) - if "text" in page: write_block(out, page["text"]) - elif ty == "patchouli:crafting": - with out.pair_tag("blockquote", clazz="crafting-info"): - out.text(f"Depicted in the book: The crafting recipe for the ") - first = True - for name in page["item_name"]: - if not first: out.text(" and ") - first = False - with out.pair_tag("code"): out.text(name) - out.text(".") - if "text" in page: write_block(out, page["text"]) - elif ty == "patchouli:image": - with out.pair_tag("p", clazz="img-wrapper"): - for img in page["images"]: - modid, coords = img.split(":") - out.empty_pair_tag("img", src=f"{repo_names[modid]}/assets/{modid}/{coords}") - if "text" in page: write_block(out, page["text"]) - elif ty == "hexcasting:crafting_multi": - recipes = page["item_name"] - with out.pair_tag("blockquote", clazz="crafting-info"): - out.text(f"Depicted in the book: Several crafting recipes, for the ") - with out.pair_tag("code"): out.text(recipes[0]) - for i in recipes[1:]: - out.text(", ") - with out.pair_tag("code"): out.text(i) - out.text(".") - if "text" in page: write_block(out, page["text"]) - elif ty == "hexcasting:brainsweep": - with out.pair_tag("blockquote", clazz="crafting-info"): - out.text(f"Depicted in the book: A mind-flaying recipe producing the ") - with out.pair_tag("code"): out.text(page["output_name"]) - out.text(".") - if "text" in page: write_block(out, page["text"]) - elif ty in ("hexcasting:pattern", "hexcasting:manual_pattern_nosig", "hexcasting:manual_pattern"): - if "name" in page: - with out.pair_tag("h4", clazz="pattern-title"): - inp = page.get("input", None) or "" - oup = page.get("output", None) or "" - pipe = f"{inp} \u2192 {oup}".strip() - suffix = f" ({pipe})" if inp or oup else "" - out.text(f"{page['name']}{suffix}") - if anchor_id: - permalink(out, "#" + anchor_id) - with out.pair_tag("details", clazz="spell-collapsible"): - out.empty_pair_tag("summary", clazz="collapse-spell") - for string, start_angle, per_world in page["op"]: - with out.pair_tag("canvas", clazz="spell-viz", width=216, height=216, data_string=string, data_start=start_angle.lower(), data_per_world=per_world): - out.text("Your browser does not support visualizing patterns. Pattern code: " + string) - write_block(out, page["text"]) - else: - with out.pair_tag("p", clazz="todo-note"): - out.text("TODO: Missing processor for type: " + ty) - if "text" in page: - write_block(out, page["text"]) - out.tag("br") - -def write_entry(out, book, entry): - with out.pair_tag("div", id=entry["id"]): - with out.pair_tag_if(entry_spoilered(book, entry), "div", clazz="spoilered"): - with out.pair_tag("h3", clazz="entry-title page-header"): - write_block(out, entry["name"]) - anchor_toc(out) - permalink(out, "#" + entry["id"]) - for page in entry["pages"]: - write_page(out, entry["id"], page) - -def write_category(out, book, category): - with out.pair_tag("section", id=category["id"]): - with out.pair_tag_if(category_spoilered(book, category), "div", clazz="spoilered"): - with out.pair_tag("h2", clazz="category-title page-header"): - write_block(out, category["name"]) - anchor_toc(out) - permalink(out, "#" + category["id"]) - write_block(out, category["description"]) - for entry in category["entries"]: - if entry["id"] not in book["blacklist"]: - write_entry(out, book, entry) - -def write_toc(out, book): - with out.pair_tag("h2", id="table-of-contents", clazz="page-header"): - out.text("Table of Contents") - with out.pair_tag("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") - permalink(out, "#table-of-contents") - for category in book["categories"]: - with out.pair_tag("details", clazz="toc-category"): - with out.pair_tag("summary"): - with out.pair_tag("a", href="#" + category["id"], clazz="spoilered" if category_spoilered(book, category) else ""): - out.text(category["name"]) - with out.pair_tag("ul"): - for entry in category["entries"]: - with out.pair_tag("li"): - with out.pair_tag("a", href="#" + entry["id"], clazz="spoilered" if entry_spoilered(book, entry) else ""): - out.text(entry["name"]) - -def write_book(out, book): - with out.pair_tag("div", clazz="container"): - with out.pair_tag("header", clazz="jumbotron"): - with out.pair_tag("h1", clazz="book-title"): - write_block(out, book["name"]) - write_block(out, book["landing_text"]) - with out.pair_tag("nav"): - write_toc(out, book) - with out.pair_tag("main", clazz="book-body"): - for category in book["categories"]: - write_category(out, book, category) - -def main(argv): - if len(argv) < 5: - print(f"Usage: {argv[0]}