Jinja, table of contents
This commit is contained in:
parent
276ca5b73f
commit
9059cfe6a5
14 changed files with 586 additions and 422 deletions
|
@ -2,7 +2,6 @@
|
|||
|
||||
modid = "hexcasting"
|
||||
book_name = "thehexbook"
|
||||
template = "template.html"
|
||||
is_0_black = false
|
||||
|
||||
recipe_dirs = [
|
||||
|
@ -11,6 +10,10 @@ recipe_dirs = [
|
|||
]
|
||||
default_recipe_dir = 0
|
||||
|
||||
# NOTE: _Raw means "don't apply variable interpolation to this value"
|
||||
pattern_regex = {_Raw='HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?'}
|
||||
|
||||
template = "main.html.jinja"
|
||||
spoilers = [
|
||||
"hexcasting:opened_eyes",
|
||||
"hexcasting:y_u_no_cast_angy",
|
||||
|
@ -18,9 +21,14 @@ spoilers = [
|
|||
]
|
||||
blacklist = []
|
||||
|
||||
# NOTE: _Raw means "don't apply variable interpolation to this value"
|
||||
# more on that later
|
||||
pattern_regex = {_Raw='HexPattern\.fromAngles\("([qweasd]+)", HexDir\.(\w+)\),\s*modLoc\("([^"]+)"\)([^;]*true\);)?'}
|
||||
[template_args]
|
||||
title = "Hex Book"
|
||||
mod_name = "Hexcasting"
|
||||
author = "petrak@, Alwinfy"
|
||||
description = "The Hex Book, all in one place."
|
||||
icon_href = "icon.png"
|
||||
is_bleeding_edge = true
|
||||
show_landing_text = true
|
||||
|
||||
[base_asset_urls]
|
||||
hexcasting = "https://raw.githubusercontent.com/gamma-delta/HexMod/main/Common/src/main/resources"
|
||||
|
|
|
@ -39,19 +39,21 @@ class I18nProps(HexDocModel[Any]):
|
|||
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."""
|
||||
|
||||
spoilers: set[ResourceLocation]
|
||||
blacklist: set[ResourceLocation]
|
||||
|
||||
recipe_dirs: list[Path]
|
||||
default_recipe_dir_index_: int = Field(alias="default_recipe_dir")
|
||||
|
||||
pattern_regex: re.Pattern[str]
|
||||
|
||||
template: str
|
||||
spoilers: set[ResourceLocation]
|
||||
blacklist: set[ResourceLocation]
|
||||
|
||||
template_args: dict[str, Any]
|
||||
|
||||
base_asset_urls: dict[str, NoTrailingSlashHttpUrl]
|
||||
"""Mapping from modid to the url of that mod's `resources` directory on GitHub."""
|
||||
|
||||
|
|
53
doc/src/common/templates.py
Normal file
53
doc/src/common/templates.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
from html import escape
|
||||
from typing import Any
|
||||
|
||||
from jinja2 import nodes
|
||||
from jinja2.ext import Extension
|
||||
from jinja2.parser import Parser
|
||||
from markupsafe import Markup
|
||||
|
||||
from minecraft.i18n import LocalizedStr
|
||||
from patchouli.text.formatting import FormatTree
|
||||
from patchouli.text.html import HTMLStream
|
||||
|
||||
|
||||
# https://stackoverflow.com/a/64392515
|
||||
class IncludeRawExtension(Extension):
|
||||
tags = {"include_raw"}
|
||||
|
||||
def parse(self, parser: Parser) -> nodes.Node:
|
||||
lineno = parser.stream.expect("name:include_raw").lineno
|
||||
template = parser.parse_expression()
|
||||
result = self.call_method("_render", [template], lineno=lineno)
|
||||
return nodes.Output([result], lineno=lineno)
|
||||
|
||||
def _render(self, filename: str) -> Markup:
|
||||
assert self.environment.loader is not None
|
||||
source = self.environment.loader.get_source(self.environment, filename)
|
||||
return Markup(source[0])
|
||||
|
||||
|
||||
def hexdoc_minify(value: str) -> str:
|
||||
return "".join(line.strip() for line in value.splitlines())
|
||||
# return minify_html.minify(
|
||||
# code=value,
|
||||
# keep_closing_tags=True,
|
||||
# keep_html_and_head_opening_tags=True,
|
||||
# keep_spaces_between_attributes=True,
|
||||
# ensure_spec_compliant_unquoted_attribute_values=True,
|
||||
# )
|
||||
|
||||
|
||||
def hexdoc_block(value: Any) -> str:
|
||||
match value:
|
||||
case LocalizedStr() | str():
|
||||
lines = str(value).splitlines()
|
||||
return "<br />".join(escape(line) for line in lines)
|
||||
case FormatTree():
|
||||
with HTMLStream() as out:
|
||||
with value.style.element(out):
|
||||
for child in value.children:
|
||||
out.write(hexdoc_block(child))
|
||||
return out.getvalue()
|
||||
case _:
|
||||
raise TypeError(value)
|
|
@ -4,13 +4,15 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
||||
|
||||
# from jinja2.sandbox import SandboxedEnvironment
|
||||
from tap import Tap
|
||||
|
||||
from common.properties import Properties
|
||||
from common.templates import IncludeRawExtension, hexdoc_block, hexdoc_minify
|
||||
from hexcasting.hex_book import HexBook
|
||||
|
||||
from .collate_data import generate_docs
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
raise RuntimeError("Minimum Python version: 3.11")
|
||||
|
||||
|
@ -31,9 +33,30 @@ def main(args: Args) -> None:
|
|||
props = Properties.load(args.properties_file)
|
||||
book = HexBook.load(*HexBook.prepare(props))
|
||||
|
||||
# load and fill the template
|
||||
template = props.template.read_text("utf-8")
|
||||
docs = generate_docs(book, template)
|
||||
# set up Jinja environment
|
||||
# TODO: SandboxedEnvironment
|
||||
env = Environment(
|
||||
# TODO: ChoiceLoader w/ ModuleLoader, but we need the directory restructure
|
||||
loader=FileSystemLoader("./templates"),
|
||||
undefined=StrictUndefined,
|
||||
lstrip_blocks=True,
|
||||
trim_blocks=True,
|
||||
autoescape=False,
|
||||
extensions=[IncludeRawExtension],
|
||||
)
|
||||
env.filters["hexdoc_minify"] = hexdoc_minify
|
||||
env.filters["hexdoc_block"] = hexdoc_block
|
||||
|
||||
# load and render template
|
||||
template = env.get_template(props.template)
|
||||
docs = template.render(
|
||||
props.template_args
|
||||
| {
|
||||
"book": book,
|
||||
"spoilers": props.spoilers,
|
||||
"blacklist": props.blacklist,
|
||||
}
|
||||
)
|
||||
|
||||
# if there's an output file specified, write to it
|
||||
# otherwise just print the generated docs
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from pathlib import Path
|
||||
from typing import Self
|
||||
from typing import Self, cast
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import Field, FieldValidationInfo, field_validator
|
||||
|
||||
from common.properties import Properties
|
||||
from common.types import Sortable, sorted_dict
|
||||
|
@ -74,6 +74,10 @@ class Category(BookModelFile[BookContext, BookContext], Sortable):
|
|||
# implement BookModelFile
|
||||
return props.categories_dir
|
||||
|
||||
@property
|
||||
def is_spoiler(self) -> bool:
|
||||
return all(entry.is_spoiler for entry in self.entries)
|
||||
|
||||
@property
|
||||
def _is_cmp_key_ready(self) -> bool:
|
||||
return self.parent_id is None or self.parent_cmp_key_ is not None
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic import (
|
||||
Field,
|
||||
FieldValidationInfo,
|
||||
ValidationInfo,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
from common.properties import Properties
|
||||
from common.types import Color, Sortable
|
||||
|
@ -17,6 +24,8 @@ class Entry(BookModelFile[BookContext, BookContext], Sortable):
|
|||
See: https://vazkiimods.github.io/Patchouli/docs/reference/entry-json
|
||||
"""
|
||||
|
||||
is_spoiler: bool = False
|
||||
|
||||
# required (entry.json)
|
||||
name: LocalizedStr
|
||||
category_id: ResourceLocation = Field(alias="category")
|
||||
|
@ -43,3 +52,12 @@ class Entry(BookModelFile[BookContext, BookContext], Sortable):
|
|||
# implement Sortable
|
||||
# note: python sorts false before true, so we invert priority
|
||||
return (not self.priority, self.sortnum, self.name)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_is_spoiler(self, info: ValidationInfo):
|
||||
context = cast(BookContext | None, info.context)
|
||||
if not context or self.advancement is None:
|
||||
return self
|
||||
|
||||
self.is_spoiler = self.advancement in context["props"].spoilers
|
||||
return self
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# TODO: type
|
||||
import io
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import dataclass
|
||||
from html import escape
|
||||
|
@ -27,17 +28,14 @@ class HTMLElement:
|
|||
self.stream.write(f"</{self.name}>")
|
||||
|
||||
|
||||
@dataclass
|
||||
class HTMLStream:
|
||||
stream: IO[str]
|
||||
|
||||
class HTMLStream(io.StringIO):
|
||||
def void_element(self, name: str, **kwargs: Any):
|
||||
"""Like `<img />`."""
|
||||
keywords = attributes_to_str(kwargs)
|
||||
self.stream.write(f"<{name}{keywords} />")
|
||||
self.write(f"<{name}{keywords} />")
|
||||
|
||||
def element(self, name: str, /, **kwargs: Any):
|
||||
return HTMLElement(self.stream, name, kwargs)
|
||||
return HTMLElement(self, name, kwargs)
|
||||
|
||||
def element_if(self, condition: bool, name: str, /, **kwargs: Any):
|
||||
if condition:
|
||||
|
@ -49,5 +47,5 @@ class HTMLStream:
|
|||
pass
|
||||
|
||||
def text(self, txt: str | LocalizedStr):
|
||||
self.stream.write(escape(str(txt)))
|
||||
self.write(escape(str(txt)))
|
||||
return self
|
||||
|
|
|
@ -1,399 +0,0 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="The Hex Book, all in one place.">
|
||||
<meta name="author" content="petrak@, Alwinfy">
|
||||
<link rel="icon" href="icon.png">
|
||||
|
||||
<title>Hex Book</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
summary { display: list-item; }
|
||||
|
||||
details.spell-collapsible {
|
||||
display: inline-block;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
padding: .5em .5em 0;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
summary.collapse-spell {
|
||||
font-weight: bold;
|
||||
margin: -.5em -.5em 0;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
details.spell-collapsible[open] {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
details[open] summary.collapse-spell {
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
details .collapse-spell::before {
|
||||
content: "Click to show spell";
|
||||
}
|
||||
details[open] .collapse-spell::before {
|
||||
content: "Click to hide spell";
|
||||
}
|
||||
blockquote.crafting-info {
|
||||
font-size: inherit;
|
||||
}
|
||||
a.permalink {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
a.permalink:hover {
|
||||
color: lightgray;
|
||||
}
|
||||
p {
|
||||
margin: 0.5ex 0;
|
||||
}
|
||||
p.fake-li {
|
||||
margin: 0;
|
||||
}
|
||||
p.fake-li::before {
|
||||
content: "\2022";
|
||||
margin: 1ex;
|
||||
}
|
||||
.linkout::before {
|
||||
content: "Link: ";
|
||||
}
|
||||
p.todo-note {
|
||||
font-style: italic;
|
||||
color: lightgray;
|
||||
}
|
||||
.obfuscated {
|
||||
filter: blur(1em);
|
||||
}
|
||||
.spoilered {
|
||||
filter: blur(1ex);
|
||||
-moz-transition: filter 0.04s linear;
|
||||
}
|
||||
.spoilered:hover {
|
||||
filter: blur(0.5ex);
|
||||
}
|
||||
.spoilered.unspoilered {
|
||||
filter: blur(0);
|
||||
}
|
||||
canvas.spell-viz {
|
||||
--dot-color: #777f;
|
||||
--dot-color: #777f;
|
||||
--start-dot-color: #f009;
|
||||
--moving-dot-color: #0fa9;
|
||||
|
||||
--path-color: darkgray;
|
||||
--visited-path-color: #0c8;
|
||||
|
||||
--dot-scale: 0.0625;
|
||||
--moving-dot-scale: 0.125;
|
||||
--line-scale: 0.08333;
|
||||
--pausetext-scale: 0.5;
|
||||
--dark-mode: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #201a20;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: #323;
|
||||
}
|
||||
|
||||
canvas.spell-viz {
|
||||
/* hack */
|
||||
--dark-mode: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<noscript>
|
||||
<style>
|
||||
/* for accessibility */
|
||||
.spoilered {
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</noscript>
|
||||
<script>
|
||||
"use strict";
|
||||
const speeds = [0, 0.25, 0.5, 1, 2, 4];
|
||||
const scrollThreshold = 100;
|
||||
const rfaQueue = [];
|
||||
const colorCache = new Map();
|
||||
function getColorRGB(ctx, str) {
|
||||
if (!colorCache.has(str)) {
|
||||
ctx.fillStyle = str;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const imgData = ctx.getImageData(0, 0, 1, 1);
|
||||
colorCache.set(str, imgData.data);
|
||||
}
|
||||
return colorCache.get(str);
|
||||
}
|
||||
function startAngle(str) {
|
||||
switch (str) {
|
||||
case "east": return 0;
|
||||
case "north_east": return 1;
|
||||
case "north_west": return 2;
|
||||
case "west": return 3;
|
||||
case "south_west": return 4;
|
||||
case "south_east": return 5;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
function offsetAngle(str) {
|
||||
switch (str) {
|
||||
case "w": return 0;
|
||||
case "q": return 1;
|
||||
case "a": return 2;
|
||||
case "s": return 3;
|
||||
case "d": return 4;
|
||||
case "e": return 5;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
function initializeElem(canvas) {
|
||||
const str = canvas.dataset.string;
|
||||
let angle = startAngle(canvas.dataset.start);
|
||||
const perWorld = canvas.dataset.perWorld === "True";
|
||||
|
||||
// build geometry
|
||||
const points = [[0, 0]];
|
||||
let lastPoint = points[0];
|
||||
let minPoint = lastPoint, maxPoint = lastPoint;
|
||||
for (const ch of "w" + str) {
|
||||
const addAngle = offsetAngle(ch);
|
||||
if (addAngle < 0) continue;
|
||||
angle = (angle + addAngle) % 6;
|
||||
const trueAngle = Math.PI / 3 * angle;
|
||||
|
||||
const [lx, ly] = lastPoint;
|
||||
const newPoint = [lx + Math.cos(trueAngle), ly - Math.sin(trueAngle)];
|
||||
|
||||
points.push(newPoint);
|
||||
lastPoint = newPoint;
|
||||
|
||||
const [mix, miy] = minPoint;
|
||||
minPoint = [Math.min(mix, newPoint[0]), Math.min(miy, newPoint[1])];
|
||||
const [max, may] = maxPoint;
|
||||
maxPoint = [Math.max(max, newPoint[0]), Math.max(may, newPoint[1])];
|
||||
}
|
||||
const size = Math.min(canvas.width, canvas.height) * 0.8;
|
||||
const scale = size / Math.max(3, Math.max(maxPoint[1] - minPoint[1], maxPoint[0] - minPoint[0]));
|
||||
const center = [(minPoint[0] + maxPoint[0]) * 0.5, (minPoint[1] + maxPoint[1]) * 0.5];
|
||||
const truePoints = points.map(p => [canvas.width * 0.5 + scale * (p[0] - center[0]), canvas.height * 0.5 + scale * (p[1] - center[1])]);
|
||||
let uniqPoints = [];
|
||||
l1: for (const point of truePoints) {
|
||||
for (const pt of uniqPoints) {
|
||||
if (Math.abs(point[0] - pt[0]) < 0.00001 && Math.abs(point[1] - pt[1]) < 0.00001) {
|
||||
continue l1;
|
||||
}
|
||||
}
|
||||
uniqPoints.push(point);
|
||||
}
|
||||
|
||||
// rendering code
|
||||
const speed = 0.0025;
|
||||
const context = canvas.getContext("2d");
|
||||
const negaProgress = -3;
|
||||
let progress = 0;
|
||||
let scrollTimeout = 1e309;
|
||||
let speedLevel = 3;
|
||||
let speedIncrement = 0;
|
||||
function speedScale() {
|
||||
return speeds[speedLevel];
|
||||
}
|
||||
|
||||
const style = getComputedStyle(canvas);
|
||||
const getProp = n => style.getPropertyValue(n);
|
||||
|
||||
const tick = dt => {
|
||||
scrollTimeout += dt;
|
||||
if (canvas.offsetParent === null) return;
|
||||
|
||||
const strokeStyle = getProp("--path-color");
|
||||
const strokeVisitedStyle = getProp("--visited-path-color");
|
||||
|
||||
const startDotStyle = getProp("--start-dot-color");
|
||||
const dotStyle = getProp("--dot-color");
|
||||
const movDotStyle = getProp("--moving-dot-color");
|
||||
|
||||
const strokeWidth = scale * +getProp("--line-scale");
|
||||
const dotRadius = scale * +getProp("--dot-scale");
|
||||
const movDotRadius = scale * +getProp("--moving-dot-scale");
|
||||
const pauseScale = scale * +getProp("--pausetext-scale");
|
||||
const bodyBg = scale * +getProp("--pausetext-scale");
|
||||
const darkMode = +getProp("--dark-mode");
|
||||
const bgColors = getColorRGB(context, getComputedStyle(document.body).backgroundColor);
|
||||
|
||||
|
||||
if (!perWorld) {
|
||||
progress += speed * dt * (progress > 0 ? speedScale() : Math.sqrt(speedScale()));
|
||||
}
|
||||
if (progress >= truePoints.length - 1) {
|
||||
progress = negaProgress;
|
||||
}
|
||||
let ix = Math.floor(progress), frac = progress - ix, core = null, fadeColor = 0;
|
||||
if (ix < 0) {
|
||||
const rawFade = 2 * progress / negaProgress - 1;
|
||||
fadeColor = 1 - Math.abs(rawFade);
|
||||
context.strokeStyle = rawFade > 0 ? strokeVisitedStyle : strokeStyle;
|
||||
ix = rawFade > 0 ? truePoints.length - 2 : 0;
|
||||
frac = +(rawFade > 0);
|
||||
} else {
|
||||
context.strokeStyle = strokeVisitedStyle;
|
||||
}
|
||||
|
||||
const [lx, ly] = truePoints[ix];
|
||||
const [rx, ry] = truePoints[ix + 1];
|
||||
core = [lx + (rx - lx) * frac, ly + (ry - ly) * frac];
|
||||
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.beginPath();
|
||||
context.lineWidth = strokeWidth;
|
||||
context.moveTo(truePoints[0][0], truePoints[0][1]);
|
||||
for (let i = 1; i < ix + 1; i++) {
|
||||
context.lineTo(truePoints[i][0], truePoints[i][1]);
|
||||
}
|
||||
context.lineTo(core[0], core[1]);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.strokeStyle = strokeStyle;
|
||||
context.moveTo(core[0], core[1]);
|
||||
for (let i = ix + 1; i < truePoints.length; i++) {
|
||||
context.lineTo(truePoints[i][0], truePoints[i][1]);
|
||||
}
|
||||
context.stroke();
|
||||
|
||||
for (let i = 0; i < uniqPoints.length; i++) {
|
||||
context.beginPath();
|
||||
context.fillStyle = (i == 0 && !perWorld) ? startDotStyle : dotStyle;
|
||||
const radius = (i == 0 && !perWorld) ? movDotRadius : dotRadius;
|
||||
context.arc(uniqPoints[i][0], uniqPoints[i][1], radius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
if (!perWorld) {
|
||||
context.beginPath();
|
||||
context.fillStyle = movDotStyle;
|
||||
context.arc(core[0], core[1], movDotRadius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
if (fadeColor) {
|
||||
context.fillStyle = `rgba(${bgColors[0]}, ${bgColors[1]}, ${bgColors[2]}, ${fadeColor})`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
if (scrollTimeout <= 2000) {
|
||||
context.fillStyle = `rgba(200, 200, 200, ${(2000 - scrollTimeout) / 1000})`;
|
||||
context.font = `${pauseScale}px sans-serif`;
|
||||
context.fillText(speedScale() ? speedScale() + "x" : "Paused", 0.2 * scale, canvas.height - 0.2 * scale);
|
||||
}
|
||||
};
|
||||
rfaQueue.push(tick);
|
||||
|
||||
// scrolling input
|
||||
if (!perWorld) {
|
||||
canvas.addEventListener("wheel", ev => {
|
||||
speedIncrement += ev.deltaY;
|
||||
const oldSpeedLevel = speedLevel;
|
||||
if (speedIncrement >= scrollThreshold) {
|
||||
speedLevel--;
|
||||
} else if (speedIncrement <= -scrollThreshold) {
|
||||
speedLevel++;
|
||||
}
|
||||
if (oldSpeedLevel != speedLevel) {
|
||||
speedIncrement = 0;
|
||||
speedLevel = Math.max(0, Math.min(speeds.length - 1, speedLevel));
|
||||
scrollTimeout = 0;
|
||||
}
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
function hookLoad(elem) {
|
||||
let init = false;
|
||||
const canvases = elem.querySelectorAll("canvas");
|
||||
elem.addEventListener("toggle", () => {
|
||||
if (!init) {
|
||||
canvases.forEach(initializeElem);
|
||||
init = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
function hookToggle(elem) {
|
||||
const details = Array.from(document.querySelectorAll("details." + elem.dataset.target));
|
||||
elem.addEventListener("click", () => {
|
||||
if (details.some(x => x.open)) {
|
||||
details.forEach(x => x.open = false);
|
||||
} else {
|
||||
details.forEach(x => x.open = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
function hookSpoiler(elem) {
|
||||
if (params.get("nospoiler") !== null) {
|
||||
elem.classList.add("unspoilered");
|
||||
} else {
|
||||
const thunk = ev => {
|
||||
if (!elem.classList.contains("unspoilered")) {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
elem.classList.add("unspoilered");
|
||||
}
|
||||
elem.removeEventListener("click", thunk);
|
||||
};
|
||||
elem.addEventListener("click", thunk);
|
||||
|
||||
if (elem instanceof HTMLAnchorElement) {
|
||||
const href = elem.getAttribute("href");
|
||||
if (href.startsWith("#")) {
|
||||
elem.addEventListener("click", () => document.getElementById(href.substring(1)).querySelector(".spoilered").classList.add("unspoilered"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('details.spell-collapsible').forEach(hookLoad);
|
||||
document.querySelectorAll('a.toggle-link').forEach(hookToggle);
|
||||
document.querySelectorAll('.spoilered').forEach(hookSpoiler);
|
||||
function tick(prevTime, time) {
|
||||
const dt = time - prevTime;
|
||||
for (const q of rfaQueue) {
|
||||
q(dt);
|
||||
}
|
||||
requestAnimationFrame(t => tick(time, t));
|
||||
}
|
||||
requestAnimationFrame(t => tick(t, t));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="margin-top: 3em;">
|
||||
<blockquote>
|
||||
<h1>This is the online version of the Hexcasting documentation.</h1>
|
||||
<p>Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for
|
||||
those.</p>
|
||||
<p>Additionally, this is built from the latest code on GitHub. It may describe <b>newer</b> features that you
|
||||
may not necessarily have, even on the latest CurseForge version!</p>
|
||||
<p><b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
|
||||
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
#DUMP_BODY_HERE
|
||||
</body>
|
||||
</html>
|
18
doc/templates/book.html.jinja
vendored
Normal file
18
doc/templates/book.html.jinja
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="container">
|
||||
{% if show_landing_text %}
|
||||
<header class="jumbotron">
|
||||
<h1 class="book-title">
|
||||
{{ book.name }}
|
||||
</h1>
|
||||
{{ book.landing_text | hexdoc_block }}
|
||||
</header>
|
||||
{% endif %}
|
||||
<nav>
|
||||
{% include "tableofcontents.html.jinja" %}
|
||||
</nav>
|
||||
<main class="book-body">
|
||||
{% for category in book.categories.values() %}
|
||||
{% include "category.html.jinja" %}
|
||||
{% endfor %}
|
||||
</main>
|
||||
</div>
|
1
doc/templates/category.html.jinja
vendored
Normal file
1
doc/templates/category.html.jinja
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
category goes here
|
11
doc/templates/macros.html.jinja
vendored
Normal file
11
doc/templates/macros.html.jinja
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% macro permalink(href) %}
|
||||
<a href="#{{ href }}" class="permalink small" title="Permalink">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro maybe_spoilered_link(value) %}
|
||||
<a href="#{{ value.id.path }}" class="{{ 'spoilered' if value.is_spoiler else '' }}">
|
||||
{{- value.name -}}
|
||||
</a>
|
||||
{%- endmacro %}
|
153
doc/templates/main.html.jinja
vendored
Normal file
153
doc/templates/main.html.jinja
vendored
Normal file
|
@ -0,0 +1,153 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="author" content="{{ author }}">
|
||||
<link rel="icon" href="{{ icon_href }}">
|
||||
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.4.1/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
summary { display: list-item; }
|
||||
|
||||
details.spell-collapsible {
|
||||
display: inline-block;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 4px;
|
||||
padding: .5em .5em 0;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
summary.collapse-spell {
|
||||
font-weight: bold;
|
||||
margin: -.5em -.5em 0;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
details.spell-collapsible[open] {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
details[open] summary.collapse-spell {
|
||||
border-bottom: 1px solid #aaa;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
details .collapse-spell::before {
|
||||
content: "Click to show spell";
|
||||
}
|
||||
details[open] .collapse-spell::before {
|
||||
content: "Click to hide spell";
|
||||
}
|
||||
blockquote.crafting-info {
|
||||
font-size: inherit;
|
||||
}
|
||||
a.permalink {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
a.permalink:hover {
|
||||
color: lightgray;
|
||||
}
|
||||
p {
|
||||
margin: 0.5ex 0;
|
||||
}
|
||||
p.fake-li {
|
||||
margin: 0;
|
||||
}
|
||||
p.fake-li::before {
|
||||
content: "\2022";
|
||||
margin: 1ex;
|
||||
}
|
||||
.linkout::before {
|
||||
content: "Link: ";
|
||||
}
|
||||
p.todo-note {
|
||||
font-style: italic;
|
||||
color: lightgray;
|
||||
}
|
||||
.obfuscated {
|
||||
filter: blur(1em);
|
||||
}
|
||||
.spoilered {
|
||||
filter: blur(1ex);
|
||||
-moz-transition: filter 0.04s linear;
|
||||
}
|
||||
.spoilered:hover {
|
||||
filter: blur(0.5ex);
|
||||
}
|
||||
.spoilered.unspoilered {
|
||||
filter: blur(0);
|
||||
}
|
||||
canvas.spell-viz {
|
||||
--dot-color: #777f;
|
||||
--dot-color: #777f;
|
||||
--start-dot-color: #f009;
|
||||
--moving-dot-color: #0fa9;
|
||||
|
||||
--path-color: darkgray;
|
||||
--visited-path-color: #0c8;
|
||||
|
||||
--dot-scale: 0.0625;
|
||||
--moving-dot-scale: 0.125;
|
||||
--line-scale: 0.08333;
|
||||
--pausetext-scale: 0.5;
|
||||
--dark-mode: 0;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #201a20;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: #323;
|
||||
}
|
||||
|
||||
canvas.spell-viz {
|
||||
/* hack */
|
||||
--dark-mode: 1;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<noscript>
|
||||
<style>
|
||||
/* for accessibility */
|
||||
.spoilered {
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</noscript>
|
||||
<script>
|
||||
{# TODO: kinda hacky #}
|
||||
{% include_raw "main.js" %}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="margin-top: 3em;">
|
||||
<blockquote>
|
||||
<h1>This is the online version of the {{ mod_name }} documentation.</h1>
|
||||
<p>Embedded images and patterns are included, but not crafting recipes or items. There's an in-game book for
|
||||
those.</p>
|
||||
{% if is_bleeding_edge %}
|
||||
<p>Additionally, this is built from the latest code on GitHub. It may describe <b>newer</b> features that you
|
||||
may not necessarily have, even on the latest CurseForge version!</p>
|
||||
{% endif %}
|
||||
<p><b>Entries which are blurred are spoilers</b>. Click to reveal them, but be aware that they may spoil endgame
|
||||
progression. Alternatively, click <a href="?nospoiler">here</a> to get a version with all spoilers showing.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
{% filter hexdoc_minify %}
|
||||
{% include "book.html.jinja" %}
|
||||
{% endfilter +%}
|
||||
</body>
|
||||
</html>
|
252
doc/templates/main.js
vendored
Normal file
252
doc/templates/main.js
vendored
Normal file
|
@ -0,0 +1,252 @@
|
|||
"use strict";
|
||||
const speeds = [0, 0.25, 0.5, 1, 2, 4];
|
||||
const scrollThreshold = 100;
|
||||
const rfaQueue = [];
|
||||
const colorCache = new Map();
|
||||
function getColorRGB(ctx, str) {
|
||||
if (!colorCache.has(str)) {
|
||||
ctx.fillStyle = str;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const imgData = ctx.getImageData(0, 0, 1, 1);
|
||||
colorCache.set(str, imgData.data);
|
||||
}
|
||||
return colorCache.get(str);
|
||||
}
|
||||
function startAngle(str) {
|
||||
switch (str) {
|
||||
case "east": return 0;
|
||||
case "north_east": return 1;
|
||||
case "north_west": return 2;
|
||||
case "west": return 3;
|
||||
case "south_west": return 4;
|
||||
case "south_east": return 5;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
function offsetAngle(str) {
|
||||
switch (str) {
|
||||
case "w": return 0;
|
||||
case "q": return 1;
|
||||
case "a": return 2;
|
||||
case "s": return 3;
|
||||
case "d": return 4;
|
||||
case "e": return 5;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
function initializeElem(canvas) {
|
||||
const str = canvas.dataset.string;
|
||||
let angle = startAngle(canvas.dataset.start);
|
||||
const perWorld = canvas.dataset.perWorld === "True";
|
||||
|
||||
// build geometry
|
||||
const points = [[0, 0]];
|
||||
let lastPoint = points[0];
|
||||
let minPoint = lastPoint, maxPoint = lastPoint;
|
||||
for (const ch of "w" + str) {
|
||||
const addAngle = offsetAngle(ch);
|
||||
if (addAngle < 0) continue;
|
||||
angle = (angle + addAngle) % 6;
|
||||
const trueAngle = Math.PI / 3 * angle;
|
||||
|
||||
const [lx, ly] = lastPoint;
|
||||
const newPoint = [lx + Math.cos(trueAngle), ly - Math.sin(trueAngle)];
|
||||
|
||||
points.push(newPoint);
|
||||
lastPoint = newPoint;
|
||||
|
||||
const [mix, miy] = minPoint;
|
||||
minPoint = [Math.min(mix, newPoint[0]), Math.min(miy, newPoint[1])];
|
||||
const [max, may] = maxPoint;
|
||||
maxPoint = [Math.max(max, newPoint[0]), Math.max(may, newPoint[1])];
|
||||
}
|
||||
const size = Math.min(canvas.width, canvas.height) * 0.8;
|
||||
const scale = size / Math.max(3, Math.max(maxPoint[1] - minPoint[1], maxPoint[0] - minPoint[0]));
|
||||
const center = [(minPoint[0] + maxPoint[0]) * 0.5, (minPoint[1] + maxPoint[1]) * 0.5];
|
||||
const truePoints = points.map(p => [canvas.width * 0.5 + scale * (p[0] - center[0]), canvas.height * 0.5 + scale * (p[1] - center[1])]);
|
||||
let uniqPoints = [];
|
||||
l1: for (const point of truePoints) {
|
||||
for (const pt of uniqPoints) {
|
||||
if (Math.abs(point[0] - pt[0]) < 0.00001 && Math.abs(point[1] - pt[1]) < 0.00001) {
|
||||
continue l1;
|
||||
}
|
||||
}
|
||||
uniqPoints.push(point);
|
||||
}
|
||||
|
||||
// rendering code
|
||||
const speed = 0.0025;
|
||||
const context = canvas.getContext("2d");
|
||||
const negaProgress = -3;
|
||||
let progress = 0;
|
||||
let scrollTimeout = 1e309;
|
||||
let speedLevel = 3;
|
||||
let speedIncrement = 0;
|
||||
function speedScale() {
|
||||
return speeds[speedLevel];
|
||||
}
|
||||
|
||||
const style = getComputedStyle(canvas);
|
||||
const getProp = n => style.getPropertyValue(n);
|
||||
|
||||
const tick = dt => {
|
||||
scrollTimeout += dt;
|
||||
if (canvas.offsetParent === null) return;
|
||||
|
||||
const strokeStyle = getProp("--path-color");
|
||||
const strokeVisitedStyle = getProp("--visited-path-color");
|
||||
|
||||
const startDotStyle = getProp("--start-dot-color");
|
||||
const dotStyle = getProp("--dot-color");
|
||||
const movDotStyle = getProp("--moving-dot-color");
|
||||
|
||||
const strokeWidth = scale * +getProp("--line-scale");
|
||||
const dotRadius = scale * +getProp("--dot-scale");
|
||||
const movDotRadius = scale * +getProp("--moving-dot-scale");
|
||||
const pauseScale = scale * +getProp("--pausetext-scale");
|
||||
const bodyBg = scale * +getProp("--pausetext-scale");
|
||||
const darkMode = +getProp("--dark-mode");
|
||||
const bgColors = getColorRGB(context, getComputedStyle(document.body).backgroundColor);
|
||||
|
||||
|
||||
if (!perWorld) {
|
||||
progress += speed * dt * (progress > 0 ? speedScale() : Math.sqrt(speedScale()));
|
||||
}
|
||||
if (progress >= truePoints.length - 1) {
|
||||
progress = negaProgress;
|
||||
}
|
||||
let ix = Math.floor(progress), frac = progress - ix, core = null, fadeColor = 0;
|
||||
if (ix < 0) {
|
||||
const rawFade = 2 * progress / negaProgress - 1;
|
||||
fadeColor = 1 - Math.abs(rawFade);
|
||||
context.strokeStyle = rawFade > 0 ? strokeVisitedStyle : strokeStyle;
|
||||
ix = rawFade > 0 ? truePoints.length - 2 : 0;
|
||||
frac = +(rawFade > 0);
|
||||
} else {
|
||||
context.strokeStyle = strokeVisitedStyle;
|
||||
}
|
||||
|
||||
const [lx, ly] = truePoints[ix];
|
||||
const [rx, ry] = truePoints[ix + 1];
|
||||
core = [lx + (rx - lx) * frac, ly + (ry - ly) * frac];
|
||||
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.beginPath();
|
||||
context.lineWidth = strokeWidth;
|
||||
context.moveTo(truePoints[0][0], truePoints[0][1]);
|
||||
for (let i = 1; i < ix + 1; i++) {
|
||||
context.lineTo(truePoints[i][0], truePoints[i][1]);
|
||||
}
|
||||
context.lineTo(core[0], core[1]);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.strokeStyle = strokeStyle;
|
||||
context.moveTo(core[0], core[1]);
|
||||
for (let i = ix + 1; i < truePoints.length; i++) {
|
||||
context.lineTo(truePoints[i][0], truePoints[i][1]);
|
||||
}
|
||||
context.stroke();
|
||||
|
||||
for (let i = 0; i < uniqPoints.length; i++) {
|
||||
context.beginPath();
|
||||
context.fillStyle = (i == 0 && !perWorld) ? startDotStyle : dotStyle;
|
||||
const radius = (i == 0 && !perWorld) ? movDotRadius : dotRadius;
|
||||
context.arc(uniqPoints[i][0], uniqPoints[i][1], radius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
|
||||
if (!perWorld) {
|
||||
context.beginPath();
|
||||
context.fillStyle = movDotStyle;
|
||||
context.arc(core[0], core[1], movDotRadius, 0, 2 * Math.PI);
|
||||
context.fill();
|
||||
}
|
||||
if (fadeColor) {
|
||||
context.fillStyle = `rgba(${bgColors[0]}, ${bgColors[1]}, ${bgColors[2]}, ${fadeColor})`;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
if (scrollTimeout <= 2000) {
|
||||
context.fillStyle = `rgba(200, 200, 200, ${(2000 - scrollTimeout) / 1000})`;
|
||||
context.font = `${pauseScale}px sans-serif`;
|
||||
context.fillText(speedScale() ? speedScale() + "x" : "Paused", 0.2 * scale, canvas.height - 0.2 * scale);
|
||||
}
|
||||
};
|
||||
rfaQueue.push(tick);
|
||||
|
||||
// scrolling input
|
||||
if (!perWorld) {
|
||||
canvas.addEventListener("wheel", ev => {
|
||||
speedIncrement += ev.deltaY;
|
||||
const oldSpeedLevel = speedLevel;
|
||||
if (speedIncrement >= scrollThreshold) {
|
||||
speedLevel--;
|
||||
} else if (speedIncrement <= -scrollThreshold) {
|
||||
speedLevel++;
|
||||
}
|
||||
if (oldSpeedLevel != speedLevel) {
|
||||
speedIncrement = 0;
|
||||
speedLevel = Math.max(0, Math.min(speeds.length - 1, speedLevel));
|
||||
scrollTimeout = 0;
|
||||
}
|
||||
ev.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
||||
function hookLoad(elem) {
|
||||
let init = false;
|
||||
const canvases = elem.querySelectorAll("canvas");
|
||||
elem.addEventListener("toggle", () => {
|
||||
if (!init) {
|
||||
canvases.forEach(initializeElem);
|
||||
init = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
function hookToggle(elem) {
|
||||
const details = Array.from(document.querySelectorAll("details." + elem.dataset.target));
|
||||
elem.addEventListener("click", () => {
|
||||
if (details.some(x => x.open)) {
|
||||
details.forEach(x => x.open = false);
|
||||
} else {
|
||||
details.forEach(x => x.open = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
function hookSpoiler(elem) {
|
||||
if (params.get("nospoiler") !== null) {
|
||||
elem.classList.add("unspoilered");
|
||||
} else {
|
||||
const thunk = ev => {
|
||||
if (!elem.classList.contains("unspoilered")) {
|
||||
ev.preventDefault();
|
||||
ev.stopImmediatePropagation();
|
||||
elem.classList.add("unspoilered");
|
||||
}
|
||||
elem.removeEventListener("click", thunk);
|
||||
};
|
||||
elem.addEventListener("click", thunk);
|
||||
|
||||
if (elem instanceof HTMLAnchorElement) {
|
||||
const href = elem.getAttribute("href");
|
||||
if (href.startsWith("#")) {
|
||||
elem.addEventListener("click", () => document.getElementById(href.substring(1)).querySelector(".spoilered").classList.add("unspoilered"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('details.spell-collapsible').forEach(hookLoad);
|
||||
document.querySelectorAll('a.toggle-link').forEach(hookToggle);
|
||||
document.querySelectorAll('.spoilered').forEach(hookSpoiler);
|
||||
function tick(prevTime, time) {
|
||||
const dt = time - prevTime;
|
||||
for (const q of rfaQueue) {
|
||||
q(dt);
|
||||
}
|
||||
requestAnimationFrame(t => tick(time, t));
|
||||
}
|
||||
requestAnimationFrame(t => tick(t, t));
|
||||
});
|
22
doc/templates/tableofcontents.html.jinja
vendored
Normal file
22
doc/templates/tableofcontents.html.jinja
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% import "macros.html.jinja" as macros %}
|
||||
<h2 id="table-of-contents" class="page-header">
|
||||
Table of Contents
|
||||
<a href="javascript:void(0)" class="permalink toggle-link small" data_target="toc-category" title="Toggle all">
|
||||
<i class="bi bi-list-nested"></i>
|
||||
</a>
|
||||
{{ macros.permalink("table-of-contents") }}
|
||||
</h2>
|
||||
{% for category in book.categories.values() %}
|
||||
<details class="toc-category">
|
||||
<summary>
|
||||
{{ macros.maybe_spoilered_link(category) }}
|
||||
</summary>
|
||||
<ul>
|
||||
{% for entry in category.entries %}
|
||||
<li>
|
||||
{{ macros.maybe_spoilered_link(entry) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endfor %}
|
Loading…
Reference in a new issue