Files
animated_wallpaper/build.py
T

177 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
Build a single self-contained HTML file from the source files.
Usage:
python3 build.py [output.html]
python3 build.py --check build to a temp file and only verify
What gets inlined:
- config.js, i18n.js (plain <script> swap)
- vendor/suncalc.js, vendor/plane3d.js,
vendor/clouds3d.js (plain <script> swap)
- vendor/stars.json (window.__starsData)
- every stylesheet referenced by window.THEMES
(window.__inlineThemeCss, so
theme switching works offline)
The result needs no sibling files. Runtime DATA is still fetched on load:
basemap and elevation tiles, Overpass road/settlement cells and the Open-Meteo
weather API. Serve the file from the same nginx as the development site (its
CORS proxies make the tiles reachable) or open it with a warm IndexedDB cache
for fully offline rendering. Opened via file:// it still runs sky, clouds,
terrain shading and the whole UI, just without the textured ground.
The build fails loudly if any external script or stylesheet reference survives.
"""
import json
import os
import re
import sys
import tempfile
ROOT = os.path.dirname(os.path.abspath(__file__))
INLINED_SCRIPTS = [
'config.js',
'i18n.js',
'vendor/suncalc.js',
'vendor/plane3d.js',
'vendor/clouds3d.js',
]
def read(path):
with open(os.path.join(ROOT, path), 'r', encoding='utf-8') as f:
return f.read()
def inline_script(html, tag, body):
old = '<script src="%s"></script>' % tag
if old not in html:
raise SystemExit('build.py: expected tag not found: ' + tag)
# a literal </script> inside an inline block would end it early
return html.replace(old, '<script>\n%s\n</script>' % body.replace('</', '<\\/'), 1)
def parse_themes(config_src):
"""Return {themeId: cssFileName} from window.THEMES.
Accepts both shapes:
'ash': 'ash.css'
'ash': { css: 'ash.css', lighting: { ... } }
"""
m = re.search(r'window\.THEMES\s*=\s*\{', config_src)
if not m:
raise SystemExit('build.py: window.THEMES not found in config.js')
i = m.end()
depth = 1
block_start = i
while i < len(config_src) and depth:
c = config_src[i]
if c == '{':
depth += 1
elif c == '}':
depth -= 1
i += 1
block = config_src[block_start:i - 1]
themes = {}
# top level entries only: split on commas that sit at brace depth 0
parts, buf, depth = [], '', 0
for c in block:
if c == '{':
depth += 1
elif c == '}':
depth -= 1
if c == ',' and depth == 0:
parts.append(buf)
buf = ''
else:
buf += c
parts.append(buf)
for part in parts:
entry = re.match(r"\s*['\"]?([A-Za-z0-9_-]+)['\"]?\s*:\s*(.*)", part, re.S)
if not entry:
continue
name, value = entry.group(1), entry.group(2)
css = re.search(r"['\"]([^'\"]+\.css)['\"]", value)
if not css:
raise SystemExit('build.py: theme %r has no .css reference' % name)
themes[name] = css.group(1)
if not themes:
raise SystemExit('build.py: window.THEMES is empty')
return themes
def verify(html):
"""Fail if anything still points at a sibling file."""
leftovers = re.findall(r'<script[^>]*\bsrc="([^"]+)"', html)
leftovers += [re.search(r'\bhref="([^"]+)"', tag).group(1)
for tag in re.findall(r'<link[^>]*>', html)
if re.search(r'\bhref="[^"]+\.css"', tag)]
if leftovers:
raise SystemExit('build.py: not self-contained, still referencing: ' +
', '.join(sorted(set(leftovers))))
def main():
check = '--check' in sys.argv
args = [a for a in sys.argv[1:] if not a.startswith('--')]
out = args[0] if args else 'background-standalone.html'
html = read('background.html')
config_src = read('config.js')
themes = parse_themes(config_src)
# embedded data preamble (must run before the scene scripts)
stars = json.loads(read('vendor/stars.json'))
theme_css = {name: read(fname) for name, fname in themes.items()}
embed = (
'<script>\n'
'window.__starsData = %s;\n'
'window.__inlineThemeCss = %s;\n'
'</script>\n'
% (json.dumps(stars, separators=(',', ':')), json.dumps(theme_css))
)
# must come before config.js so the early theme loader sees the CSS
anchor = '<script src="config.js"></script>'
if anchor not in html:
raise SystemExit('build.py: anchor script tag not found in background.html')
html = html.replace(anchor, embed + anchor, 1)
for tag in INLINED_SCRIPTS:
html = inline_script(html, tag, read(tag))
# themes are embedded in window.__inlineThemeCss, so the stylesheet link
# is dropped completely (the theme loader guards on getElementById)
rep_link = re.search(r'\n[ \t]*<link id="theme-css"[^>]*>', html)
if not rep_link:
raise SystemExit('build.py: theme-css link not found in background.html')
html = html[:rep_link.start()] + '\n <!-- standalone build: themes are embedded -->' + \
html[rep_link.end():]
verify(html)
if check:
with tempfile.NamedTemporaryFile('w', suffix='.html', delete=False) as f:
f.write(html)
tmp = f.name
print('OK: self-contained (%d bytes, %d themes, %d stars)'
% (len(html), len(themes), len(stars)))
os.unlink(tmp)
return
with open(os.path.join(ROOT, out), 'w', encoding='utf-8') as f:
f.write(html)
print('Wrote %s (%d bytes)' % (out, len(html)))
print('Inlined: %s, stars.json (%d entries), themes: %s'
% (', '.join(INLINED_SCRIPTS), len(stars), ', '.join(sorted(themes))))
if __name__ == '__main__':
main()