"""
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 '' for a close tag), and only
treat it as a real tag if that run is immediately followed by
whitespace, '/' or '>'. 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