|
#!/usr/bin/env python3 |
|
# |
|
# Embeds Bitsy games into a Twine story. |
|
# |
|
# Bitsy games should be exported as .html and renamed as `bitsy-foo.html`, where `foo` can be replaced |
|
# with any other id. |
|
# In Twine, games can be embedded as <canvas id="bitsy-foo"></canvas>, where `foo` matches the id |
|
# of the Bitsy game. |
|
|
|
# WARNING: the following code sucks complete ass and was a mistake to have even written. |
|
|
|
import argparse |
|
from pathlib import Path |
|
|
|
parser = argparse.ArgumentParser(description='Embed Bitsy games into a Twine story.') |
|
parser.add_argument('twine', metavar='twine-story', type=Path, help='the exported Twine story') |
|
parser.add_argument('bitsies', metavar='bitsy-game', type=Path, nargs='+', help='an exported Bitsy game') |
|
parser.add_argument('output', type=Path, help='where to store the result') |
|
|
|
args = parser.parse_args() |
|
|
|
twine_path = args.twine |
|
bitsy_paths = args.bitsies |
|
output_path = args.output |
|
|
|
# This function from the Bitsy engine needs to be replaced with this version that takes an id |
|
# so that multiple Bitsy games can be played from the same HTML page. |
|
# Also, this function needs to exist so things don't explode. |
|
replacement_startExportedGame = ''' |
|
function startExportedGame(id) { |
|
console.log('STARTING GAME', id); |
|
attachCanvas(document.getElementById("bitsy-" + id)); |
|
load_game(document.getElementById("exportedGameData-" + id).text.slice(1)); |
|
} |
|
''' |
|
|
|
# This JS needs to be injected into the Twine story so the Bitsy games can be started. |
|
twine_hook = ''' |
|
var targetNode = document.querySelector('tw-story'); |
|
var config = { childList: true }; |
|
var callback = (mutationsList) => { |
|
for (var mutation of mutationsList) { |
|
for (var item of mutation.addedNodes) { |
|
if (item.className === 'transition-out') continue; |
|
var canvas = item.querySelector('canvas[id^=bitsy-]'); |
|
if (canvas !== null) { |
|
var id = canvas.id.replace(/^bitsy-/, ''); |
|
startExportedGame(id); |
|
} |
|
} |
|
} |
|
}; |
|
var observer = new MutationObserver(callback); |
|
observer.observe(targetNode, config); |
|
''' |
|
|
|
print('Parsing Bitsy games...') |
|
|
|
# We're going to need traversal later, so we might as well do this the right way now. |
|
from html.parser import HTMLParser as RawHTMLParser |
|
import re |
|
|
|
class HTMLParser(RawHTMLParser): |
|
def __init__(self, path, **kwargs): |
|
super().__init__(**kwargs) |
|
self.path = path |
|
self.feed(path.read_text('utf-8')) |
|
self.close() |
|
|
|
class BitsyParser(HTMLParser): |
|
def __init__(self, path): |
|
filename = path.name |
|
match_obj = re.fullmatch(r'bitsy-(.*).html', filename) |
|
self.id = match_obj.group(1) |
|
self.engine = '' |
|
self.expecting_engine = False |
|
self.game_data = '' |
|
self.expecting_game_data = False |
|
super().__init__(path) |
|
print('Adding Bitsy hook to Twine...') |
|
self.engine += twine_hook |
|
|
|
def handle_starttag(self, tag, attrs): |
|
if tag == 'script': |
|
if len(attrs) == 0: |
|
self.expecting_engine = True |
|
elif attrs[0] == ('type', 'bitsyGameData'): |
|
self.expecting_game_data = True |
|
|
|
def handle_data(self, data): |
|
if self.expecting_engine: |
|
if data.lstrip().startswith('function startExportedGame()'): |
|
print('Injecting support for multiple Bitsy games...') |
|
data = replacement_startExportedGame |
|
self.engine += data |
|
elif self.expecting_game_data: |
|
self.game_data += data |
|
|
|
def handle_endtag(self, tag): |
|
self.expecting_engine = False |
|
self.expecting_game_data = False |
|
|
|
class TwineParser(HTMLParser): |
|
def __init__(self, input_path, bitsy_parsed, output_path): |
|
self.bitsy_parsed = bitsy_parsed |
|
self.input_path = input_path |
|
self.output_path = output_path |
|
self.output_file = output_path.open(mode='w', encoding='utf-8') |
|
self.expecting_harlowe = False |
|
super().__init__(input_path, convert_charrefs=False) |
|
self.output_file.close() |
|
|
|
def print(self, text): |
|
print(text, end='', file=self.output_file) |
|
|
|
def parse_attrs(self, tag, attrs): |
|
if tag == 'script': |
|
if ('data-main', 'harlowe') in attrs: |
|
self.expecting_harlowe = True |
|
def process_attr(attr): |
|
k, v = attr |
|
if v is None: |
|
return k |
|
else: |
|
return k + '="' + v + '"' |
|
attr_texts = [' ' + process_attr(attr) for attr in attrs] |
|
return ''.join(attr_texts) |
|
|
|
def handle_starttag(self, tag, attrs): |
|
tag_text = '<' + tag + self.parse_attrs(tag, attrs) + '>' |
|
self.print(tag_text) |
|
|
|
def handle_endtag(self, tag): |
|
if tag == 'head': |
|
print('Adding game data to Twine file...') |
|
for bitsy_game in self.bitsy_parsed: |
|
id = bitsy_game.id |
|
self.print('\n<script type="bitsyGameData" id="exportedGameData-' + id + '">') |
|
self.print(bitsy_game.game_data) |
|
self.print('</script>\n') |
|
elif tag == 'body': |
|
print('Injecting Bitsy engine to Twine...') |
|
self.print('\n<script>') |
|
self.print(self.bitsy_parsed[0].engine) |
|
self.print('</script>\n') |
|
self.print('</' + tag + '>') |
|
|
|
def handle_startendtag(self, tag, attrs): |
|
tag_text = '<' + tag + self.parse_attrs(tag, attrs) + ' />' |
|
self.print(tag_text) |
|
|
|
def handle_data(self, data): |
|
if self.expecting_harlowe: |
|
print('Disabling JS strict mode...') |
|
data = data.replace('"use strict";', '', 1) |
|
self.expecting_harlowe = False |
|
self.print(data) |
|
|
|
def handle_entityref(self, name): |
|
self.print('&' + name + ';') |
|
|
|
def handle_charref(self, name): |
|
self.print('&#' + name + ';') |
|
|
|
def handle_comment(self, name): |
|
print('AAA comment') |
|
|
|
def handle_decl(self, decl): |
|
self.print('<!' + decl + '>') |
|
|
|
def handle_pi(data): |
|
print('AAAA Processing Instruction') |
|
|
|
bitsy_parsed = [BitsyParser(x) for x in bitsy_paths] |
|
twine_parsed = TwineParser(twine_path, bitsy_parsed, output_path) |
|
print('All done!') |