Skip to content

Commit 8837e61

Browse files
authored
WebHost, Multiple Worlds: fix images not showing in guides (#5576)
* Multiple: resize FR RA network commands screenshot This is now more in line with the text (and the english version). * Multiple: optimize EN RA network commands screenshot The URL has changed, so it's a good time to optimize. * WebHost, Worlds: fix retroarch images not showing Implements a src/url replacement for relative paths. Moves the RA screenshots to worlds/generic since they are shared. Also now uses the FR version in ffmq. Also fixes the formatting that resultet in the list breaking. Also moves imports in render_markdown. Guides now also properly render on Github. * Factorio: optimize screenshots The URL has changed, so it's a good time to optimize. * Factorio: change guide screenshots to use relative URL * Test: markdown: fix tests on Windows We also can't use delete=True, delete_on_close=False because that's not supported in Py3.11. * Test: markdown: fix typo I hope that's it now. *sigh* * Landstalker: fix doc images not showing Change to relative img urls. * Landstalker: optimize doc PNGs The URL has changed, so it's a good time to optimize.
1 parent 2bf410f commit 8837e61

29 files changed

+209
-98
lines changed

WebHostLib/markdown.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import re
2+
from collections import Counter
3+
4+
import mistune
5+
from werkzeug.utils import secure_filename
6+
7+
8+
__all__ = [
9+
"ImgUrlRewriteInlineParser",
10+
'render_markdown',
11+
]
12+
13+
14+
class ImgUrlRewriteInlineParser(mistune.InlineParser):
15+
relative_url_base: str
16+
17+
def __init__(self, relative_url_base: str, hard_wrap: bool = False) -> None:
18+
super().__init__(hard_wrap)
19+
self.relative_url_base = relative_url_base
20+
21+
@staticmethod
22+
def _find_game_name_by_folder_name(name: str) -> str | None:
23+
from worlds.AutoWorld import AutoWorldRegister
24+
25+
for world_name, world_type in AutoWorldRegister.world_types.items():
26+
if world_type.__module__ == f"worlds.{name}":
27+
return world_name
28+
return None
29+
30+
def parse_link(self, m: re.Match[str], state: mistune.InlineState) -> int | None:
31+
res = super().parse_link(m, state)
32+
if res is not None and state.tokens and state.tokens[-1]["type"] == "image":
33+
image_token = state.tokens[-1]
34+
url: str = image_token["attrs"]["url"]
35+
if not url.startswith("/") and not "://" in url:
36+
# replace relative URL to another world's doc folder with the webhost folder layout
37+
if url.startswith("../../") and "/docs/" in self.relative_url_base:
38+
parts = url.split("/", 4)
39+
if parts[2] != ".." and parts[3] == "docs":
40+
game_name = self._find_game_name_by_folder_name(parts[2])
41+
if game_name is not None:
42+
url = "/".join(parts[1:2] + [secure_filename(game_name)] + parts[4:])
43+
# change relative URL to point to deployment folder
44+
url = f"{self.relative_url_base}/{url}"
45+
image_token['attrs']['url'] = url
46+
return res
47+
48+
49+
def render_markdown(path: str, img_url_base: str | None = None) -> str:
50+
markdown = mistune.create_markdown(
51+
escape=False,
52+
plugins=[
53+
"strikethrough",
54+
"footnotes",
55+
"table",
56+
"speedup",
57+
],
58+
)
59+
60+
heading_id_count: Counter[str] = Counter()
61+
62+
def heading_id(text: str) -> str:
63+
nonlocal heading_id_count
64+
65+
# there is no good way to do this without regex
66+
s = re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-").strip("-")
67+
n = heading_id_count[s]
68+
heading_id_count[s] += 1
69+
if n > 0:
70+
s += f"-{n}"
71+
return s
72+
73+
def id_hook(_: mistune.Markdown, state: mistune.BlockState) -> None:
74+
for tok in state.tokens:
75+
if tok["type"] == "heading" and tok["attrs"]["level"] < 4:
76+
text = tok["text"]
77+
assert isinstance(text, str)
78+
unique_id = heading_id(text)
79+
tok["attrs"]["id"] = unique_id
80+
tok["text"] = f"<a href=\"#{unique_id}\">{text}</a>" # make header link to itself
81+
82+
markdown.before_render_hooks.append(id_hook)
83+
if img_url_base:
84+
markdown.inline = ImgUrlRewriteInlineParser(img_url_base)
85+
86+
with open(path, encoding="utf-8-sig") as f:
87+
document = f.read()
88+
html = markdown(document)
89+
assert isinstance(html, str), "Unexpected mistune renderer in render_markdown"
90+
return html

WebHostLib/misc.py

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from worlds.AutoWorld import AutoWorldRegister, World
1111
from . import app, cache
12+
from .markdown import render_markdown
1213
from .models import Seed, Room, Command, UUID, uuid4
1314
from Utils import title_sorted
1415

@@ -27,49 +28,6 @@ def get_visible_worlds() -> dict[str, type(World)]:
2728
return worlds
2829

2930

30-
def render_markdown(path: str) -> str:
31-
import mistune
32-
from collections import Counter
33-
34-
markdown = mistune.create_markdown(
35-
escape=False,
36-
plugins=[
37-
"strikethrough",
38-
"footnotes",
39-
"table",
40-
"speedup",
41-
],
42-
)
43-
44-
heading_id_count: Counter[str] = Counter()
45-
46-
def heading_id(text: str) -> str:
47-
nonlocal heading_id_count
48-
import re # there is no good way to do this without regex
49-
50-
s = re.sub(r"[^\w\- ]", "", text.lower()).replace(" ", "-").strip("-")
51-
n = heading_id_count[s]
52-
heading_id_count[s] += 1
53-
if n > 0:
54-
s += f"-{n}"
55-
return s
56-
57-
def id_hook(_: mistune.Markdown, state: mistune.BlockState) -> None:
58-
for tok in state.tokens:
59-
if tok["type"] == "heading" and tok["attrs"]["level"] < 4:
60-
text = tok["text"]
61-
assert isinstance(text, str)
62-
unique_id = heading_id(text)
63-
tok["attrs"]["id"] = unique_id
64-
tok["text"] = f"<a href=\"#{unique_id}\">{text}</a>" # make header link to itself
65-
66-
markdown.before_render_hooks.append(id_hook)
67-
68-
with open(path, encoding="utf-8-sig") as f:
69-
document = f.read()
70-
return markdown(document)
71-
72-
7331
@app.errorhandler(404)
7432
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
7533
def page_not_found(err):
@@ -91,10 +49,9 @@ def game_info(game, lang):
9149
theme = get_world_theme(game)
9250
secure_game_name = secure_filename(game)
9351
lang = secure_filename(lang)
94-
document = render_markdown(os.path.join(
95-
app.static_folder, "generated", "docs",
96-
secure_game_name, f"{lang}_{secure_game_name}.md"
97-
))
52+
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name)
53+
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}")
54+
document = render_markdown(os.path.join(file_dir, f"{lang}_{secure_game_name}.md"), file_dir_url)
9855
return render_template(
9956
"markdown_document.html",
10057
title=f"{game} Guide",
@@ -119,10 +76,9 @@ def tutorial(game: str, file: str):
11976
theme = get_world_theme(game)
12077
secure_game_name = secure_filename(game)
12178
file = secure_filename(file)
122-
document = render_markdown(os.path.join(
123-
app.static_folder, "generated", "docs",
124-
secure_game_name, file+".md"
125-
))
79+
file_dir = os.path.join(app.static_folder, "generated", "docs", secure_game_name)
80+
file_dir_url = url_for("static", filename=f"generated/docs/{secure_game_name}")
81+
document = render_markdown(os.path.join(file_dir, f"{file}.md"), file_dir_url)
12682
return render_template(
12783
"markdown_document.html",
12884
title=f"{game} Guide",

test/webhost/test_markdown.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import unittest
3+
from tempfile import NamedTemporaryFile
4+
5+
from mistune import HTMLRenderer, Markdown
6+
7+
from WebHostLib.markdown import ImgUrlRewriteInlineParser, render_markdown
8+
9+
10+
class ImgUrlRewriteTest(unittest.TestCase):
11+
markdown: Markdown
12+
base_url = "/static/generated/docs/some_game"
13+
14+
def setUp(self) -> None:
15+
self.markdown = Markdown(
16+
renderer=HTMLRenderer(escape=False),
17+
inline=ImgUrlRewriteInlineParser(self.base_url),
18+
)
19+
20+
def test_relative_img_rewrite(self) -> None:
21+
html = self.markdown("![Image](image.png)")
22+
self.assertIn(f'src="{self.base_url}/image.png"', html)
23+
24+
def test_absolute_img_no_rewrite(self) -> None:
25+
html = self.markdown("![Image](/image.png)")
26+
self.assertIn(f'src="/image.png"', html)
27+
self.assertNotIn(self.base_url, html)
28+
29+
def test_remote_img_no_rewrite(self) -> None:
30+
html = self.markdown("![Image](https://example.com/image.png)")
31+
self.assertIn(f'src="https://example.com/image.png"', html)
32+
self.assertNotIn(self.base_url, html)
33+
34+
def test_relative_link_no_rewrite(self) -> None:
35+
# The parser is only supposed to update images, not links.
36+
html = self.markdown("[Link](image.png)")
37+
self.assertIn(f'href="image.png"', html)
38+
self.assertNotIn(self.base_url, html)
39+
40+
def test_absolute_link_no_rewrite(self) -> None:
41+
html = self.markdown("[Link](/image.png)")
42+
self.assertIn(f'href="/image.png"', html)
43+
self.assertNotIn(self.base_url, html)
44+
45+
def test_auto_link_no_rewrite(self) -> None:
46+
html = self.markdown("<https://example.com/image.png>")
47+
self.assertIn(f'href="https://example.com/image.png"', html)
48+
self.assertNotIn(self.base_url, html)
49+
50+
def test_relative_img_to_other_game(self) -> None:
51+
html = self.markdown("![Image](../../generic/docs/image.png)")
52+
self.assertIn(f'src="{self.base_url}/../Archipelago/image.png"', html)
53+
54+
55+
class RenderMarkdownTest(unittest.TestCase):
56+
"""Tests that render_markdown does the right thing."""
57+
base_url = "/static/generated/docs/some_game"
58+
59+
def test_relative_img_rewrite(self) -> None:
60+
f = NamedTemporaryFile(delete=False)
61+
try:
62+
f.write("![Image](image.png)".encode("utf-8"))
63+
f.close()
64+
html = render_markdown(f.name, self.base_url)
65+
self.assertIn(f'src="{self.base_url}/image.png"', html)
66+
finally:
67+
os.unlink(f.name)
68+
69+
def test_no_img_rewrite(self) -> None:
70+
f = NamedTemporaryFile(delete=False)
71+
try:
72+
f.write("![Image](image.png)".encode("utf-8"))
73+
f.close()
74+
html = render_markdown(f.name)
75+
self.assertIn(f'src="image.png"', html)
76+
self.assertNotIn(self.base_url, html)
77+
finally:
78+
os.unlink(f.name)

worlds/alttp/docs/multiworld_en.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ You only have to do these steps once.
8888
1. Enter the RetroArch main menu screen.
8989
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
9090
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
91-
Network Command Port at 55355.
92-
93-
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
91+
Network Command Port at 55355. \
92+
![Screenshot of Network Commands setting](../../generic/docs/retroarch-network-commands-en.png)
9493
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
9594
Performance)".
9695

worlds/alttp/docs/multiworld_es.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ Sólo hay que seguir estos pasos una vez.
8888
1. Comienza en la pantalla del menú principal de RetroArch.
8989
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
9090
3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto,
91-
el Puerto de comandos de red.
92-
93-
![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
91+
el Puerto de comandos de red. \
92+
![Captura de pantalla del ajuste Comandos de red](../../generic/docs/retroarch-network-commands-en.png)
9493
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
9594
SFC (bsnes-mercury Performance)".
9695

worlds/alttp/docs/multiworld_fr.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ Vous n'avez qu'à faire ces étapes qu'une fois.
8989
1. Entrez dans le menu principal RetroArch
9090
2. Allez dans Réglages --> Interface utilisateur. Mettez "Afficher les réglages avancés" sur ON.
9191
3. Allez dans Réglages --> Réseau. Mettez "Commandes Réseau" sur ON. (trouvé sous Request Device 16.) Laissez le
92-
Port des commandes réseau à 555355.
93-
94-
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-fr.png)
92+
Port des commandes réseau à 555355. \
93+
![Screenshot of Network Commands setting](../../generic/docs/retroarch-network-commands-fr.png)
9594
4. Allez dans Menu Principal --> Mise à jour en ligne --> Téléchargement de cœurs. Descendez jusqu'a"Nintendo - SNES / SFC (bsnes-mercury Performance)" et
9695
sélectionnez le.
9796

-30.6 KB
Binary file not shown.
-20 KB
Binary file not shown.

worlds/dkc3/docs/setup_en.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,8 @@ You only have to do these steps once. Note, RetroArch 1.9.x will not work as it
111111
1. Enter the RetroArch main menu screen.
112112
2. Go to Settings --> User Interface. Set "Show Advanced Settings" to ON.
113113
3. Go to Settings --> Network. Set "Network Commands" to ON. (It is found below Request Device 16.) Leave the default
114-
Network Command Port at 55355.
115-
116-
![Screenshot of Network Commands setting](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
114+
Network Command Port at 55355. \
115+
![Screenshot of Network Commands setting](../../generic/docs/retroarch-network-commands-en.png)
117116
4. Go to Main Menu --> Online Updater --> Core Downloader. Scroll down and select "Nintendo - SNES / SFC (bsnes-mercury
118117
Performance)".
119118

-20.4 KB
Loading

0 commit comments

Comments
 (0)