Skip to content

Instantly share code, notes, and snippets.

@worldoptimizer
Last active April 27, 2023 16:27
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save worldoptimizer/68dcb3a3d39f46a7e3eabd59e7afddff to your computer and use it in GitHub Desktop.
Save worldoptimizer/68dcb3a3d39f46a7e3eabd59e7afddff to your computer and use it in GitHub Desktop.
Hype Template
#!/usr/bin/python
# HypeTemplate.hype-export.py
# Export Script for Tumult Hype to produce offer a generic template system
#
# MIT License
# Copyright (c) 2022 Max Ziebell
#
import argparse
import json
import sys
import distutils.util
import os
# update info
# current_script_version = 1
# version_info_url = ".../latest_script_version.txt" # only returns a version number
# download_url = "..." # gives a user info to download and install
# minimum_update_check_duration_in_seconds = 60 * 60 * 24 # once a day
# defaults_bundle_identifier = "com.tumult.Hype2.hype-export.HypeTemplate"
import sys
from os import path
class HypeURLType:
Unknown = 0
HypeJS = 1
Resource = 2
Link = 3
ResourcesFolder = 4
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--hype_version')
parser.add_argument('--hype_build')
parser.add_argument('--export_uid')
parser.add_argument('--get_options', action='store_true')
parser.add_argument('--replace_url')
parser.add_argument('--url_type')
parser.add_argument('--is_reference', default="False")
parser.add_argument('--should_preload')
parser.add_argument('--modify_staging_path')
parser.add_argument('--destination_path')
parser.add_argument('--export_info_json_path')
parser.add_argument('--is_preview', default="False")
parser.add_argument('--check_for_updates', action='store_true')
args, unknown = parser.parse_known_args()
## --get_options
## return arguments to be presented in the Hype UI as a dictionary:
## 'export_options' is a dictionary of key/value pairs that make modifications to Hype's export/preview system. Some useful ones:
## 'exportShouldInlineHypeJS' : boolean
## 'exportShouldInlineDocumentLoader' : boolean
## 'exportShouldUseExternalRuntime' : boolean
## 'exportExternalRuntimeURL' : string
## 'exportShouldSaveHTMLFile' : boolean
## 'indexTitle' : string
## 'exportShouldBustBrowserCaching' : boolean
## 'exportShouldIncludeTextContents' : boolean
## 'exportShouldIncludePIE' : boolean
## 'exportSupportInternetExplorer6789' : boolean
## 'initialSceneIndex' : integer
## 'save_options' is a dictionary of key/value pairs that for determining when/how to export. valid keys:
## 'file_extension' : the final extension when exported (ex. "zip")
## 'allows_export' : should show up in the File > Export as HTML5 menu and Advanced Export
## 'allows_preview' : should show up in the Preview menu, if so --is_preview True is passed into the --modify_staging_path call
## 'document_arguments' should be an array of keys, these will be passed to subsequent calls via --key value
## 'extra_actions' should be an array of dictionaries
## 'label': string that is the user presented name
## 'function': javascript function to call if this action is triggered, just the name of it
## 'arguments': array of dictionaries that represent arguments passed into the function
## 'label': string that is presented to Hype UI
## 'type': string that is either "String" (will be quoted and escaped) or "Expression" (passed directly to function argument as-is)
if args.get_options:
def export_options():
return {
"exportShouldInlineHypeJS" : False,
"exportShouldInlineDocumentLoader" : False,
#"exportShouldUseExternalRuntime" : False,
#"exportExternalRuntimeURL" : "",
"exportShouldSaveHTMLFile" : True,
"exportShouldNameAsIndexDotHTML" : True,
#"indexTitle" : "",
"exportShouldBustBrowserCaching" : False,
"exportShouldIncludeTextContents" : False,
"exportShouldIncludePIE" : False,
"exportSupportInternetExplorer6789" : False,
"exportShouldSaveRestorableDocument" : False,
}
def document_arguments():
return [
"Template name",
]
def save_options():
return {
"file_extension" : "zip",
"allows_export" : True,
"allows_preview" : True,
}
options = {
"export_options" : export_options(),
"document_arguments" : document_arguments(),
"save_options" : save_options(),
"min_hype_build_version" : "574", # build number (ex "574") and *not* marketing version (ex "3.6.0")
#"max_hype_build_version" : "10000", # build number (ex "574") and *not* marketing version (ex "3.6.0")
}
exit_with_result(options)
## --replace_url [url] --url_type [HypeURLType] --is_reference [True|False] --should_preload [None|True|False] --is_preview [True|False] --export_uid [identifier]
## return a dictionary with "url", "is_reference", and optional "should_preload" keys
## if HypeURLType.ResourcesFolder, you can set the url to "." so there is no .hyperesources folder and everything
## is placed next to the .html file
## should_preload may be None type in cases where it won't be used
elif args.replace_url != None:
url_info = {}
url_info['is_reference'] = bool(distutils.util.strtobool(args.is_reference))
if args.should_preload != None:
url_info['should_preload'] = bool(distutils.util.strtobool(args.should_preload))
if int(args.url_type) == HypeURLType.ResourcesFolder:
url_info['url'] = "."
else:
url_info['url'] = args.replace_url
exit_with_result(url_info)
## --modify_staging_path [filepath] --destination_path [filepath] --export_info_json_path [filepath] --is_preview [True|False] --export_uid [identifier]
## return True if you moved successfully to the destination_path, otherwise don't return anything and Hype will make the move
## make any changes you'd like before the save is complete
## for example, if you are a zip, you need to zip and write to the destination_path
## or you may want to inject items into the HTML file
## if it is a preview, you shouldn't do things like zip it up, as Hype needs to know where the index.html file is
## export_info_json_path is a json object holding keys:
## html_filename: string that is the filename for the html file which you may want to inject changes into
## main_container_width: number representing the width of the document in pixels
## main_container_height: number representing the height of the document in pixels
## document_arguments: dictionary of key/value pairs based on what was passed in from the earlier --get_options call
## extra_actions: array of dictionaries for all usages of the extra actions. There is no guarantee these all originated from this script or version.
## function: string of function name (as passed in from --get_options)
## arguments: array of strings
elif args.modify_staging_path != None:
import os
import string
import re
import random
is_preview = bool(distutils.util.strtobool(args.is_preview))
# read export_info.json file
export_info_file = open(args.export_info_json_path)
export_info = json.loads(export_info_file.read())
export_info_file.close()
# ----------------------------------
# TEMPLATE FEATURE
# ----------------------------------
# make sure we have the key
if "Template name" in export_info["document_arguments"]:
template_name = export_info["document_arguments"]["Template name"]
#set default if not provided
if template_name == "":
template_name = "HypeTemplate"
# start with no template content
template_content = None
# look for template content in project
for root, dirs, files in os.walk(args.modify_staging_path):
for file in files:
if file.endswith(template_name+'.html'):
template_content = open(root + '/' + file).read()
os.remove(root + '/' + file)
break
# look for template content in export script folder
if template_content == None:
extension_folder = path.dirname(path.abspath(str(sys.modules['__main__'].__file__)))
for root, dirs, files in os.walk(extension_folder):
for file in files:
if file.endswith(template_name+'.html'):
template_content = open(root + '/' + file).read()
break
# proceed if we have template content
if template_content != None:
# load existing html
index_path = os.path.join(args.modify_staging_path, export_info['html_filename'].encode("utf-8"))
index_contents = None
with open(index_path, 'r') as target_file:
index_contents = target_file.read()
if index_contents == None:
return
# Extract infos from html
html_title = re.search(r'<title>(.*)</title>', index_contents).group(1)
hype_div = re.search(r'<div id="(.*)" class="HYPE_document" style="(.*)">', index_contents)
hype_div_id= hype_div.group(1)
hype_div_styles= hype_div.group(2)
custom_head_html = re.search(r'<!-- copy these lines to your document head: -->(.*?)<!-- end copy -->', index_contents, re.DOTALL).group(1)
viewport_meta_regex = re.compile(r'<meta name="viewport" content="(.*?)" />')
viewport_meta_tag = viewport_meta_regex.search(custom_head_html).group(0)
custom_head_html = custom_head_html.replace(viewport_meta_tag, '')
document_loader_html = re.search(r'<!-- copy these lines to your document: -->(.*?)<!-- end copy -->', index_contents, re.DOTALL).group(1)
document_loader_script_src = re.search(r'<script type="text/javascript" charset="utf-8" src="(.*)"></script>', document_loader_html).group(1)
document_loader_filename = re.search(r'/(.*_hype_generated_script\.js)', document_loader_script_src).group(1)
document_loader_path = re.search(r'(.*/)', document_loader_script_src).group(1)
hype_document_style_width_value = re.search(r'width:(.*?)(px|%)', hype_div_styles).group(1)
hype_document_style_width_unit = re.search(r'width:(.*?)(px|%)', hype_div_styles).group(2)
hype_document_style_height_value = re.search(r'height:(.*?)(px|%)', hype_div_styles).group(1)
hype_document_style_height_unit = re.search(r'height:(.*?)(px|%)', hype_div_styles).group(2)
hype_document_style_width = hype_document_style_width_value + hype_document_style_width_unit
hype_document_style_height = hype_document_style_height_value + hype_document_style_height_unit
# remove any reference to bundle in head html
custom_head_html = re.sub(r'<script type="text/javascript" src=".*HypeTemplate.js"></script>', '', custom_head_html)
# substitute in template
template_engine = string.Template(template_content)
template_content = template_engine.safe_substitute({
'containerWidth': export_info['main_container_width'],
'containerHeight': export_info['main_container_height'],
'htmlFilename': export_info['html_filename'],
'customHeadHTML': custom_head_html,
'documentLoaderHTML': document_loader_html,
'title': html_title,
'hypeDocumentStyles': hype_div_styles,
'hypeDocumentId': hype_div_id,
'documentLoaderScriptSource': document_loader_script_src,
'documentLoaderScriptPath': document_loader_path,
'documentLoaderFilename': document_loader_filename,
'hypeDocumentStyleWidth': hype_document_style_width,
'hypeDocumentStyleHeight': hype_document_style_height,
'hypeDocumentStyleWidthValue': hype_document_style_width_value,
'hypeDocumentStyleHeightValue': hype_document_style_height_value,
'hypeDocumentStyleWidthUnit': hype_document_style_width_unit,
'hypeDocumentStyleHeightUnit': hype_document_style_height_unit,
'cacheBuster': str(random.randint(10000,99999)),
'viewportMetaTag': viewport_meta_tag,
# can be extended ...
})
# write substituted template back to index
with open(index_path, 'w') as target_file:
target_file.write(template_content)
# ----------------------------------
# BUNDLED JAVASCRIPT FEATURE
# ----------------------------------
# load bundled javascript content and inject it in document loader
hype_template_bundled_script_content = None
for root, dirs, files in os.walk(args.modify_staging_path):
for file in files:
if file == "HypeTemplate.js":
hype_template_bundled_script = os.path.join(root, file)
hype_template_bundled_script_content = open(hype_template_bundled_script).read()
os.remove(hype_template_bundled_script)
break
if hype_template_bundled_script_content is not None:
for root, dirs, files in os.walk(args.modify_staging_path):
for file in files:
if file.endswith("_hype_generated_script.js"):
hype_generated_script = os.path.join(root, file)
with open(hype_generated_script, "r") as f:
hype_generated_script_content = f.read()
with open(hype_generated_script, "w") as f:
f.write(hype_template_bundled_script_content + hype_generated_script_content)
break
import shutil
shutil.rmtree(args.destination_path, ignore_errors=True)
if is_preview == True:
shutil.move(args.modify_staging_path, args.destination_path)
exit_with_result(True)
else:
zip(args.modify_staging_path, args.destination_path)
exit_with_result(True)
## --check_for_updates
## return a dictionary with "url", "from_version", and "to_version" keys if there is an update, otherwise don't return anything and exit
## it is your responsibility to decide how often to check
elif args.check_for_updates:
import subprocess
import urllib2
last_check_timestamp = None
try:
last_check_timestamp = subprocess.check_output(["defaults", "read", defaults_bundle_identifier, "last_check_timestamp"]).strip()
except:
pass
try:
timestamp_now = subprocess.check_output(["date", "+%s"]).strip()
if (last_check_timestamp == None) or ((int(timestamp_now) - int(last_check_timestamp)) > minimum_update_check_duration_in_seconds):
subprocess.check_output(["defaults", "write", defaults_bundle_identifier, "last_check_timestamp", timestamp_now])
request = urllib2.Request(version_info_url, headers={'User-Agent' : "Magic Browser"})
latest_script_version = int(urllib2.urlopen(request).read().strip())
if latest_script_version > current_script_version:
exit_with_result({"url" : download_url, "from_version" : str(current_script_version), "to_version" : str(latest_script_version)})
except:
pass
# UTILITIES
# communicate info back to Hype
# uses delimiter (20 equal signs) so any above printing doesn't interfere with json data
def exit_with_result(result):
import sys
print "===================="
print json.dumps({"result" : result})
sys.exit(0)
# from http://stackoverflow.com/questions/14568647/create-zip-in-python
def zip(src, dst):
import os
import zipfile
zf = zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED)
abs_src = os.path.abspath(src)
for dirname, subdirs, files in os.walk(src):
for filename in files:
absname = os.path.abspath(os.path.join(dirname, filename))
arcname = absname[len(abs_src) + 1:]
zf.write(absname, arcname)
zf.close()
if __name__ == "__main__":
main()
@themorgantown
Copy link

Just a quick suggestion to change html_titel to html_title

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment