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 @@ - + - - {{ game }} script viewer - {% include 'partials/head.html' %} - - - -

{{ game }} script viewer

+ + {{ game }} script viewer + {% include 'partials/head.html' %} + + + +
+

{{ game }} script viewer

-{% include 'partials/info.html' %} + +
- + + diff --git a/templates/disambig.html b/templates/disambig.html index 99b52169..21ef51a2 100644 --- a/templates/disambig.html +++ b/templates/disambig.html @@ -1,23 +1,29 @@ - + - - {{ game }} script viewer - {% include 'partials/head.html' %} - - - -

{{ game }} script viewer

+ + {{ game }} script viewer + {% include 'partials/head.html' %} + + + +
+

{{ game }} script viewer

+

{{ script_name }}

+

This script exists in multiple chapters:

-

{{ script_name }}

-

This script exists in multiple chapters:

- + +
-{% if footer %} - -{% endif %} - + + diff --git a/templates/highlight/alarm.html b/templates/highlight/alarm.html index 0e48683e..35b09370 100644 --- a/templates/highlight/alarm.html +++ b/templates/highlight/alarm.html @@ -1,9 +1,9 @@ -{{ before_alarm }}{{ alarm_content }}{{ content_rest }}{% if script_content %}
{{ script_name }}.gml

{{ script_content | safe }}
{% endif %}
diff --git a/templates/highlight/function.html b/templates/highlight/function.html index e52e114c..cdfddf92 100644 --- a/templates/highlight/function.html +++ b/templates/highlight/function.html @@ -1,11 +1,11 @@ -{{ before_function }}{{ function_name }}{% if script_content %}
{{ function_name }}

{{ script_content | safe }}
{% endif %}
( diff --git a/templates/highlight/text.html b/templates/highlight/text.html index 09946484..bf6d77aa 100644 --- a/templates/highlight/text.html +++ b/templates/highlight/text.html @@ -1 +1 @@ -
{{ parsed_text | safe }}
{{ before_var }}{{ variable }}{{ after_var }}
+
{{ parsed_text | safe }}
{{ before_var }}{{ variable }}{{ after_var }}
diff --git a/templates/index.html b/templates/index.html index 37404682..bc63a9d3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,46 +1,54 @@ - + - - {{ game }} script viewer - {% include 'partials/head.html' %} - - - - -

{{ game }} script viewer

+ + {{ game }} script viewer + {% include 'partials/head.html' %} + + + + +
+

{{ game }} script viewer

+ + {% include 'partials/search.html' %} + + {% for section_type, section in sections.items() %} +

{{ section.name }}

-{% include 'partials/info.html' %} + + + + + + + + -{% include 'partials/search.html' %} + + {% for segment, entries in section.entries.items() %} + + + -{% for section_type, section in sections.items() %} -

{{ section.name }}

-
NameLinesRaw script
{{ segment }}
- - - - - - - - - {% for segment, entries in section.entries.items() %} - - - - {% for entry in entries %} - - - - - - {% endfor %} + {% for entry in entries %} + + + + + + {% endfor %} + {% endfor %} + +
NameLinesRaw script
{{ segment }}
{{ entry.name }}{{ entry.lines }}raw
{{ entry.name }}{{ entry.lines }}raw
{% endfor %} - - -{% endfor %} -{% if footer %} - -{% endif %} - +
+ + + diff --git a/templates/partials/head.html b/templates/partials/head.html index 831cd925..b8df119f 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -1,4 +1,8 @@ - + + + + + diff --git a/templates/partials/info.html b/templates/partials/info.html index bad1c778..7a3d7083 100644 --- a/templates/partials/info.html +++ b/templates/partials/info.html @@ -1,14 +1,17 @@ diff --git a/templates/partials/search.html b/templates/partials/search.html index df2ec182..23822bbe 100644 --- a/templates/partials/search.html +++ b/templates/partials/search.html @@ -1,10 +1,11 @@
-

+

+
Search options

diff --git a/templates/script_page.html b/templates/script_page.html index 3640e26f..e534cc22 100644 --- a/templates/script_page.html +++ b/templates/script_page.html @@ -1,33 +1,43 @@ - + - - {{ game }} script viewer - {{ script_name }} - {% include 'partials/head.html' %} - - - - -

{{ game }} script viewer

+ + {{ game }} script viewer - {{ script_name }} + {% include 'partials/head.html' %} + + + + +
+

{{ game }} script viewer

-{% include 'partials/info.html' %} + ← back to main script listing
-← back to main script listing
+

{{ script_name }}

+ (view raw script w/o annotations or w/e) -

{{ script_name }}

-(view raw script w/o annotations or w/e) + + {% for line in lines %} + + -
+ {{ loop.index }} +
- {% for line in lines %} - - - - - {% endfor %} -
{{ loop.index }}
{{ line | safe }}
+ +
{{ line | safe }}
+ + + {% endfor %} + +
-{% if footer %} -
{{ footer }}
-{% endif %} - - +
+ {% include 'partials/info.html' %} + + {% if footer %} +

{{ footer }}

+ {% endif %} +
+ + +