""" Lowercases jet tag NAMES ( / ) and attribute NAMES (ATTR=) throughout a jet source file, WITHOUT touching: - attribute VALUES (quoted or unquoted) - $[...]/#[...] variable references anywhere (including nested ones, and ones that appear inside attribute values) - literal HTML/text content outside of tags Handles the same real-content wrinkles this session's own JetParser.cpp fixes had to: tags embedded inside a quoted attribute value (e.g. linkbutton's own HREF="javascript: ........."), and both single and double quoted attribute values. This is a straightforward character-by-character scanner, not a regex - regex can't reliably track the nested quote/tag structure real content uses. """ import sys IDENT_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") def skip_variable_ref(s, i, out): """s[i] is '$' or '#', s[i+1] is '['. Copies the whole $[...]/#[...] reference verbatim (including nested [...] refs inside it) to out, returns the index just past the closing ']'.""" out.append(s[i]) out.append(s[i + 1]) i += 2 depth = 1 while i < len(s) and depth > 0: if s[i] == '[': depth += 1 out.append(s[i]) i += 1 elif s[i] == ']': depth -= 1 out.append(s[i]) i += 1 else: out.append(s[i]) i += 1 return i def is_var_start(s, i): return i + 1 < len(s) and s[i] in '$#' and s[i + 1] == '[' def looks_like_tag(s, i): """s[i] is '<'. Peeks (without consuming) to decide whether this really starts a jet tag, using the same rule Tag.cpp's own constructor uses at runtime (Tag.cpp:50): read an identifier-char run right after '<' (or after ''. A literal '<' embedded in JS/text (e.g. console.log("<")) fails this and must be left untouched - real content relies on exactly this rule to disambiguate.""" n = len(s) j = i + 1 if j < n and s[j] == '/': k = j + 1 while k < n and s[k] in IDENT_CHARS: k += 1 if k == j + 1: return False return k < n and s[k] == '>' if j >= n or not s[j].isalpha(): return False k = j while k < n and s[k] in IDENT_CHARS: k += 1 return k < n and s[k] in ' \t\r\n/>' def skip_quoted_value(s, i, quote_char, out): """s[i] is the opening quote character (already consumed by caller, i.e. this points at the FIRST character of the value). Copies the value verbatim, including any embedded ... constructs and variable refs, stopping at (and consuming) the matching closing quote. Returns the index just past the closing quote.""" while i < len(s): if s[i] == quote_char: out.append(s[i]) return i + 1 if is_var_start(s, i): i = skip_variable_ref(s, i, out) continue if s[i] == '<' and looks_like_tag(s, i): i = skip_embedded_tag(s, i, out) continue out.append(s[i]) i += 1 return i def skip_embedded_tag(s, i, out): """s[i] is '<', starting a tag embedded inside a quoted attribute value (or, via lowercase_source's own top-level loop, an ordinary top-level tag). Lowercases its name and attribute names the same way the top-level scanner does, then either stops after a self-closing '/>' or, if it looks like it has its own body, copies through to its own matching close tag verbatim (rare inside a quoted value in real content; the void-tag list below covers the common built-ins that never do). Returns the index just past this tag's own closing '>' (and, for a real container tag, past its matching close tag too).""" out.append(s[i]) # '<' i += 1 closing = False if i < len(s) and s[i] == '/': closing = True out.append(s[i]) i += 1 name_start = i while i < len(s) and s[i] in IDENT_CHARS: i += 1 name = s[name_start:i] out.append(name.lower()) if closing: # just the name, then skip to '>' while i < len(s) and s[i] != '>': out.append(s[i]) i += 1 if i < len(s): out.append(s[i]) i += 1 return i # attribute region self_closed = False while i < len(s): if s[i] in ' \t\r\n': out.append(s[i]) i += 1 continue if s[i] == '/' and i + 1 < len(s) and s[i + 1] == '>': out.append('/>') i += 2 self_closed = True break if s[i] == '>': out.append('>') i += 1 break if s[i] in IDENT_CHARS: attr_start = i while i < len(s) and s[i] in IDENT_CHARS: i += 1 attr_name = s[attr_start:i] out.append(attr_name.lower()) # optional whitespace before '=' j = i while j < len(s) and s[j] in ' \t\r\n': j += 1 if j < len(s) and s[j] == '=': out.append(s[i:j]) i = j out.append('=') i += 1 # optional whitespace after '=' while i < len(s) and s[i] in ' \t\r\n': out.append(s[i]) i += 1 if i < len(s) and s[i] in ('"', "'"): q = s[i] out.append(q) i += 1 i = skip_quoted_value(s, i, q, out) else: # unquoted value - copy verbatim to next # whitespace/'>'//' while i < len(s) and s[i] not in ' \t\r\n>/': if is_var_start(s, i): i = skip_variable_ref(s, i, out) else: out.append(s[i]) i += 1 continue # anything unexpected - copy through defensively out.append(s[i]) i += 1 void_tags = { "set", "include", "cookie", "header", "read", "write", "call", "system", "dump", "br", "img", "input", "hr", "meta", "link", "area", "base", "col", "embed", "source", "track", "wbr", "else", } if self_closed or name.lower() in void_tags: return i # real container tag embedded in a quoted value - copy through to # its own matching close, tracking nesting of the same tag name. depth = 1 nlower = name.lower() while i < len(s) and depth > 0: if is_var_start(s, i): i = skip_variable_ref(s, i, out) continue if s[i] == '<': # peek for open/close of the same tag name j = i + 1 is_close = False if j < len(s) and s[j] == '/': is_close = True j += 1 k = j while k < len(s) and s[k] in IDENT_CHARS: k += 1 token = s[j:k] if not looks_like_tag(s, i): out.append(s[i]) i += 1 continue if token.lower() == nlower: if is_close: depth -= 1 else: depth += 1 i = skip_embedded_tag(s, i, out) continue i = skip_embedded_tag(s, i, out) continue out.append(s[i]) i += 1 return i def lowercase_source(s): out = [] i = 0 n = len(s) while i < n: if is_var_start(s, i): i = skip_variable_ref(s, i, out) continue if s[i] == '<' and looks_like_tag(s, i): i = skip_embedded_tag(s, i, out) continue out.append(s[i]) i += 1 return ''.join(out) def main(): if len(sys.argv) != 3: print("usage: lowercase_jet.py ") sys.exit(1) with open(sys.argv[1], encoding="utf-8", errors="surrogateescape", newline="") as f: content = f.read() result = lowercase_source(content) with open(sys.argv[2], "w", encoding="utf-8", errors="surrogateescape", newline="") as f: f.write(result) print("wrote", sys.argv[2], "(%d bytes -> %d bytes)" % (len(content), len(result))) if __name__ == "__main__": main()