Files
JetCore/jetc/lowercase_jet.py
T
Brad Arant c1491b044b Add IMAP webmail tag, admin/opensource subpage tiers, store/cart, htmleditor, uploads
New __imap*/__smtpsend/__eval Jet tags give templates real IMAP
(list/message/flag/delete/move/search/folders/accounts) and SMTP-send
capability, backing the new /email webmail client (email.jet/
email.menu, assets/images/email.js + newemail.js) and its
mainpagetemplate.jet-based page shell. Most of this tag family's
source was already being built into the live site (Tag.cpp already
registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/
__eval) but had never actually been committed; this also registers
and commits the remaining five (__imapmove/__imapfolders/
__imapsearch/__smtpsend/__imapaccounts).

admin.page/admin.menu gain a real subpage tier (admin-email,
admin-newemail, admin-invoices, admin-products, admin-users) replacing
several previously top-level pages, mirrored by a similar
opensource.page subpage tier (opensource-overview/asm1802/barantmail/
jet/serverframework/synthmax) - old top-level pages retired via git
rename (asm1802.page, jet.page, serverframework.page,
admin_invoices.page); barantmail.page is dropped outright, folded into
opensource-barantmail.subpage.

New storefront (store.page, store-createstore.subpage,
barantstore.page) and cart flow (cart.jet, updatecartqty.proc,
removecartitem.proc, editproduct.page/selectproduct.proc/
update_product.proc for inventory maintenance against the barant
database).

New document upload/download manager (documents.jet, documents_api.jet,
documents_upload.proc, documents_delete.proc,
assets/images/documents.js + documents-page.js) with a persistent
floating panel, and a rich-text page editor (htmleditor.jet/.page,
createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js
+ its icon/logo assets).

SESSIONCTL.tag: login now redirects back to the originally-requested
page instead of always landing on /account ($[start:0] vs $[start]
variable-name mismatch).

main.css/menu.bar/invoices.menu/jet_overview.section/login.menu:
styling and navigation updates for the above; new site assets (fonts,
images, audio) referenced by them.

jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py,
strip_fragments.py: small dev tooling used while building the above.
2026-09-06 04:14:00 +00:00

262 lines
8.4 KiB
Python

"""
Lowercases jet tag NAMES (<TagName> / </TagName>) 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: <IF ...>...<ELSE>...</IF>..."), 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 <tag>...</tag> 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 <input> <output>")
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()