<database> was SESSIONCTL.tag's own JET-1.4-only concept - the new JetCore port never implemented it. The real, dispatched tag for opening a MySQL connection is <mysql> (confirmed in Tag.cpp's dispatch chain), so these 3 product pages - the ones deferred in the previous conversion pass because they pull live rows via <sql> - now wrap their content in a real <mysql host="localhost" database="barant" user="barant" password="..."> matching barant.com's own config.cfg, with <retain> (a SESSIONCTL-only form-echo helper with no analog here) stripped. host="localhost" is correct per config.cfg but only reachable from barant.com itself - a locally-run .jtc genuinely cannot connect from here. That's intentional: the connection fails at runtime exactly as if the database were down, and APIServer's existing catch(coreutils::Exception) path (ApiSession.cpp) already turns that into a clean 500 rather than a crash - verified live: all 3 pages return HTTP 500 with mysql's own "database and host parameters are not valid." message, and the server process stays up and responsive throughout. The real DB password is deliberately not a literal in this file - it's read from BARANT_DB_PASSWORD at conversion time, and the resulting .jet output for these 3 pages is gitignored rather than committed, so the credential never enters git history even though it does end up baked into the locally-generated, locally-deployed .jtc.
285 lines
12 KiB
Python
285 lines
12 KiB
Python
import re
|
|
import os
|
|
import base64
|
|
import subprocess
|
|
|
|
ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
SRC = ROOT + "/src"
|
|
OUT = ROOT + "/jet"
|
|
ASSETS = ROOT + "/assets"
|
|
IMG_CACHE = ROOT + "/assets/images"
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
os.makedirs(IMG_CACHE, exist_ok=True)
|
|
|
|
IMG_MIME = {
|
|
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp",
|
|
".ico": "image/x-icon",
|
|
}
|
|
|
|
|
|
def resolve_images(html):
|
|
"""Find any remaining /images/<file> reference (src="..." or url('...'))
|
|
and inline it as a data: URI, fetching+caching from barant.com on first
|
|
use - APIServer has no static-file serving, same reason the chrome logo
|
|
and page CSS/fonts needed inlining earlier this session."""
|
|
def repl(m):
|
|
path = m.group(1) # e.g. images/grid.png
|
|
fname = path.split("/")[-1]
|
|
ext = os.path.splitext(fname)[1].lower()
|
|
mime = IMG_MIME.get(ext, "application/octet-stream")
|
|
local_path = IMG_CACHE + "/" + fname
|
|
if not os.path.exists(local_path):
|
|
subprocess.run(["curl", "-s", "-o", local_path, "https://barant.com/" + path], check=True)
|
|
return data_uri(local_path, mime)
|
|
html = re.sub(r'/images/([A-Za-z0-9_.-]+)(?=["\'])', repl, html)
|
|
return html
|
|
|
|
|
|
def read(path):
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
return f.read()
|
|
|
|
|
|
def data_uri(path, mime):
|
|
with open(path, "rb") as f:
|
|
b64 = base64.b64encode(f.read()).decode("ascii")
|
|
return "data:" + mime + ";base64," + b64
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# linkbutton -> plain <a>. The old SESSIONCTL PAGE/PROCESS form-postback model
|
|
# doesn't exist in this static conversion (no shared <form> state machine, no
|
|
# session), so a JS document.forms[0]... postback would just be dead code -
|
|
# every page is now its own real #api GET/POST route, so a real href is both
|
|
# simpler and more correct for this architecture.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
LINKBUTTON_RE = re.compile(r'<linkbutton\s+([^>]*)>(.*?)</linkbutton>', re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def linkbutton_attr(attrs, name):
|
|
m = re.search(name + r'="([^"]*)"', attrs, re.IGNORECASE)
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
def flatten_linkbuttons(html):
|
|
def repl(m):
|
|
attrs, content = m.group(1), m.group(2)
|
|
page = linkbutton_attr(attrs, "page")
|
|
cls = linkbutton_attr(attrs, "class")
|
|
if not page:
|
|
# process-only actions (logout, addtocart, etc.) have no static
|
|
# target in this conversion - render as inert text, not a link.
|
|
return content
|
|
cls_attr = (' class="%s"' % cls) if cls else ""
|
|
return '<a href="/%s"%s>%s</a>' % (page, cls_attr, content)
|
|
return LINKBUTTON_RE.sub(repl, html)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sectionframe -> its barant.tag expansion.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SECTIONFRAME_RE = re.compile(r'<sectionframe\s+title="([^"]*)"\s*>(.*?)</sectionframe>', re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def flatten_sectionframes(html):
|
|
def repl(m):
|
|
title, content = m.group(1), m.group(2)
|
|
heading = ('<div class="sectionframeheading"><b>%s</b></div>' % title) if title else ""
|
|
return '<div class="sectionframe">%s<div class="sectionframecontents">%s</div></div>' % (heading, content)
|
|
# Run twice to catch one level of nesting (not expected, but cheap insurance).
|
|
html = SECTIONFRAME_RE.sub(repl, html)
|
|
html = SECTIONFRAME_RE.sub(repl, html)
|
|
return html
|
|
|
|
|
|
def flatten_tabs(menu_bar_html):
|
|
def repl(m):
|
|
content = m.group(1)
|
|
return '<td class="baritem">%s</td><td style="border-bottom: 1pt solid #bbb;"> </td>' % content
|
|
return re.sub(r'<tab>(.*?)</tab>', repl, menu_bar_html, flags=re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def drop_tag_block(html, tag):
|
|
"""Remove a <tag name="..."> ... </tag> definition block entirely."""
|
|
pattern = re.compile(r'<tag\s+name="%s".*?</tag>' % tag, re.IGNORECASE | re.DOTALL)
|
|
return pattern.sub('', html)
|
|
|
|
|
|
def strip_setenv(html):
|
|
return re.sub(r'<setenv\s+name="[^"]*">.*?</setenv>', '', html, flags=re.IGNORECASE | re.DOTALL)
|
|
|
|
|
|
def strip_retain(html):
|
|
"""<retain name="X"> is a SESSIONCTL.tag helper that echoes a posted form
|
|
field back as a hidden input - meaningless with no <form>/session in this
|
|
static conversion, and (since it isn't a tag jetc's fallback interpreter
|
|
knows about) would otherwise render as literal untouched text rather
|
|
than erroring, which is worse than just dropping it."""
|
|
return re.sub(r'<retain\s+name="[^"]*"\s*>', '', html, flags=re.IGNORECASE)
|
|
|
|
|
|
# Pages that need a live product-row lookup - <database> (SESSIONCTL-only,
|
|
# not implemented by the new JetCore port) is replaced with the real
|
|
# <mysql> tag it should have used all along. host="localhost" matches
|
|
# barant.com's own config.cfg verbatim: MySQL is only reachable from the
|
|
# server itself, so a locally-run .jtc genuinely can't connect here - by
|
|
# design, per instruction, this fails at runtime exactly as if the
|
|
# database were down, and APIServer's existing catch(coreutils::Exception)
|
|
# path (ApiSession.cpp) already turns that into a clean 500, not a crash.
|
|
#
|
|
# The real credential is deliberately NOT a literal here - it's supplied
|
|
# via the BARANT_DB_PASSWORD env var at conversion time, since the .jet
|
|
# files this produces for these pages get gitignored rather than committed
|
|
# (see .gitignore) precisely so the password never lands in git history.
|
|
MYSQL_PAGES = {"vst", "modular1", "modular1_patches"}
|
|
DB_PASSWORD = os.environ.get("BARANT_DB_PASSWORD", "")
|
|
MYSQL_OPEN = '<mysql host="localhost" database="barant" user="barant" password="%s" sessionid="">' % DB_PASSWORD
|
|
MYSQL_CLOSE = '</mysql>'
|
|
|
|
|
|
def selfclose_set(html):
|
|
"""Old JET shorthand allows <set name="x" value="y"> with no closing tag
|
|
and no container - jetc's own real <set> codegen requires this to be
|
|
self-closed (<set .../>) or it reads as having an (empty) container,
|
|
which conflicts with value= at runtime ("set tag cannot have both value
|
|
and a container")."""
|
|
return re.sub(r'<set ([^>]*[^/])>', r'<set \1 />', html, flags=re.IGNORECASE)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build the shared CSS (same assets already used for barant.jet).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
main_css = read(ASSETS + "/main.css")
|
|
control_css = read(ASSETS + "/control.css")
|
|
|
|
for old, new_mime, path in [
|
|
("url('https://barant.com/fonts/AvenirLTStd-Light.ttf')", "font/ttf", ASSETS + "/AvenirLTStd-Light.ttf"),
|
|
("url('https://barant.com/fonts/jackinput.TTF')", "font/ttf", ASSETS + "/jackinput.TTF"),
|
|
("url('https://barant.com/fonts/Exo2-RegularCondensed.otf')", "font/otf", ASSETS + "/Exo2-RegularCondensed.otf"),
|
|
]:
|
|
main_css = main_css.replace(old, "url('" + data_uri(path, new_mime) + "')")
|
|
|
|
STYLE_BLOCK = "<style>\n" + main_css + "\n" + control_css + "\n</style>"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build the nav bar (menu.bar), flattened.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
menu_bar = read(SRC + "/menu.bar")
|
|
menu_bar = drop_tag_block(menu_bar, "tab")
|
|
menu_bar = menu_bar.replace('width: 100%">', 'width="100%">') # same stray-quote bug as barant.jet's source
|
|
menu_bar = flatten_tabs(menu_bar)
|
|
menu_bar = flatten_linkbuttons(menu_bar)
|
|
|
|
# Session/login-gated nav items: CART only shows with an active cart (never,
|
|
# here); ACCOUNT/ADMIN only show logged in (never, here) - only the LOGIN
|
|
# fallback applies for an anonymous visitor, which is this conversion's only
|
|
# supported state (no session backend exists in this static conversion).
|
|
menu_bar = re.sub(r'<if value1="\$\[_session_id\]".*?</if>', '', menu_bar, flags=re.DOTALL)
|
|
menu_bar = re.sub(
|
|
r'<if value1="\$\[_login_id\]".*?<else>\s*(<td class="baritem">.*?LOGIN.*?</td>.*?)</if>',
|
|
r'\1', menu_bar, flags=re.DOTALL)
|
|
|
|
NAV_HTML = menu_bar
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build the default sidebar (opensource.menu; cart.menu/login.menu are both
|
|
# session-gated and always empty for an anonymous visitor, so they're
|
|
# omitted rather than included-and-always-false).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
opensource_menu = read(SRC + "/opensource.menu")
|
|
opensource_menu = flatten_sectionframes(opensource_menu)
|
|
opensource_menu = flatten_linkbuttons(opensource_menu)
|
|
|
|
SIDEBAR_HTML = opensource_menu
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Build the page chrome from mainpagetemplate.temp: the part of <template>
|
|
# before <contents> and the part after.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
temp_raw = read(SRC + "/mainpagetemplate.temp")
|
|
m = re.search(r'<template>(.*)</template>', temp_raw, re.DOTALL)
|
|
template_body = m.group(1)
|
|
|
|
# Substitute the chrome-level includes/placeholders that are now fixed for
|
|
# every page in this conversion (no per-page menubar/menu overrides exist -
|
|
# confirmed by grepping every source .page file for _menubar/_menu overrides
|
|
# before writing this script).
|
|
template_body = template_body.replace('<include file="login.menu">', '') # session-gated, always empty
|
|
template_body = re.sub(r'<if value1="\$\[_menubar\]" value2="" type="eq">\s*<include file="menu.bar">\s*<else>\s*<include file="\$\[_menubar\]">\s*</if>',
|
|
NAV_HTML, template_body, flags=re.DOTALL)
|
|
template_body = re.sub(r'<if value1="\$\[_menu\]" value2="" type="eq">\s*<include file="opensource.menu">\s*</if>\s*\$\[_menu\]\s*<include file="cart.menu">',
|
|
SIDEBAR_HTML, template_body, flags=re.DOTALL)
|
|
|
|
CONTENTS_MARKER = "<contents>"
|
|
idx = template_body.index(CONTENTS_MARKER)
|
|
CHROME_BEFORE = resolve_images(template_body[:idx])
|
|
CHROME_AFTER = resolve_images(template_body[idx + len(CONTENTS_MARKER):])
|
|
|
|
|
|
def convert_page(name):
|
|
src_path = SRC + "/" + name + ".page"
|
|
text = read(src_path)
|
|
|
|
title_m = re.search(r'<title>(.*?)</title>', text, re.DOTALL)
|
|
page_title = title_m.group(1).strip() if title_m else "BARANT Technology Group"
|
|
|
|
contents_m = re.search(r'<contents>(.*)</contents>', text, re.DOTALL)
|
|
if not contents_m:
|
|
raise ValueError("no <contents> block found in %s" % name)
|
|
contents = contents_m.group(1)
|
|
|
|
contents = strip_setenv(contents)
|
|
contents = strip_retain(contents)
|
|
contents = selfclose_set(contents)
|
|
contents = flatten_sectionframes(contents)
|
|
contents = flatten_linkbuttons(contents)
|
|
contents = resolve_images(contents)
|
|
|
|
if name in MYSQL_PAGES:
|
|
contents = MYSQL_OPEN + contents + MYSQL_CLOSE
|
|
|
|
if name == "jet":
|
|
# jet.page dispatches on a $[ARGS] query-string sub-page selector
|
|
# (?tags, ?set, etc. -> jet_<sub>.section) - that request-derived
|
|
# variable isn't populated in this static conversion (no ARGS
|
|
# plumbing wired up), so it reads as uninitialized rather than
|
|
# empty. This conversion pass only covers the default/no-args
|
|
# overview state (jet_overview.section, inlined directly rather
|
|
# than via runtime <include> - untested path-resolution risk for
|
|
# a hot-loaded module whose cwd isn't this source tree); per-topic
|
|
# sub-pages are a follow-up if wanted.
|
|
overview = read(SRC + "/jet_overview.section")
|
|
overview = flatten_sectionframes(overview)
|
|
contents = overview
|
|
|
|
chrome_before = CHROME_BEFORE.replace('<exclude>$[_title]</exclude>', page_title)
|
|
|
|
body = (
|
|
'<set name="jetc_doctype" value="<!DOCTYPE html>" />$[jetc_doctype]'
|
|
'<html lang="en"><head><title>%s</title>%s</head><body>'
|
|
'%s%s%s</body></html>'
|
|
) % (page_title, STYLE_BLOCK, chrome_before, contents, CHROME_AFTER)
|
|
|
|
jet_text = "#api GET /%s\n%s\n" % (name, body)
|
|
|
|
out_path = OUT + "/" + name + ".jet"
|
|
with open(out_path, "w", encoding="utf-8") as f:
|
|
f.write(jet_text)
|
|
return out_path
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
pages = sys.argv[1:]
|
|
for p in pages:
|
|
out = convert_page(p)
|
|
print("converted", p, "->", out)
|