{{ game }} script viewer
-{% include 'partials/info.html' %} +-
+ {% for (chapter_id, chapter) in chapters %}
+
- {{ chapter }} + {% endfor %} +
diff --git a/.github/workflows/deploy-dr.yml b/.github/workflows/deploy-dr.yml
index e3dcf8f2..b44c8a8c 100644
--- a/.github/workflows/deploy-dr.yml
+++ b/.github/workflows/deploy-dr.yml
@@ -28,16 +28,17 @@ jobs:
./utmtcli/UndertaleModCli load game/chapter3_windows/data.win --scripts 'scripts/ExportCodeFormatted.csx'
./utmtcli/UndertaleModCli load game/chapter4_windows/data.win --scripts 'scripts/ExportCodeFormatted.csx'
mkdir decompiled-deltarune
+ mv game/Export_Code decompiled-deltarune/init
mv game/chapter1_windows/Export_Code decompiled-deltarune/ch1
mv game/chapter2_windows/Export_Code decompiled-deltarune/ch2
mv game/chapter3_windows/Export_Code decompiled-deltarune/ch3
mv game/chapter4_windows/Export_Code decompiled-deltarune/ch4
- name: Build
- run: ./build.sh deltarune
+ run: python3 build.py deltarune
- name: Publish
uses: netlify/actions/cli@master
with:
- args: deploy --prod --dir=out --message="GitHub Actions" --timeout=3600
+ args: deploy --prod --dir=out/deltarune --message="GitHub Actions" --timeout=3600
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_DR }}
- NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+ NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/deploy-ut.yml b/.github/workflows/deploy-ut.yml
index 61f7607c..01801739 100644
--- a/.github/workflows/deploy-ut.yml
+++ b/.github/workflows/deploy-ut.yml
@@ -26,11 +26,11 @@ jobs:
./utmtcli/UndertaleModCli load game/assets/game.unx --scripts 'scripts/ExportCodeFormatted.csx'
mv game/assets/Export_Code decompiled-undertale
- name: Build
- run: ./build.sh undertale
+ run: python3 build.py undertale
- name: Publish
uses: netlify/actions/cli@master
with:
- args: deploy --dir=out --prod
+ args: deploy --dir=out/undertale --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_UT }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
diff --git a/.github/workflows/deploy-uty.yml b/.github/workflows/deploy-uty.yml
index 4d554605..9844c0a1 100644
--- a/.github/workflows/deploy-uty.yml
+++ b/.github/workflows/deploy-uty.yml
@@ -22,11 +22,11 @@ jobs:
./utmtcli/UndertaleModCli load data.win --scripts 'scripts/ExportCodeFormatted.csx'
mv Export_Code decompiled-undertaleyellow
- name: Build
- run: ./build.sh undertaleyellow
+ run: python3 build.py undertaleyellow
- name: Publish
uses: netlify/actions/cli@master
with:
- args: deploy --dir=out --prod
+ args: deploy --dir=out/undertaleyellow --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID_UTY }}
- NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+ NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
\ No newline at end of file
diff --git a/README.md b/README.md
index 1c8c59fb..2443520f 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,9 @@ While this may not be suited for personal use, contributions that make it easier
## Building
-Download either Undertale, Deltarune, or Undertale Yellow and extract their scripts using [UndertaleModTool](https://github.com/UnderminersTeam/UndertaleModTool)'s `ExportAllCode.csx` script. The scripts need to be located in `decompiled-{undertale,deltarune,undertaleyellow}` directories.
+Download either Undertale, Deltarune, or Undertale Yellow and extract their scripts using [UndertaleModTool](https://github.com/UnderminersTeam/UndertaleModTool)'s `ExportAllCode.csx` script. The scripts need to be located in `decompiled-{undertale,deltarune,undertaleyellow}` directories. For multi-chapter games (deltarune), use subdirectories for individual chapters' scripts (e.g. `decompiled-deltarune/{ch1,ch2,ch3,ch4,init}`).
-After installing prerequisites, first install required dependencies of the project using `pip install -r requirements.txt`, then build the site using `./build.sh [game]`. The site is placed by default in the `out` directory. To view the site after building, (if you have Python installed), run `./dev.sh`. A Bash (or any Linux shell) environment is assumed when running the mentioned commands.
+After installing prerequisites, first install required dependencies of the project using `pip install -r requirements.txt`, then build the site using `python3 build.py [game]`. The site is placed by default in the `out/[game]` directory. To view the site after building, (if you have Python installed), run `./dev.sh [game]`. A Bash (or any Linux shell) environment is assumed when running the mentioned commands.
## Disclaimer
diff --git a/build.py b/build.py
new file mode 100644
index 00000000..864fbba2
--- /dev/null
+++ b/build.py
@@ -0,0 +1,158 @@
+import argparse
+import os
+import zipfile
+from io import BytesIO
+from os.path import dirname, exists, join, relpath
+from shutil import copyfile, rmtree
+from typing import Optional
+
+import requests
+from loguru import logger
+from tqdm import tqdm
+
+from data import Data
+from generate import generate
+
+DIR = dirname(__file__)
+STATIC = join(DIR, 'static')
+
+
+def download(url: str, file: str):
+ data = requests.get(url).content
+ parent = dirname(file)
+
+ if not exists(parent):
+ os.makedirs(parent)
+
+ with open(file, 'wb') as f:
+ f.write(data)
+
+
+def build(game: str, chapter: Optional[str]):
+ out = join(DIR, 'out', game)
+
+ raw = (
+ join(out, 'raw', chapter) if chapter is not None else join(out, 'raw')
+ )
+
+ input_dir = (
+ join(DIR, f'decompiled-{game}', chapter)
+ if chapter is not None
+ else join(DIR, f'decompiled-{game}')
+ )
+
+ logger.info(f'Building chapter: {chapter}')
+ logger.info('Finding script files...')
+
+ scripts: list[str] = []
+
+ for root, _, files in os.walk(input_dir):
+ for name in files:
+ if name.lower().endswith('.gml'):
+ scripts.append(join(root, name))
+
+ logger.info('Copying script files...')
+
+ for file in tqdm(scripts):
+ rel = relpath(file, input_dir)
+ target = join(raw, rel)
+ txt = join(raw, '.'.join(rel.split('.')[:-1]) + '.txt')
+ parent = dirname(target)
+
+ if not exists(parent):
+ os.makedirs(parent)
+
+ copyfile(file, target)
+ copyfile(file, txt)
+
+
+def run(game: str):
+ data = Data(game)
+ out = join(DIR, 'out', game)
+ static_out = join(out, 'static')
+
+ if exists(out):
+ logger.info('Clearing existing output...')
+ rmtree(out)
+
+ os.makedirs(out)
+
+ chapters = data.get_chapters()
+
+ if chapters is not None and len(chapters) > 0:
+ logger.info('Building chapters...')
+
+ for chapter in chapters.keys():
+ build(game, chapter)
+ else:
+ build(game, None)
+
+ logger.info('Copying static files...')
+
+ static_files: list[str] = []
+
+ for root, _, files in os.walk(STATIC):
+ for file in files:
+ static_files.append(relpath(join(root, file), STATIC))
+
+ os.makedirs(static_out)
+
+ for file in tqdm(static_files):
+ out_path = join(static_out, file)
+ parent = dirname(out_path)
+
+ if not exists(parent):
+ os.makedirs(parent)
+
+ copyfile(join(STATIC, file), out_path)
+
+ copyfile(join(DIR, '_headers'), join(out, '_headers'))
+
+ logger.info('Downloading font...')
+
+ font_url = (
+ 'https://download-cdn.jetbrains.com/fonts/JetBrainsMono-2.304.zip'
+ )
+ data = requests.get(font_url).content
+
+ with zipfile.ZipFile(BytesIO(data), 'r') as z:
+ z.extractall(static_out)
+
+ logger.info('Downloading highlight.js...')
+
+ hjs_base = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1'
+
+ download(
+ f'{hjs_base}/highlight.min.js',
+ join(static_out, 'highlight', 'highlight.min.js'),
+ )
+
+ download(
+ f'{hjs_base}/languages/gml.min.js',
+ join(static_out, 'highlight', 'gml.min.js'),
+ )
+
+ download(
+ f'{hjs_base}/styles/github-dark.min.css',
+ join(static_out, 'highlight', 'github-dark.min.css'),
+ )
+
+ logger.info('Generating website...')
+
+ generate(game)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description='Generates the code viewer website.'
+ )
+
+ parser.add_argument(
+ 'game',
+ type=str,
+ help='game for which to generate the website',
+ )
+
+ args = parser.parse_args()
+
+ run(args.game)
diff --git a/build.sh b/build.sh
deleted file mode 100755
index 72483cca..00000000
--- a/build.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-set -e
-cd "${0%/*}"
-rm -rf out
-mkdir -p out
-cp -rL "decompiled-$1" out/raw
-find out/raw -name "*.gml" -exec sh -c 'cp "$1" "${1%.gml}.txt"' _ {} \;
-cp -r static out
-cp _headers out
-python generate.py "$1"
diff --git a/data.py b/data.py
index f352a175..c48f9fbe 100644
--- a/data.py
+++ b/data.py
@@ -19,7 +19,7 @@ class Config:
game: str
links: Dict[str, str]
cache: int
- chapters: Optional[List[str]] = None
+ chapters: Optional[Dict[str, str]] = None
footer: Optional[str] = None
@@ -32,7 +32,8 @@ def __init__(self, game: str):
self.sums: Optional[Dict[str, str]] = None
self.lang: Optional[Dict[str, str]] = None
self.config: Optional[Config] = None
- self.chapter: int = -1
+ self.chapter: Optional[str] = None
+ self.chapter_id: Optional[str] = None
def load_json(self, filename: str) -> Any:
script_dir = get_script_path()
@@ -42,7 +43,9 @@ def load_json(self, filename: str) -> Any:
def load_textdata(self, scriptname: str) -> Dict[str, str]:
script_dir = get_script_path()
- lang_file = script_dir / 'out' / 'raw' / f'{scriptname}.gml'
+ lang_file = (
+ script_dir / 'out' / self.game / 'raw' / f'{scriptname}.gml'
+ )
ret = {}
textdata_regex = re.compile(
r'ds_map_add\(global\.text_data_[a-z]+, '
@@ -100,7 +103,10 @@ def get_room_by_name(self, room_name: str) -> Optional[Room]:
if self.rooms is None:
self.rooms = self.load_rooms()
for room in self.rooms:
- if room.name == room_name:
+ if room.name == room_name or (
+ self.chapter is not None
+ and room.name == f'{room_name}_{self.chapter_id}'
+ ):
return room
return None
@@ -122,8 +128,8 @@ def get_localized_string_ch1(self, key: str) -> str:
def get_game_name(self) -> str:
if self.config is None:
self.config = self.load_config()
- if self.chapter >= 0:
- return f'{self.config.game} (Chapter {self.chapter + 1})'
+ if self.chapter_id is not None and self.chapter_id != '':
+ return f'{self.config.game} ({self.chapter})'
return self.config.game
def get_game_links(self) -> Dict[str, str]:
@@ -141,10 +147,13 @@ def get_cache_version(self) -> int:
self.config = self.load_config()
return self.config.cache
- def get_chapters(self) -> Optional[List[str]]:
+ def get_chapters(self) -> Optional[Dict[str, str]]:
if self.config is None:
self.config = self.load_config()
return self.config.chapters
- def select_chapter(self, chapter_idx: int):
- self.chapter = chapter_idx
+ def select_chapter(
+ self, chapter_id: Optional[str], chapter: Optional[str]
+ ):
+ self.chapter_id = chapter_id
+ self.chapter = chapter
diff --git a/data/deltarune/config.json b/data/deltarune/config.json
index f2481198..5a3ada23 100644
--- a/data/deltarune/config.json
+++ b/data/deltarune/config.json
@@ -1,6 +1,12 @@
{
"game": "Deltarune",
- "chapters": ["ch1", "ch2", "ch3", "ch4"],
+ "chapters": {
+ "init": "Chapter Select",
+ "ch1": "Chapter 1",
+ "ch2": "Chapter 2",
+ "ch3": "Chapter 3",
+ "ch4": "Chapter 4"
+ },
"links": {
"Source code": "https://github.com/utdrwiki/code-viewer",
"r/Underminers": "https://www.reddit.com/r/Underminers/",
diff --git a/dev.sh b/dev.sh
index 004b2c2b..184a2332 100755
--- a/dev.sh
+++ b/dev.sh
@@ -1,3 +1,3 @@
#!/bin/bash
cd "${0%/*}"
-python -m http.server -d out
+python -m http.server -d out/"$1"
diff --git a/generate.py b/generate.py
index 2a6d5c90..21474586 100755
--- a/generate.py
+++ b/generate.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+
import argparse
import hashlib
import os
@@ -6,104 +7,166 @@
from typing import Dict, List, Set
from jinja2 import Environment, FileSystemLoader, select_autoescape
+from loguru import logger
+from tqdm import tqdm
from data import Data
from index import ScriptIndex, process_scripts, write_index
from script import render_script, write_script
from util import get_script_path
-AggregateIndex = Dict[str, List[int]]
-
+type AggregateIndex = Dict[str, List[str]]
env = Environment(
loader=FileSystemLoader('templates'),
- autoescape=select_autoescape(['html'])
+ autoescape=select_autoescape(['html']),
)
def process_indices(indices: List[ScriptIndex], data: Data) -> AggregateIndex:
aggregate: AggregateIndex = {}
checksums: Dict[str, Set[str]] = {}
- for chapter_idx, index in enumerate(indices):
+
+ logger.info('Processing indices...')
+
+ for chapter_idx, index in tqdm(enumerate(indices)):
for script, text in index.text.items():
if script not in aggregate:
aggregate[script] = []
checksums[script] = set()
+
fulltext_bytes = '\n'.join(text).encode('utf-8')
checksum = hashlib.md5(fulltext_bytes).hexdigest()
+ chapters = data.get_chapters()
+
+ if chapters is None:
+ raise Exception('No chapters list found!')
+
if checksum not in checksums[script]:
+ chapter = list(chapters.keys())[chapter_idx]
+
checksums[script].add(checksum)
- aggregate[script].append(chapter_idx)
+ aggregate[script].append(chapter)
+
return aggregate
def write_redirects(aggregate: AggregateIndex, data: Data, output_dir: Path):
redirects: Dict[str, str] = {}
- chapters = data.get_chapters() or []
- for script, chapter_indices in aggregate.items():
+ chapters = data.get_chapters() or {}
+
+ logger.info('Writing redirects...')
+
+ for script, chapter_indices in tqdm(aggregate.items()):
if len(chapter_indices) > 1:
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
with open(output_dir / f'{script}.html', 'w') as disambig_file:
- disambig_file.write(env.get_template('disambig.html').render(
- script_name=script,
- chapters=[chapters[idx] for idx in chapter_indices],
- game=data.get_game_name(),
- links=data.get_game_links(),
- footer=data.get_game_footer(),
- ))
+ disambig_file.write(
+ env.get_template('disambig.html').render(
+ script_name=script,
+ chapters=[chapters[idx] for idx in chapter_indices],
+ game=data.get_game_name(),
+ links=data.get_game_links(),
+ footer=data.get_game_footer(),
+ )
+ )
else:
chapter = chapters[chapter_indices[0]]
redirects[f'/{script}*'] = f'/{chapter}/{script}.html'
+
with open(output_dir / '_redirects', 'w') as redirects_file:
for old_path, new_path in redirects.items():
redirects_file.write(f'{old_path} {new_path}\n')
def write_chapter_index(data: Data, output_dir: Path):
- chapters = data.get_chapters() or []
+ chapters = (data.get_chapters() or {}).items()
+
+ logger.info('Rendering chapter index...')
+
with open(output_dir / 'index.html', 'w') as index_file:
- index_file.write(env.get_template('chapters.html').render(
- chapters=chapters,
- game=data.get_game_name(),
- links=data.get_game_links(),
- footer=data.get_game_footer(),
- ))
+ index_file.write(
+ env.get_template('chapters.html').render(
+ chapters=chapters,
+ game=data.get_game_name(),
+ links=data.get_game_links(),
+ footer=data.get_game_footer(),
+ )
+ )
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(
- description='Generates the code viewer website.'
- )
- parser.add_argument(
- 'game',
- type=str,
- help='game for which to generate the website'
- )
- args = parser.parse_args()
- data = Data(args.game)
+def generate(game: str):
+ data = Data(game)
chapters = data.get_chapters()
script_dir = get_script_path()
- decompiled_dir = script_dir / f'decompiled-{args.game}'
- output_dir = script_dir / 'out'
+ decompiled_dir = script_dir / f'decompiled-{game}'
+ output_dir = script_dir / 'out' / game
+
os.makedirs(output_dir, exist_ok=True)
+
if chapters is not None:
indices = []
- for chapter_idx, chapter in enumerate(chapters):
- decompiled_dir_ch = decompiled_dir / chapter
- output_dir_ch = output_dir / chapter
+
+ for chapter_id, chapter in chapters.items():
+ decompiled_dir_ch = decompiled_dir / chapter_id
+ output_dir_ch = output_dir / chapter_id
+
os.makedirs(output_dir_ch, exist_ok=True)
- data.select_chapter(chapter_idx)
+
+ data.select_chapter(chapter_id, chapter)
+
+ logger.info(f"['{chapter}'] Creating index...")
+
index = process_scripts(data, decompiled_dir_ch)
+
+ logger.info(f"['{chapter}'] Rendering index...")
+
write_index(index, data, output_dir_ch)
- for script in index.text.keys():
+
+ logger.info(f"['{chapter}'] Rendering scripts' pages...")
+
+ for script in tqdm(index.text.keys()):
rendered = render_script(script, index.text, data)
+
write_script(rendered, script, output_dir_ch)
- data.select_chapter(-1)
+
+ data.select_chapter(None, None)
indices.append(index)
+
+ logger.info('Linking chapters...')
+
write_redirects(process_indices(indices, data), data, output_dir)
write_chapter_index(data, output_dir)
else:
+ logger.info('Creating index...')
+
index = process_scripts(data, decompiled_dir)
+
+ logger.info('Rendering index...')
+
write_index(index, data, output_dir)
- for script in index.text.keys():
+
+ logger.info("Rendering scripts' pages...")
+
+ for script in tqdm(index.text.keys()):
rendered = render_script(script, index.text, data)
+
write_script(rendered, script, output_dir)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description='Generates the code viewer website.'
+ )
+
+ parser.add_argument(
+ 'game',
+ type=str,
+ help='game for which to generate the website',
+ )
+
+ args = parser.parse_args()
+
+ generate(args.game)
diff --git a/index.py b/index.py
index 561f3431..62ebc003 100755
--- a/index.py
+++ b/index.py
@@ -47,15 +47,14 @@ def classify(path: Path, data: Data) -> Tuple[SectionType, str]:
if filename.startswith('gml_Object_'):
obj_name_match = re.match(
r'gml_Object_(.*)_((\w+)_(\d+)|Collision_(.*))\.gml',
- filename
+ filename,
)
if obj_name_match is None:
raise ValueError(f'Failed to find object name: {filename}')
return 'object', obj_name_match.group(1)
if filename.startswith('gml_RoomCC_'):
room_name_match = re.match(
- r'gml_RoomCC_(.+)_(\d+)_(\w+)\.gml',
- filename
+ r'gml_RoomCC_(.+)_(\d+)_(\w+)\.gml', filename
)
if room_name_match is None:
raise ValueError(f'Failed to find room name: {filename}')
@@ -69,13 +68,16 @@ def classify(path: Path, data: Data) -> Tuple[SectionType, str]:
def process_scripts(data: Data, decompiled_dir: Path) -> ScriptIndex:
- index = ScriptIndex({
- 'script': Section('Scripts'),
- 'object': Section('Objects'),
- 'roomcc': Section('Room Creation Codes'),
- 'room': Section('Rooms'),
- 'junk': Section('Duplicated or common scripts'),
- }, {})
+ index = ScriptIndex(
+ {
+ 'script': Section('Scripts'),
+ 'object': Section('Objects'),
+ 'roomcc': Section('Room Creation Codes'),
+ 'room': Section('Rooms'),
+ 'junk': Section('Duplicated or common scripts'),
+ },
+ {},
+ )
files = sorted(f for f in os.listdir(decompiled_dir) if f.endswith('.gml'))
for file in files:
filename = decompiled_dir / file
@@ -84,17 +86,19 @@ def process_scripts(data: Data, decompiled_dir: Path) -> ScriptIndex:
section, segment = classify(filename, data)
if segment not in index.sections[section].entries:
index.sections[section].entries[segment] = []
- chapters = data.get_chapters()
- if chapters is None:
+ if data.chapter_id is None:
chapter_segment = ''
else:
- chapter_segment = f'/{chapters[data.chapter]}'
- index.sections[section].entries[segment].append(Entry(
- url=file.replace('.gml', '.html'),
- raw_url=f"/raw{chapter_segment}/{file.replace('.gml', '.txt')}",
- name=name,
- lines=len(lines)
- ))
+ chapter_segment = f'/{data.chapter_id}'
+ entry_name = file.replace('.gml', '.txt')
+ index.sections[section].entries[segment].append(
+ Entry(
+ url=file.replace('.gml', '.html'),
+ raw_url=f'/raw{chapter_segment}/{entry_name}',
+ name=name,
+ lines=len(lines),
+ )
+ )
index.text[name] = lines
return index
@@ -102,13 +106,15 @@ def process_scripts(data: Data, decompiled_dir: Path) -> ScriptIndex:
def write_index(index: ScriptIndex, data: Data, output_dir: Path) -> None:
with open(output_dir / 'index.html', 'w', encoding='utf-8') as f:
env = Environment(loader=FileSystemLoader('templates'))
- f.write(env.get_template('index.html').render(
- sections=index.sections,
- game=data.get_game_name(),
- links=data.get_game_links(),
- cache_version=data.get_cache_version(),
- footer=data.get_game_footer(),
- ))
+ f.write(
+ env.get_template('index.html').render(
+ sections=index.sections,
+ game=data.get_game_name(),
+ links=data.get_game_links(),
+ cache_version=data.get_cache_version(),
+ footer=data.get_game_footer(),
+ )
+ )
with open(output_dir / 'index.json', 'w', encoding='utf-8') as f:
json.dump(index.text, f, separators=(',', ':'))
@@ -120,7 +126,7 @@ def write_index(index: ScriptIndex, data: Data, output_dir: Path) -> None:
parser.add_argument(
'game',
type=str,
- help='game for which to generate the website'
+ help='game for which to generate the website',
)
args = parser.parse_args()
data = Data(args.game)
diff --git a/requirements.txt b/requirements.txt
index 8e1edb74..03fac34a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,6 @@ flake8-tidy-imports
isort
pep8-naming
pyright
+loguru
+tqdm
+requests
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 00000000..40c48586
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,6 @@
+line-length = 79
+
+[format]
+quote-style = "single"
+indent-style = "space"
+docstring-code-format = true
diff --git a/script.py b/script.py
index 4742d06e..b96feffa 100755
--- a/script.py
+++ b/script.py
@@ -14,7 +14,7 @@
env = Environment(
loader=FileSystemLoader('templates'),
- autoescape=select_autoescape(['html'])
+ autoescape=select_autoescape(['html']),
)
@@ -22,45 +22,43 @@ def parse_text(text: str) -> str:
text = re.sub(
r'(?Wait for input',
- text
+ text,
)
text = re.sub(
r'\^([1-9])(.)',
r'\2Delay \1\1',
- text
+ text,
)
text = re.sub(r'(?', text)
text = re.sub(
r'(?Close Message',
- text
+ text,
)
text = re.sub(
- r'\\\\[EM](.)',
- r'Face \1',
- text
+ r'\\\\[EM](.)', r'Face \1', text
)
text = re.sub(
r'\\\\m(.)\*?',
r'Mini face \1 ',
- text
+ text,
)
text = re.sub(
r'\\\\f(.)\*?',
r'Mini text \1 ',
- text
+ text,
)
text = re.sub(
r'\\\\c(.)(.*?)(?=\\\\c|$)',
r'\2',
- text
+ text,
)
text = re.sub(r'\\\\T(.)', r'Sound \1', text)
text = re.sub(r'\\\\F(.)', r'Char \1', text)
text = re.sub(
r'\\\\C(.)',
r'Choice type \1',
- text
+ text,
)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'`(.)', r'\1', text)
@@ -72,7 +70,7 @@ def highlight_text(matches: re.Match[str]) -> str:
before_var='"',
variable=matches[2],
after_var=matches[3],
- parsed_text=parse_text(matches[2])
+ parsed_text=parse_text(matches[2]),
)
@@ -81,7 +79,7 @@ def highlight_text_ch1(matches: re.Match[str], data: Data) -> str:
before_var=matches[1],
variable=matches[2],
after_var=matches[3],
- parsed_text=parse_text(data.get_localized_string_ch1(matches[2]))
+ parsed_text=parse_text(data.get_localized_string_ch1(matches[2])),
)
@@ -95,7 +93,7 @@ def highlight_room(matches: re.Match[str], data: Data) -> str:
return env.get_template('highlight/room.html').render(
before_room=matches[1],
room_name=room.name,
- room_description=room.description
+ room_description=room.description,
)
@@ -104,7 +102,7 @@ def highlight_enemy(matches: re.Match[str], data: Data) -> str:
return env.get_template('highlight/enemy.html').render(
before_enemy=matches[1],
enemy_id=enemy_id,
- enemy_name=data.get_enemy(enemy_id)
+ enemy_name=data.get_enemy(enemy_id),
)
@@ -115,14 +113,14 @@ def highlight_flag(matches: re.Match[str], data: Data) -> str:
return env.get_template('highlight/flag_not_found.html').render(
before_flag=matches[1],
flag_id=flag_id,
- after_flag=matches[3]
+ after_flag=matches[3],
)
else:
return env.get_template('highlight/flag_found.html').render(
before_flag=matches[1],
flag_id=flag_id,
flag_description=flag_description,
- after_flag=matches[3]
+ after_flag=matches[3],
)
@@ -131,7 +129,7 @@ def highlight_function(
script_name: str,
text: Dict[str, List[str]],
data: Data,
- resolve_references: bool
+ resolve_references: bool,
) -> str:
function_name = matches[2]
script_name = f'gml_GlobalScript_{function_name}'
@@ -156,7 +154,7 @@ def highlight_function(
before_function=matches[1],
script_name=script_name,
function_name=function_name,
- script_content=script_content
+ script_content=script_content,
)
@@ -165,7 +163,7 @@ def highlight_alarm(
script_name: str,
text: Dict[str, List[str]],
data: Data,
- resolve_references: bool
+ resolve_references: bool,
) -> str:
before_alarm = matches[1]
alarm_content = matches[2]
@@ -194,7 +192,7 @@ def highlight_alarm(
alarm_content=alarm_content,
script_name=script_name,
script_content=script_content,
- content_rest=content_rest
+ content_rest=content_rest,
)
@@ -203,73 +201,74 @@ def process_line(
script_name: str,
text: Dict[str, List[str]],
data: Data,
- resolve_references: bool = True
+ resolve_references: bool = True,
) -> str:
# Highlight localized strings
line = re.sub(
r'([A-Za-z0-9_]+loc\((?:\d+, )?)"((?:[^"\\]|\\.)+)(", "[a-z0-9_-]+")\)', # noqa: E501
lambda matches: matches[1] + highlight_text(matches) + ')',
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
line = re.sub(
r'(scr_(?:84_get_lang_string(?:_ch1)?|gettext)\(")([a-zA-Z0-9_-]+)("\))', # noqa: E501
lambda matches: highlight_text_ch1(matches, data),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
# Highlight flags, rooms and enemies
line = re.sub(
r'(global\.flag\[)(\d+)(\])',
lambda matches: highlight_flag(matches, data),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
line = re.sub(
r'(room_goto\()([A-Za-z0-9_]+)',
lambda matches: highlight_room(matches, data),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
line = re.sub(
r'(global\.monstertype\b.*[!=]+\s*)(\d+)',
lambda matches: highlight_enemy(matches, data),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
# Link to functions and alarms
line = re.sub(
r'(\b)(s?cr?_[a-zA-Z0-9_]+)\(',
- lambda matches: highlight_function(matches, script_name, text, data,
- resolve_references),
+ lambda matches: highlight_function(
+ matches, script_name, text, data, resolve_references
+ ),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
line = re.sub(
r'(^|\s+)(alarm\[(\d+)\])(.*)',
- lambda matches: highlight_alarm(matches, script_name, text, data,
- resolve_references),
+ lambda matches: highlight_alarm(
+ matches, script_name, text, data, resolve_references
+ ),
line,
- flags=re.IGNORECASE
+ flags=re.IGNORECASE,
)
+ line = f"{line}"
+
return line
def render_script(
- script_name: str,
- text: Dict[str, List[str]],
- data: Data
+ script_name: str, text: Dict[str, List[str]], data: Data
) -> str:
lines = [
process_line(line, script_name, text, data)
for line in text[script_name]
]
- chapters = data.get_chapters()
- if chapters is None:
+ if data.chapter_id is None:
chapter_segment = ''
else:
- chapter_segment = f'/{chapters[data.chapter]}'
+ chapter_segment = f'/{data.chapter_id}'
return env.get_template('script_page.html').render(
script_name=script_name,
raw_url=f'/raw{chapter_segment}/{script_name}.txt',
@@ -293,7 +292,7 @@ def write_script(output: str, script_name: str, output_dir: Path):
parser.add_argument(
'game',
type=str,
- help='game for which to generate the website'
+ help='game for which to generate the website',
)
args = parser.parse_args()
data = Data(args.game)
diff --git a/static/fonts.css b/static/fonts.css
new file mode 100644
index 00000000..2543b39e
--- /dev/null
+++ b/static/fonts.css
@@ -0,0 +1,15 @@
+@font-face {
+ font-family: 'JetBrains Mono';
+ src: url('/static/fonts/webfonts/JetBrainsMono-Bold.woff2') format('woff2');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+
+@font-face {
+ font-family: 'JetBrains Mono';
+ src: url('/static/fonts/webfonts/JetBrainsMono-Regular.woff2') format('woff2');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
diff --git a/static/index.css b/static/index.css
index 6a9aea6c..bbae3e12 100644
--- a/static/index.css
+++ b/static/index.css
@@ -1,7 +1,7 @@
input {
background: var(--secondary-background);
padding: 0.1em 0.5em;
- font-family: monospace;
+ font-family: JetBrains Mono, monospace;
color: var(--text-color);
border: 1px solid var(--border-color);
font-size: inherit;
diff --git a/static/main.css b/static/main.css
index d6102beb..bd0328a8 100644
--- a/static/main.css
+++ b/static/main.css
@@ -46,13 +46,20 @@
--function-box-background-color: rgba(30, 50, 30, 0.9);
}
+html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
body {
background: var(--background);
- padding: 1em 2em;
}
body, pre {
- font-family: Consolas, Ubuntu Mono, monospace;
+ font-family: Consolas, JetBrains Mono, monospace;
font-size: 12pt;
color: var(--text-color);
margin: 0;
@@ -86,11 +93,11 @@ h2 {
#yrstruly {
position: relative;
- right: -1em;
float: right;
background: var(--secondary-background);
padding: 1em 2em;
border: 1px solid var(--border-color);
+ margin: 0;
}
.comma-list {
@@ -120,7 +127,46 @@ h2 {
footer {
background-color: var(--secondary-background);
- clear: both;
- margin-top: 20px;
- padding: 20px;
+ padding: 1rem;
+
+ display: flex;
+ flex-direction: column;
+
+ gap: 1rem;
+ width: calc(100% - 2rem);
+}
+
+fieldset p {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ column-gap: 0.5rem;
+}
+
+fieldset legend {
+ padding: 0 1rem;
+}
+
+main {
+ padding: 2rem;
+}
+
+input[type=submit] {
+ cursor: pointer;
+ color: var(--text-color);
+ background-color: var(--background);
+
+ transition:
+ color 0.125s ease-in-out,
+ background-color 0.125s ease-in-out;
+}
+
+input[type=submit]:hover {
+ color: var(--background);
+ background-color: var(--text-color);
+}
+
+.search-menu {
+ padding-bottom: 1rem;
}
diff --git a/static/script.css b/static/script.css
index b5c02b8e..171a8072 100644
--- a/static/script.css
+++ b/static/script.css
@@ -1,3 +1,15 @@
+pre:not(.funcCode), code, .hljs {
+ font-family: "JetBrains Mono";
+}
+
+.code .selected {
+ background-color: var(--selected-line-background-color);
+}
+
+.inline-code {
+ display: inline-block;
+}
+
.langvar {
color: var(--lang-var-text-color);
font-style: italic;
@@ -264,7 +276,3 @@
.code tr td:first-child {
user-select: none;
}
-
-.code .selected {
- background-color: var(--selected-line-background-color);
-}
diff --git a/static/script.js b/static/script.js
index 5fb18923..6125af81 100644
--- a/static/script.js
+++ b/static/script.js
@@ -2,19 +2,70 @@
'use strict';
function highlightHash() {
const selectedRow = document.querySelector('.code .selected');
+
if (selectedRow) {
selectedRow.classList.remove('selected');
}
+
const hash = window.location.hash;
+
if (!hash || !hash.startsWith('#L')) {
return;
}
+
const elem = document.getElementById(hash.substring(1));
+
if (!elem) {
return;
}
+
elem.parentElement.classList.add('selected');
}
+
highlightHash();
- window.addEventListener('hashchange', highlightHash);
+
+ const elements = [
+ ...document.getElementsByClassName("code-line"),
+ ];
+
+ // Unfortunately a standard document tree walker doesn't work here - it misses quite a lot of nodes.
+ // ¯\_(ツ)_/¯
+
+ /** @param {Node} el */
+ const getTextNodes = (el) => {
+ const nodes = [];
+
+ for (const child of el.childNodes) {
+ if (
+ child.nodeType == Node.ELEMENT_NODE &&
+ child.classList.contains("highlighted")
+ ) continue;
+
+ if (child.nodeType == Node.TEXT_NODE) {
+ nodes.push(child);
+ } else {
+ nodes.push(...getTextNodes(child));
+ }
+ }
+
+ return nodes;
+ };
+
+ for (const el of elements) {
+ // Highlighting has to be done super carefully and manually like this, otherwise
+ // the annotations won't show up and get overwritten by highlight.js.
+
+ for (const node of getTextNodes(el)) {
+ if (node.textContent.trim() == "") continue;
+
+ const replacement = document.createElement("code");
+
+ replacement.classList.add("highlighted");
+ replacement.innerHTML = hljs.highlight(node.textContent, { language: "gml" }).value;
+
+ node.replaceWith(replacement);
+ }
+ }
+
+ window.addEventListener("hashchange", highlightHash);
})();
diff --git a/static/search.js b/static/search.js
index 1979704a..f946a2a2 100644
--- a/static/search.js
+++ b/static/search.js
@@ -112,7 +112,7 @@
hitLineLink.textContent = `${line.index}:`;
hitLineNumber.appendChild(hitLineLink);
hitRow.appendChild(hitLineNumber);
- hitLine.textContent = line.content;
+ hitLine.innerHTML = hljs.highlight(line.content, { language: "gml" }).value;
hitRow.appendChild(hitLine);
searchHits.appendChild(hitRow);
hitRow.classList.add('table-subsection-content');
diff --git a/templates/chapters.html b/templates/chapters.html
index 22f62872..985ea0f2 100644
--- a/templates/chapters.html
+++ b/templates/chapters.html
@@ -1,23 +1,27 @@
-
+
-
This script exists in multiple chapters:
-This script exists in multiple chapters:
-{{ footer }}
+ {% endif %} +{{ script_name }}.gml
{{ script_content | safe }}
{{ function_name }}
{{ script_content | safe }}
| Name | +Lines | +Raw script | +
|---|---|---|
| {{ segment }} | +||
| Name | -Lines | -Raw script | -
|---|---|---|
| {{ segment }} | -||
| {{ entry.name }} | -{{ entry.lines }} | -raw | -
| {{ entry.name }} | +{{ entry.lines }} | +raw | +
{{ footer }}
+ {% endif %} +