mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 04:01:32 -06:00
WebHost: server render remaining markdown using mistune (#5276)
--------- Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com> Co-authored-by: qwint <qwint.42@gmail.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
This commit is contained in:
@@ -1926,7 +1926,7 @@ class Tutorial(NamedTuple):
|
|||||||
description: str
|
description: str
|
||||||
language: str
|
language: str
|
||||||
file_name: str
|
file_name: str
|
||||||
link: str
|
link: str # unused
|
||||||
authors: List[str]
|
authors: List[str]
|
||||||
|
|
||||||
|
|
||||||
|
48
WebHost.py
48
WebHost.py
@@ -54,16 +54,15 @@ def get_app() -> "Flask":
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
|
def copy_tutorials_files_to_static() -> None:
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
zfile: zipfile.ZipInfo
|
zfile: zipfile.ZipInfo
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
worlds = {}
|
worlds = {}
|
||||||
data = []
|
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
|
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
|
||||||
worlds[game] = world
|
worlds[game] = world
|
||||||
@@ -72,7 +71,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
|||||||
shutil.rmtree(base_target_path, ignore_errors=True)
|
shutil.rmtree(base_target_path, ignore_errors=True)
|
||||||
for game, world in worlds.items():
|
for game, world in worlds.items():
|
||||||
# copy files from world's docs folder to the generated folder
|
# copy files from world's docs folder to the generated folder
|
||||||
target_path = os.path.join(base_target_path, get_file_safe_name(game))
|
target_path = os.path.join(base_target_path, secure_filename(game))
|
||||||
os.makedirs(target_path, exist_ok=True)
|
os.makedirs(target_path, exist_ok=True)
|
||||||
|
|
||||||
if world.zip_path:
|
if world.zip_path:
|
||||||
@@ -85,45 +84,14 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]
|
|||||||
for zfile in zf.infolist():
|
for zfile in zf.infolist():
|
||||||
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
if not zfile.is_dir() and "/docs/" in zfile.filename:
|
||||||
zfile.filename = os.path.basename(zfile.filename)
|
zfile.filename = os.path.basename(zfile.filename)
|
||||||
zf.extract(zfile, target_path)
|
with open(os.path.join(target_path, secure_filename(zfile.filename)), "wb") as f:
|
||||||
|
f.write(zf.read(zfile))
|
||||||
else:
|
else:
|
||||||
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
|
||||||
files = os.listdir(source_path)
|
files = os.listdir(source_path)
|
||||||
for file in files:
|
for file in files:
|
||||||
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
|
shutil.copyfile(Utils.local_path(source_path, file),
|
||||||
|
Utils.local_path(target_path, secure_filename(file)))
|
||||||
# build a json tutorial dict per game
|
|
||||||
game_data = {'gameTitle': game, 'tutorials': []}
|
|
||||||
for tutorial in world.web.tutorials:
|
|
||||||
# build dict for the json file
|
|
||||||
current_tutorial = {
|
|
||||||
'name': tutorial.tutorial_name,
|
|
||||||
'description': tutorial.description,
|
|
||||||
'files': [{
|
|
||||||
'language': tutorial.language,
|
|
||||||
'filename': game + '/' + tutorial.file_name,
|
|
||||||
'link': f'{game}/{tutorial.link}',
|
|
||||||
'authors': tutorial.authors
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
# check if the name of the current guide exists already
|
|
||||||
for guide in game_data['tutorials']:
|
|
||||||
if guide and tutorial.tutorial_name == guide['name']:
|
|
||||||
guide['files'].append(current_tutorial['files'][0])
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
game_data['tutorials'].append(current_tutorial)
|
|
||||||
|
|
||||||
data.append(game_data)
|
|
||||||
with open(Utils.local_path("WebHostLib", "static", "generated", "tutorials.json"), 'w', encoding='utf-8-sig') as json_target:
|
|
||||||
generic_data = {}
|
|
||||||
for games in data:
|
|
||||||
if 'Archipelago' in games['gameTitle']:
|
|
||||||
generic_data = data.pop(data.index(games))
|
|
||||||
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"])
|
|
||||||
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
|
|
||||||
return sorted_data
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -142,7 +110,7 @@ if __name__ == "__main__":
|
|||||||
logging.warning("Could not update LttP sprites.")
|
logging.warning("Could not update LttP sprites.")
|
||||||
app = get_app()
|
app = get_app()
|
||||||
create_options_files()
|
create_options_files()
|
||||||
create_ordered_tutorials_file()
|
copy_tutorials_files_to_static()
|
||||||
if app.config["SELFLAUNCH"]:
|
if app.config["SELFLAUNCH"]:
|
||||||
autohost(app.config)
|
autohost(app.config)
|
||||||
if app.config["SELFGEN"]:
|
if app.config["SELFGEN"]:
|
||||||
|
@@ -7,17 +7,69 @@ from flask import request, redirect, url_for, render_template, Response, session
|
|||||||
from pony.orm import count, commit, db_session
|
from pony.orm import count, commit, db_session
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister, World
|
||||||
from . import app, cache
|
from . import app, cache
|
||||||
from .models import Seed, Room, Command, UUID, uuid4
|
from .models import Seed, Room, Command, UUID, uuid4
|
||||||
|
from Utils import title_sorted
|
||||||
|
|
||||||
|
|
||||||
def get_world_theme(game_name: str):
|
def get_world_theme(game_name: str) -> str:
|
||||||
if game_name in AutoWorldRegister.world_types:
|
if game_name in AutoWorldRegister.world_types:
|
||||||
return AutoWorldRegister.world_types[game_name].web.theme
|
return AutoWorldRegister.world_types[game_name].web.theme
|
||||||
return 'grass'
|
return 'grass'
|
||||||
|
|
||||||
|
|
||||||
|
def get_visible_worlds() -> dict[str, type(World)]:
|
||||||
|
worlds = {}
|
||||||
|
for game, world in AutoWorldRegister.world_types.items():
|
||||||
|
if not world.hidden:
|
||||||
|
worlds[game] = world
|
||||||
|
return worlds
|
||||||
|
|
||||||
|
|
||||||
|
def render_markdown(path: str) -> str:
|
||||||
|
import mistune
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
markdown = mistune.create_markdown(
|
||||||
|
escape=False,
|
||||||
|
plugins=[
|
||||||
|
"strikethrough",
|
||||||
|
"footnotes",
|
||||||
|
"table",
|
||||||
|
"speedup",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
heading_id_count: Counter[str] = Counter()
|
||||||
|
|
||||||
|
def heading_id(text: str) -> str:
|
||||||
|
nonlocal heading_id_count
|
||||||
|
import re # there is no good way to do this without regex
|
||||||
|
|
||||||
|
s = re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-").strip("-")
|
||||||
|
n = heading_id_count[s]
|
||||||
|
heading_id_count[s] += 1
|
||||||
|
if n > 0:
|
||||||
|
s += f"-{n}"
|
||||||
|
return s
|
||||||
|
|
||||||
|
def id_hook(_: mistune.Markdown, state: mistune.BlockState) -> None:
|
||||||
|
for tok in state.tokens:
|
||||||
|
if tok["type"] == "heading" and tok["attrs"]["level"] < 4:
|
||||||
|
text = tok["text"]
|
||||||
|
assert isinstance(text, str)
|
||||||
|
unique_id = heading_id(text)
|
||||||
|
tok["attrs"]["id"] = unique_id
|
||||||
|
tok["text"] = f"<a href=\"#{unique_id}\">{text}</a>" # make header link to itself
|
||||||
|
|
||||||
|
markdown.before_render_hooks.append(id_hook)
|
||||||
|
|
||||||
|
with open(path, encoding="utf-8-sig") as f:
|
||||||
|
document = f.read()
|
||||||
|
return markdown(document)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||||
def page_not_found(err):
|
def page_not_found(err):
|
||||||
@@ -31,83 +83,88 @@ def start_playing():
|
|||||||
return render_template(f"startPlaying.html")
|
return render_template(f"startPlaying.html")
|
||||||
|
|
||||||
|
|
||||||
# Game Info Pages
|
|
||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def game_info(game, lang):
|
def game_info(game, lang):
|
||||||
try:
|
"""Game Info Pages"""
|
||||||
world = AutoWorldRegister.world_types[game]
|
theme = get_world_theme(game)
|
||||||
if lang not in world.web.game_info_languages:
|
secure_game_name = secure_filename(game)
|
||||||
raise KeyError("Sorry, this game's info page is not available in that language yet.")
|
lang = secure_filename(lang)
|
||||||
except KeyError:
|
document = render_markdown(os.path.join(
|
||||||
return abort(404)
|
app.static_folder, "generated", "docs",
|
||||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
secure_game_name, f"{lang}_{secure_game_name}.md"
|
||||||
|
))
|
||||||
|
return render_template(
|
||||||
|
"markdown_document.html",
|
||||||
|
title=f"{game} Guide",
|
||||||
|
html_from_markdown=document,
|
||||||
|
theme=theme,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# List of supported games
|
|
||||||
@app.route('/games')
|
@app.route('/games')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def games():
|
def games():
|
||||||
worlds = {}
|
"""List of supported games"""
|
||||||
for game, world in AutoWorldRegister.world_types.items():
|
return render_template("supportedGames.html", worlds=get_visible_worlds())
|
||||||
if not world.hidden:
|
|
||||||
worlds[game] = world
|
|
||||||
return render_template("supportedGames.html", worlds=worlds)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
@app.route('/tutorial/<string:game>/<string:file>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def tutorial(game, file, lang):
|
def tutorial(game: str, file: str):
|
||||||
try:
|
theme = get_world_theme(game)
|
||||||
world = AutoWorldRegister.world_types[game]
|
secure_game_name = secure_filename(game)
|
||||||
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
|
file = secure_filename(file)
|
||||||
raise KeyError("Sorry, the tutorial is not available in that language yet.")
|
document = render_markdown(os.path.join(
|
||||||
except KeyError:
|
app.static_folder, "generated", "docs",
|
||||||
return abort(404)
|
secure_game_name, file+".md"
|
||||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
))
|
||||||
|
return render_template(
|
||||||
|
"markdown_document.html",
|
||||||
|
title=f"{game} Guide",
|
||||||
|
html_from_markdown=document,
|
||||||
|
theme=theme,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tutorial/')
|
@app.route('/tutorial/')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def tutorial_landing():
|
def tutorial_landing():
|
||||||
return render_template("tutorialLanding.html")
|
tutorials = {}
|
||||||
|
worlds = AutoWorldRegister.world_types
|
||||||
|
for world_name, world_type in worlds.items():
|
||||||
|
current_world = tutorials[world_name] = {}
|
||||||
|
for tutorial in world_type.web.tutorials:
|
||||||
|
current_tutorial = current_world.setdefault(tutorial.tutorial_name, {
|
||||||
|
"description": tutorial.description, "files": {}})
|
||||||
|
current_tutorial["files"][secure_filename(tutorial.file_name).rsplit(".", 1)[0]] = {
|
||||||
|
"authors": tutorial.authors,
|
||||||
|
"language": tutorial.language
|
||||||
|
}
|
||||||
|
tutorials = {world_name: tutorials for world_name, tutorials in title_sorted(
|
||||||
|
tutorials.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game)}
|
||||||
|
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/faq/<string:lang>/')
|
@app.route('/faq/<string:lang>/')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def faq(lang: str):
|
def faq(lang: str):
|
||||||
import markdown
|
document = render_markdown(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md"))
|
||||||
with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f:
|
|
||||||
document = f.read()
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"markdown_document.html",
|
"markdown_document.html",
|
||||||
title="Frequently Asked Questions",
|
title="Frequently Asked Questions",
|
||||||
html_from_markdown=markdown.markdown(
|
html_from_markdown=document,
|
||||||
document,
|
|
||||||
extensions=["toc", "mdx_breakless_lists"],
|
|
||||||
extension_configs={
|
|
||||||
"toc": {"anchorlink": True}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/glossary/<string:lang>/')
|
@app.route('/glossary/<string:lang>/')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def glossary(lang: str):
|
def glossary(lang: str):
|
||||||
import markdown
|
document = render_markdown(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md"))
|
||||||
with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f:
|
|
||||||
document = f.read()
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"markdown_document.html",
|
"markdown_document.html",
|
||||||
title="Glossary",
|
title="Glossary",
|
||||||
html_from_markdown=markdown.markdown(
|
html_from_markdown=document,
|
||||||
document,
|
|
||||||
extensions=["toc", "mdx_breakless_lists"],
|
|
||||||
extension_configs={
|
|
||||||
"toc": {"anchorlink": True}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -7,6 +7,5 @@ Flask-Compress>=1.17
|
|||||||
Flask-Limiter>=3.12
|
Flask-Limiter>=3.12
|
||||||
bokeh>=3.6.3
|
bokeh>=3.6.3
|
||||||
markupsafe>=3.0.2
|
markupsafe>=3.0.2
|
||||||
Markdown>=3.7
|
|
||||||
mdx-breakless-lists>=1.0.1
|
|
||||||
setproctitle>=1.3.5
|
setproctitle>=1.3.5
|
||||||
|
mistune>=3.1.3
|
||||||
|
@@ -1,45 +0,0 @@
|
|||||||
window.addEventListener('load', () => {
|
|
||||||
const gameInfo = document.getElementById('game-info');
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const ajax = new XMLHttpRequest();
|
|
||||||
ajax.onreadystatechange = () => {
|
|
||||||
if (ajax.readyState !== 4) { return; }
|
|
||||||
if (ajax.status === 404) {
|
|
||||||
reject("Sorry, this game's info page is not available in that language yet.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ajax.status !== 200) {
|
|
||||||
reject("Something went wrong while loading the info page.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(ajax.responseText);
|
|
||||||
};
|
|
||||||
ajax.open('GET', `${window.location.origin}/static/generated/docs/${gameInfo.getAttribute('data-game')}/` +
|
|
||||||
`${gameInfo.getAttribute('data-lang')}_${gameInfo.getAttribute('data-game')}.md`, true);
|
|
||||||
ajax.send();
|
|
||||||
}).then((results) => {
|
|
||||||
// Populate page with HTML generated from markdown
|
|
||||||
showdown.setOption('tables', true);
|
|
||||||
showdown.setOption('strikethrough', true);
|
|
||||||
showdown.setOption('literalMidWordUnderscores', true);
|
|
||||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
|
||||||
|
|
||||||
// Reset the id of all header divs to something nicer
|
|
||||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
|
||||||
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
|
|
||||||
header.setAttribute('id', headerId);
|
|
||||||
header.addEventListener('click', () => {
|
|
||||||
window.location.hash = `#${headerId}`;
|
|
||||||
header.scrollIntoView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
|
||||||
document.fonts.ready.finally(() => {
|
|
||||||
if (window.location.hash) {
|
|
||||||
const scrollTarget = document.getElementById(window.location.hash.substring(1));
|
|
||||||
scrollTarget?.scrollIntoView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,52 +0,0 @@
|
|||||||
window.addEventListener('load', () => {
|
|
||||||
const tutorialWrapper = document.getElementById('tutorial-wrapper');
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const ajax = new XMLHttpRequest();
|
|
||||||
ajax.onreadystatechange = () => {
|
|
||||||
if (ajax.readyState !== 4) { return; }
|
|
||||||
if (ajax.status === 404) {
|
|
||||||
reject("Sorry, the tutorial is not available in that language yet.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ajax.status !== 200) {
|
|
||||||
reject("Something went wrong while loading the tutorial.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(ajax.responseText);
|
|
||||||
};
|
|
||||||
ajax.open('GET', `${window.location.origin}/static/generated/docs/` +
|
|
||||||
`${tutorialWrapper.getAttribute('data-game')}/${tutorialWrapper.getAttribute('data-file')}_` +
|
|
||||||
`${tutorialWrapper.getAttribute('data-lang')}.md`, true);
|
|
||||||
ajax.send();
|
|
||||||
}).then((results) => {
|
|
||||||
// Populate page with HTML generated from markdown
|
|
||||||
showdown.setOption('tables', true);
|
|
||||||
showdown.setOption('strikethrough', true);
|
|
||||||
showdown.setOption('literalMidWordUnderscores', true);
|
|
||||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
|
||||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
|
||||||
|
|
||||||
const title = document.querySelector('h1')
|
|
||||||
if (title) {
|
|
||||||
document.title = title.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the id of all header divs to something nicer
|
|
||||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
|
||||||
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
|
|
||||||
header.setAttribute('id', headerId);
|
|
||||||
header.addEventListener('click', () => {
|
|
||||||
window.location.hash = `#${headerId}`;
|
|
||||||
header.scrollIntoView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manually scroll the user to the appropriate header if anchor navigation is used
|
|
||||||
document.fonts.ready.finally(() => {
|
|
||||||
if (window.location.hash) {
|
|
||||||
const scrollTarget = document.getElementById(window.location.hash.substring(1));
|
|
||||||
scrollTarget?.scrollIntoView();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,81 +0,0 @@
|
|||||||
const showError = () => {
|
|
||||||
const tutorial = document.getElementById('tutorial-landing');
|
|
||||||
document.getElementById('page-title').innerText = 'This page is out of logic!';
|
|
||||||
tutorial.removeChild(document.getElementById('loading'));
|
|
||||||
const userMessage = document.createElement('h3');
|
|
||||||
const homepageLink = document.createElement('a');
|
|
||||||
homepageLink.innerText = 'Click here';
|
|
||||||
homepageLink.setAttribute('href', '/');
|
|
||||||
userMessage.append(homepageLink);
|
|
||||||
userMessage.append(' to go back to safety!');
|
|
||||||
tutorial.append(userMessage);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const ajax = new XMLHttpRequest();
|
|
||||||
ajax.onreadystatechange = () => {
|
|
||||||
if (ajax.readyState !== 4) { return; }
|
|
||||||
const tutorialDiv = document.getElementById('tutorial-landing');
|
|
||||||
if (ajax.status !== 200) { return showError(); }
|
|
||||||
|
|
||||||
try {
|
|
||||||
const games = JSON.parse(ajax.responseText);
|
|
||||||
games.forEach((game) => {
|
|
||||||
const gameTitle = document.createElement('h2');
|
|
||||||
gameTitle.innerText = game.gameTitle;
|
|
||||||
gameTitle.id = `${encodeURIComponent(game.gameTitle)}`;
|
|
||||||
tutorialDiv.appendChild(gameTitle);
|
|
||||||
|
|
||||||
game.tutorials.forEach((tutorial) => {
|
|
||||||
const tutorialName = document.createElement('h3');
|
|
||||||
tutorialName.innerText = tutorial.name;
|
|
||||||
tutorialDiv.appendChild(tutorialName);
|
|
||||||
|
|
||||||
const tutorialDescription = document.createElement('p');
|
|
||||||
tutorialDescription.innerText = tutorial.description;
|
|
||||||
tutorialDiv.appendChild(tutorialDescription);
|
|
||||||
|
|
||||||
const intro = document.createElement('p');
|
|
||||||
intro.innerText = 'This guide is available in the following languages:';
|
|
||||||
tutorialDiv.appendChild(intro);
|
|
||||||
|
|
||||||
const fileList = document.createElement('ul');
|
|
||||||
tutorial.files.forEach((file) => {
|
|
||||||
const listItem = document.createElement('li');
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
anchor.innerText = file.language;
|
|
||||||
anchor.setAttribute('href', `${window.location.origin}/tutorial/${file.link}`);
|
|
||||||
listItem.appendChild(anchor);
|
|
||||||
|
|
||||||
listItem.append(' by ');
|
|
||||||
for (let author of file.authors) {
|
|
||||||
listItem.append(author);
|
|
||||||
if (file.authors.indexOf(author) !== (file.authors.length -1)) {
|
|
||||||
listItem.append(', ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileList.appendChild(listItem);
|
|
||||||
});
|
|
||||||
tutorialDiv.appendChild(fileList);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tutorialDiv.removeChild(document.getElementById('loading'));
|
|
||||||
} catch (error) {
|
|
||||||
showError();
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we are on an anchor when coming in, and scroll to it.
|
|
||||||
const hash = window.location.hash;
|
|
||||||
if (hash) {
|
|
||||||
const offset = 128; // To account for navbar banner at top of page.
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
const rect = document.getElementById(hash.slice(1)).getBoundingClientRect();
|
|
||||||
window.scrollTo(rect.left, rect.top - offset);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true);
|
|
||||||
ajax.send();
|
|
||||||
});
|
|
@@ -1,17 +0,0 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
<title>{{ game }} Info</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
|
||||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/gameInfo.js") }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{% include 'header/'+theme+'Header.html' %}
|
|
||||||
<div id="game-info" class="markdown" data-lang="{{ lang }}" data-game="{{ game | get_file_safe_name }}">
|
|
||||||
<!-- Populated my JS / MD -->
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@@ -1,7 +1,8 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% set theme_name = theme|default("grass", true) %}
|
||||||
|
{% include "header/"+theme_name+"Header.html" %}
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{% include 'header/'+theme+'Header.html' %}
|
|
||||||
<title>Archipelago</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
|
||||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tutorial.js") }}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div id="tutorial-wrapper" class="markdown" data-game="{{ game | get_file_safe_name }}" data-file="{{ file | get_file_safe_name }}" data-lang="{{ lang }}">
|
|
||||||
<!-- Content generated by JavaScript -->
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@@ -3,14 +3,32 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<title>Archipelago Guides</title>
|
<title>Archipelago Guides</title>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}"/>
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorialLanding.css") }}" />
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorialLanding.css") }}"/>
|
||||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/tutorialLanding.js") }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="tutorial-landing" class="markdown" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
|
<div id="tutorial-landing" class="markdown">
|
||||||
<h1 id="page-title">Archipelago Guides</h1>
|
<h1>Archipelago Guides</h1>
|
||||||
<p id="loading">Loading...</p>
|
{% for world_name, world_type in worlds.items() %}
|
||||||
|
<h2 id="{{ world_type.game | urlencode }}">{{ world_type.game }}</h2>
|
||||||
|
{% for tutorial_name, tutorial_data in tutorials[world_name].items() %}
|
||||||
|
<h3>{{ tutorial_name }}</h3>
|
||||||
|
<p>{{ tutorial_data.description }}</p>
|
||||||
|
<p>This guide is available in the following languages:</p>
|
||||||
|
<ul>
|
||||||
|
{% for file_name, file_data in tutorial_data.files.items() %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for("tutorial", game=world_name, file=file_name) }}">{{ file_data.language }}</a>
|
||||||
|
by
|
||||||
|
{% for author in file_data.authors %}
|
||||||
|
{{ author }}
|
||||||
|
{% if not loop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -2,6 +2,8 @@ import unittest
|
|||||||
import Utils
|
import Utils
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
import WebHost
|
import WebHost
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
|
||||||
@@ -9,36 +11,30 @@ from worlds.AutoWorld import AutoWorldRegister
|
|||||||
class TestDocs(unittest.TestCase):
|
class TestDocs(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls) -> None:
|
def setUpClass(cls) -> None:
|
||||||
cls.tutorials_data = WebHost.create_ordered_tutorials_file()
|
WebHost.copy_tutorials_files_to_static()
|
||||||
|
|
||||||
def test_has_tutorial(self):
|
def test_has_tutorial(self):
|
||||||
games_with_tutorial = set(entry["gameTitle"] for entry in self.tutorials_data)
|
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
with self.subTest(game_name):
|
with self.subTest(game_name):
|
||||||
try:
|
tutorials = world_type.web.tutorials
|
||||||
self.assertIn(game_name, games_with_tutorial)
|
self.assertGreater(len(tutorials), 0, msg=f"{game_name} has no setup tutorial.")
|
||||||
except AssertionError:
|
|
||||||
# look for partial name in the tutorial name
|
safe_name = secure_filename(game_name)
|
||||||
for game in games_with_tutorial:
|
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name)
|
||||||
if game_name in game:
|
for tutorial in tutorials:
|
||||||
break
|
self.assertTrue(
|
||||||
else:
|
os.path.isfile(Utils.local_path(target_path, secure_filename(tutorial.file_name))),
|
||||||
self.fail(f"{game_name} has no setup tutorial. "
|
f'{game_name} missing tutorial file {tutorial.file_name}.'
|
||||||
f"Games with Tutorial: {games_with_tutorial}")
|
)
|
||||||
|
|
||||||
def test_has_game_info(self):
|
def test_has_game_info(self):
|
||||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
if not world_type.hidden:
|
if not world_type.hidden:
|
||||||
safe_name = Utils.get_file_safe_name(game_name)
|
safe_name = secure_filename(game_name)
|
||||||
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name)
|
target_path = Utils.local_path("WebHostLib", "static", "generated", "docs", safe_name)
|
||||||
for game_info_lang in world_type.web.game_info_languages:
|
for game_info_lang in world_type.web.game_info_languages:
|
||||||
with self.subTest(game_name):
|
with self.subTest(game_name):
|
||||||
self.assertTrue(
|
|
||||||
safe_name == game_name or
|
|
||||||
not os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{game_name}.md')),
|
|
||||||
f'Info docs have be named <lang>_{safe_name}.md for {game_name}.'
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{safe_name}.md')),
|
os.path.isfile(Utils.local_path(target_path, f'{game_info_lang}_{safe_name}.md')),
|
||||||
f'{game_name} missing game info file for "{game_info_lang}" language.'
|
f'{game_name} missing game info file for "{game_info_lang}" language.'
|
||||||
|
@@ -29,8 +29,3 @@ class TestFileGeneration(unittest.TestCase):
|
|||||||
with open(file, encoding="utf-8-sig") as f:
|
with open(file, encoding="utf-8-sig") as f:
|
||||||
for value in roll_options({file.name: f.read()})[0].values():
|
for value in roll_options({file.name: f.read()})[0].values():
|
||||||
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.")
|
||||||
|
|
||||||
def test_tutorial(self):
|
|
||||||
WebHost.create_ordered_tutorials_file()
|
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json")))
|
|
||||||
self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json")))
|
|
||||||
|
@@ -34,7 +34,7 @@ class AWebInTime(WebWorld):
|
|||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide for setting up A Hat in Time to be played in Archipelago.",
|
"A guide for setting up A Hat in Time to be played in Archipelago.",
|
||||||
"English",
|
"English",
|
||||||
"ahit_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["CookieCat"]
|
["CookieCat"]
|
||||||
)]
|
)]
|
||||||
|
@@ -25,7 +25,7 @@ class OSRSWeb(WebWorld):
|
|||||||
"Multiworld Setup Guide",
|
"Multiworld Setup Guide",
|
||||||
"A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld",
|
"A guide to setting up the Old School Runescape Randomizer connected to an Archipelago Multiworld",
|
||||||
"English",
|
"English",
|
||||||
"docs/setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["digiholic"]
|
["digiholic"]
|
||||||
)
|
)
|
||||||
|
@@ -56,7 +56,7 @@ class Yugioh06Web(WebWorld):
|
|||||||
"A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 "
|
"A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 "
|
||||||
"for Archipelago on your computer.",
|
"for Archipelago on your computer.",
|
||||||
"English",
|
"English",
|
||||||
"docs/setup_en.md",
|
"setup_en.md",
|
||||||
"setup/en",
|
"setup/en",
|
||||||
["Rensen"],
|
["Rensen"],
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user