diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 000737ec5..0fe96b006 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.19' + 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/en_us.json b/Common/src/main/resources/assets/hexcasting/lang/en_us.json index 8690ad72e..866ae6591 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/en_us.json +++ b/Common/src/main/resources/assets/hexcasting/lang/en_us.json @@ -644,7 +644,7 @@ "hexcasting.page.101.4.header": "An Example", "hexcasting.page.101.4": "It's interesting to note that the $(italic)rotation/$ of a pattern doesn't seem to matter at all. These two patterns both perform an action called $(l:patterns/basics#hexcasting:get_caster)$(action)Mind's Reflection/$, for example.", "hexcasting.page.101.5": "A _Hex is cast by drawing (valid) actions in sequence. Each action might do one of a few things:$(li)Gather some information about the environment, leaving it on the top of the stack;$(li)manipulate the info gathered (e.g. adding two numbers); or$(li)perform some magical effect, like summoning lightning or an explosion. (These actions are called \"spells.\")$(p)When I start casting a _Hex, it creates an empty stack. Actions manipulate the top of that stack.", - "hexcasting.page.101.6": "For example, $(l:patterns/basics#hexcasting:get_caster)$(action)Mind's Reflection/$ will create an iota representing $(italic)me/$, the caster, and add it to the top of the stack. $(l:patterns/basics#hexcasting:get_entity_pos)$(action)Compass Purification/$ will take the iota at the top the stack, if it represents an entity, and transform it into an iota representing that entity's location.$(br2)So, drawing those patterns in that order would result in an iota on the stack representing my position.", + "hexcasting.page.101.6": "For example, $(l:patterns/basics#hexcasting:get_caster)$(action)Mind's Reflection/$ will create an iota representing $(italic)me/$, the caster, and add it to the top of the stack. $(l:patterns/basics#hexcasting:entity_pos/eye)$(action)Compass Purification/$ will take the iota at the top the stack, if it represents an entity, and transform it into an iota representing that entity's location.$(br2)So, drawing those patterns in that order would result in an iota on the stack representing my position.", "hexcasting.page.101.7": "$(thing)Iotas/$ can represent things like myself or my position, but there are several other types I can manipulate with $(thing)Actions/$. Here's a comprehensive list:$(li)Numbers (which some legends called \"doubles\");$(li)Vectors, a collection of three numbers representing a position, movement, or direction in the world;$(li)Booleans or \"bools\" for short, representing an abstract True or False,", "hexcasting.page.101.8": "$(li)Entities, like myself, chickens, and minecarts;$(li)Influences, peculiar types of iota that seem to represent abstract ideas;$(li)Patterns themselves, used for crafting magic items and truly mind-boggling feats like $(italic)spells that cast other spells/$; and$(li)A list of several of the above, gathered into a single iota.", "hexcasting.page.101.9": "Of course, there's no such thing as a free lunch. All spells, and certain other actions, require _media as payment.$(br2)The best I can figure, a _Hex is a little bit like a plan of action presented to Nature-- in this analogy, the _media is used to provide the arguments to back it up, so Nature will accept your plan and carry it out.", @@ -864,7 +864,7 @@ "hexcasting.page.basics_pattern.get_entity_look": "Transforms an entity on the stack into the direction it's looking in, as a unit vector.", "hexcasting.page.basics_pattern.print": "Displays the top iota of the stack to me.", "hexcasting.page.basics_pattern.raycast.1": "Combines two vectors (a position and a direction) into the answer to the question: If I stood at the position and looked in the direction, what block would I be looking at? Costs a negligible amount of _media.", - "hexcasting.page.basics_pattern.raycast.2": "If it doesn't hit anything, the vectors will combine into $(l:casting/influences)$(thing)Null/$.$(br2)A common sequence of patterns, the so-called \"raycast mantra,\" is $(action)Mind's Reflection/$, $(action)Compass Purification/$, $(action)Mind's Reflection/$, $(action)Alidade Purification/$, $(action)Archer's Distillation/$. Together, they return the vector position of the block I am looking at.", + "hexcasting.page.basics_pattern.raycast.2": "If it doesn't hit anything, the vectors will combine into $(l:casting/influences)$(thing)Null/$.$(br2)A common sequence of patterns, the so-called \"raycast mantra,\" is $(l:patterns/basics#hexcasting:get_caster)$(action)Mind's Reflection/$, $(l:patterns/basics#hexcasting:entity_pos/eye)$(action)Compass Purification/$, $(l:patterns/basics#hexcasting:get_caster)$(action)Mind's Reflection/$, $(l:patterns/basics#hexcasting:get_entity_look)$(action)Alidade Purification/$, $(l:patterns/basics#hexcasting:raycast)$(action)Archer's Distillation/$. Together, they return the vector position of the block I am looking at.", "hexcasting.page.basics_pattern.raycast/axis.1": "Like $(l:patterns/basics#hexcasting:raycast)$(action)Archer's Distillation/$, but instead returns a vector representing the answer to the question: Which $(italic)side/$ of the block am I looking at? Costs a negligible amount of _media.", "hexcasting.page.basics_pattern.raycast/axis.2": "More specifically, it returns the $(italic)normal vector/$ of the face hit, or a unit vector pointing perpendicular to the face.$(li)If I am looking at a floor, it will return (0, 1, 0).$(li)If I am looking at the south face of a block, it will return (0, 0, 1).", "hexcasting.page.basics_pattern.raycast/entity": "Like $(l:patterns/basics#hexcasting:raycast)$(action)Archer's Distillation/$, but instead returns the $(italic)entity/$ I am looking at. Costs a negligible amount of _media.", @@ -951,7 +951,7 @@ "hexcasting.page.stackmanip.stack_len": "Pushes the size of the stack as a number to the top of the stack. (For example, a stack of [0, 1] will become [0, 1, 2].)", "hexcasting.page.stackmanip.duplicate_n": "Removes the number at the top of the stack, then copies the top iota of the stack that number of times. (A count of 2 results in two of the iota on the stack, not three.)", "hexcasting.page.stackmanip.fisherman": "Grabs the element in the stack indexed by the number and brings it to the top.", - "hexcasting.page.stackmanip.fisherman/copy": "Like $(action)Fisherman's Gambit/$, but instead of moving the iota, copies it.", + "hexcasting.page.stackmanip.fisherman/copy": "Like $(l:patterns/stackmanip#hexcasting:fisherman)$(action)Fisherman's Gambit/$, but instead of moving the iota, copies it.", "hexcasting.page.stackmanip.mask.1": "An infinite family of actions that keep or remove elements at the top of the stack based on the sequence of dips and lines.", "hexcasting.page.stackmanip.mask.2": "Assuming that I draw a Bookkeeper's Gambit pattern left-to-right, the number of iotas the action will require is determined by the horizontal distance covered by the pattern. From deepest in the stack to shallowest, a flat line will keep the iota, whereas a triangle dipping down will remove it.$(br2)If my stack contains $(italic)0, 1, 2/$ from deepest to shallowest, drawing the first pattern opposite will give me $(italic)1/$, the second will give me $(italic)0/$, and the third will give me $(italic)0, 2/$ (the 0 at the bottom is left untouched).", "hexcasting.page.stackmanip.swizzle.1": "Rearranges the top elements of the stack based on the given numerical code, which is the index of the permutation wanted.", @@ -959,7 +959,7 @@ "hexcasting.page.stackmanip.swizzle.link": "Table of Codes", "hexcasting.entry.logic": "Logical Operators", - "hexcasting.page.logic.bool_coerce": "Convert an argument to a boolean. The number $(thing)0/$, $(l:influences#null)$(thing)Null/$, and the empty list become False; everything else becomes True.", + "hexcasting.page.logic.bool_coerce": "Convert an argument to a boolean. The number $(thing)0/$, $(l:casting/influences)$(thing)Null/$, and the empty list become False; everything else becomes True.", "hexcasting.page.logic.not": "If the argument is True, return False; if it is False, return True.", "hexcasting.page.logic.or": "Returns True if at least one of the arguments are True; otherwise returns False.", "hexcasting.page.logic.and": "Returns True if both arguments are true; otherwise returns False.", 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..277068e19 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json +++ b/Common/src/main/resources/assets/hexcasting/lang/ru_ru.json @@ -173,7 +173,7 @@ "hexcasting.subtitles.impetus.cleric.register": "Cleric Impetus dings", "hexcasting.spell.hexcasting:get_caster": "Зеркало нарцисса", - "hexcasting.spell.hexcasting:get_entity_pos": "Положение сущности", + "hexcasting.spell.hexcasting:entity_pos/eye": "Положение сущности", "hexcasting.spell.hexcasting:get_entity_look": "Взгляд алидады", "hexcasting.spell.hexcasting:get_entity_height": "Высота сущности", "hexcasting.spell.hexcasting:get_entity_velocity": "Скорость сущности", @@ -449,7 +449,7 @@ "hexcasting.page.101.4.header": "Пример", "hexcasting.page.101.4": "Следует заметить что $(italic)направление/$ этих рун не важно. Обе этих руны исполняют действие $(l:patterns/basics#hexcasting:get_caster)$(action)Зеркало нарцисса/$", "hexcasting.page.101.5": "Заклинания исполняются написанием (верных) рун по очереди. Each action might do one of a few things:$(li)Gather some information about the environment, leaving it on the top of the stack;$(li)manipulate the info gathered (e.g. adding two numbers); or$(li)perform some magical effect, like summoning lightning or an explosion. (These actions are called \"spells.\")$(p)When I start casting a _Hex, it creates an empty stack. Actions manipulate the top of that stack.", - "hexcasting.page.101.6": "$(l:patterns/basics#hexcasting:get_caster)$(action)Зеркало нарцисса/$ поместит на верх стэка информацию об игроке, который исполняет заклинание. $(l:patterns/basics#hexcasting:get_entity_pos)$(action)Положение сущности/$ возьмёт информацию о сущности с верха стэка и вернёт её положение на верх стэка. $(br2)Так что написание рун в этом порядке положит на верх стэка моё положение.", + "hexcasting.page.101.6": "$(l:patterns/basics#hexcasting:get_caster)$(action)Зеркало нарцисса/$ поместит на верх стэка информацию об игроке, который исполняет заклинание. $(l:patterns/basics#hexcasting:entity_pos/eye)$(action)Положение сущности/$ возьмёт информацию о сущности с верха стэка и вернёт её положение на верх стэка. $(br2)Так что написание рун в этом порядке положит на верх стэка моё положение.", "hexcasting.page.101.7": "$(thing)Информация/$ может представлять такие вещи как я или моя позиция, но есть другие типы которыми я могу управлять с помощью $(thing)Действий$(0). Список типов информации:$(li)Числа (\"doubles\");$(li)Вектора (\"vector\") 3 числа представляющие позицию или направление", "hexcasting.page.101.8": "$(li)Сущности, как я, куры, вагонетки, валяющиеся предметы;$(li)Абстракции;$(li)Очередь из рун, использующаяся для создания магических предметов и мозговыносящих заклинаний как $(italic)заклинания, которые вызывают другие/$; И$(li)Очередь с любыми типами информации", "hexcasting.page.101.9": "Естественно все заклинания тратят Мысли в качестве оплаты.$(br2)Как я понимаю заклинания это план действий, представленный Природе - в этой аналогии Мысли используются для соединения с Природой, передачи ей информации.", @@ -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/assets/hexcasting/lang/zh_cn.json b/Common/src/main/resources/assets/hexcasting/lang/zh_cn.json index 56d1e7d56..106376e92 100644 --- a/Common/src/main/resources/assets/hexcasting/lang/zh_cn.json +++ b/Common/src/main/resources/assets/hexcasting/lang/zh_cn.json @@ -644,7 +644,7 @@ "hexcasting.page.101.4.header": "示例", "hexcasting.page.101.4": "有意思的是,图案整体的$(italic)方向/$完全不影响图案的功用。例如,上图中的两个图案都会执行名为$(l:patterns/basics#hexcasting:get_caster)$(action)意识之精思/$的操作。", "hexcasting.page.101.5": "$(hex)咒术/$是通过按顺序绘制(有效的)图案施放的。每个操作都能完成如下几件事中的一件:$(li)获取有关环境的信息,然后将其置于栈顶。$(li)操控获取到的信息(例如加和两个数)。或者$(li)产生魔法效果,例如召唤闪电或产生爆炸。(这些操作被称为“法术”。)$(p)在开始施放$(hex)咒术/$时会自动创建一个空栈。操作会影响栈顶的若干元素。", - "hexcasting.page.101.6": "例如,$(l:patterns/basics#hexcasting:get_caster)$(action)意识之精思/$会创建一个代表$(italic)我/$,即施法者的 iota,并将其置于栈顶。$(l:patterns/basics#hexcasting:get_entity_pos)$(action)指南针之纯化/$会接受栈顶的 iota(前提是该 iota 代表一个实体),并将其转换为代表该实体位置的 iota。$(br2)所以,依序绘制上述图案就会在栈顶创建一个代表我的位置的 iota。", + "hexcasting.page.101.6": "例如,$(l:patterns/basics#hexcasting:get_caster)$(action)意识之精思/$会创建一个代表$(italic)我/$,即施法者的 iota,并将其置于栈顶。$(l:patterns/basics#hexcasting:entity_pos/eye)$(action)指南针之纯化/$会接受栈顶的 iota(前提是该 iota 代表一个实体),并将其转换为代表该实体位置的 iota。$(br2)所以,依序绘制上述图案就会在栈顶创建一个代表我的位置的 iota。", "hexcasting.page.101.7": "$(thing)Iota/$ 可以代表诸如我、我的位置等具体事物,也可以代表其他几种能用$(thing)操作/$进行操控的事物。如下是一份全面介绍:$(li)数(某些文献称之为“双精度浮点数”)。$(li)向量,一种可代表世界中位置、运动或方向的,由三个数组成的集合。$(li)布尔值(简称“布尔”),一种代表真和假的抽象概念的的 iota。", "hexcasting.page.101.8": "$(li)实体,如我自己、鸡、矿车等。$(li)虚指,一种代表抽象概念的奇怪的 iota。$(li)图案,用于制作魔法物品,也用在某些烧脑的咒术中,如$(italic)能施放其他法术的法术/$。还有$(li)列表,可由上述任意类型的 iota 构成的列表,其本身相当于一个 iota。", "hexcasting.page.101.9": "当然,天上不会掉馅饼。所有法术和一些操作会消耗$(media)媒质/$。$(br2)我个人认为,$(hex)咒术/$有点像呈现在自然面前的一个对各类操作的规划。要这么类比的话,$(media)媒质/$便是用来为这个规划提供各式参数和运行支持的能源,而自然会接受你的规划并付诸实践。", @@ -959,7 +959,7 @@ "hexcasting.page.stackmanip.swizzle.link": "编码列表", "hexcasting.entry.logic": "逻辑运算", - "hexcasting.page.logic.bool_coerce": "将参数变换为布尔值。数 $(thing)0/$、$(l:influences#null)$(thing)Null/$,以及空列表会变为 False。其余所有则变为 True。", + "hexcasting.page.logic.bool_coerce": "将参数变换为布尔值。数 $(thing)0/$、$(l:casting/influences)$(thing)Null/$,以及空列表会变为 False。其余所有则变为 True。", "hexcasting.page.logic.not": "如果参数是 True,返回 False;如果参数是 False,返回 True。", "hexcasting.page.logic.or": "如果至少有一个参数是 True,返回 True。否则返回 False。", "hexcasting.page.logic.and": "如果两个参数都是 True,返回 True。否则返回 False。", 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 b8ea6da22..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).replace("%%", "%") - -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]}