Files
MeshCore-mqtt-observer/scripts/webconfig_minify.py
agessaman 8cbc5520c9 build(webconfig): strip comments before embedding the portal page
The generator gzipped webui/index.html verbatim, so the page's comments — and
this file is commented heavily by house style — were paying flash rent. A
line-based pass now drops comments, indentation and blank lines before
compressing. The source stays as readable as it was.

Conservative on purpose: only a comment that starts its own line is removed, so
a `//` inside a URL or a `/*` inside a regex can never be mistaken for one.
Line breaks survive, which leaves JS statement boundaries (and the space a
newline contributes between HTML inline elements) exactly as written.

This ships to thousands of devices, so it is not taken on trust:
  - check_stripped() fails the build if the page's structure changed or the
    output shrank implausibly
  - the pass lives in its own module, shared with the mock backend's new
    --minify flag, so the bytes exercised in a browser are the bytes that get
    embedded rather than a second implementation that could drift
  - webconfig_minify.py joins the generator in the freshness hash, so editing
    the stripper forces a regenerate

Today's page: 22,678 -> 17,671 bytes gzipped.
2026-08-07 22:35:00 -07:00

89 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Comment/indentation stripping for webui/index.html.
Shared by the build-time generator (which embeds the stripped page in flash)
and the mock backend's --minify flag (which serves it), so what you test in a
browser is byte-for-byte what the device ships. A second implementation would
only drift.
Stdlib only; imported from a PIO extra_script, so it must stay side-effect free.
"""
# Comment openers/closers per region of the page. The region is tracked so a
# `<!--` inside <script> is never treated as a comment, and vice versa.
_COMMENTS = {
"html": [("<!--", "-->")],
"css": [("/*", "*/")],
"js": [("/*", "*/")],
}
def strip_source(text):
"""Drop comments, indentation and blank lines from the page.
Deliberately line-based and conservative. Only a comment that *starts* its
own line is removed, so a `//` inside a URL or a `/*` inside a regex is
never mistaken for one; trailing comments survive, which costs a little
flash and removes the entire class of "the minifier ate a string" bug.
Line breaks are preserved. That keeps JS statement boundaries exactly as
written (no ASI surprises) and keeps the single collapsed space a newline
contributes between HTML inline elements.
"""
out, mode, closer = [], "html", None
for line in text.split("\n"):
s = line.strip()
if closer is not None: # inside a multi-line comment
at = s.find(closer)
if at < 0:
continue
s = s[at + len(closer):].strip() # code may follow the close
closer = None
if not s:
continue
for opener, close in _COMMENTS[mode]:
if not s.startswith(opener):
continue
if s.endswith(close) and len(s) > len(opener) + len(close) - 1:
s = "" # the whole line is a comment
elif close not in s[len(opener):]:
closer = close # ... and it continues below
s = ""
break # else code follows the close
if not s: # on this line: leave it be
continue
if mode == "js" and s.startswith("//"):
continue
out.append(s)
low = s.lower()
if mode == "html" and "<style" in low:
mode = "css"
elif mode == "html" and "<script" in low:
mode = "js"
elif mode != "html" and ("</style>" in low or "</script>" in low):
mode = "html"
return "\n".join(out) + "\n"
def check_stripped(raw, stripped):
"""Guard against a stripper bug silently shipping a broken portal to the
fleet. Returns a reason string when the output looks wrong, else None."""
for tag in ("<script>", "</script>", "<style>", "</style>", "</body>"):
if raw.count(tag) != stripped.count(tag):
return "%s count changed" % tag
if len(stripped) < len(raw) * 0.5:
return "output shrank by more than half (%d -> %d)" % (len(raw), len(stripped))
# Only comments and whitespace may go, so no structural token may appear
# that the source did not already have.
for token in ("{", "}", "(", ")", "<script", "<style"):
if stripped.count(token) > raw.count(token):
return "gained a %s" % token
return None