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.
262 lines
8.4 KiB
Python
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()
|