Pulls .page files and shared template infrastructure (mainpagetemplate.temp, menu.bar, barant.tag, opensource.menu, cart.menu, login.menu) read-only from barant.com via SFTP into jetc/barant-site/src, then convert.py mechanically flattens them into self-contained .jet files: linkbutton becomes a plain anchor link (the old SESSIONCTL PAGE/PROCESS form-postback model has no equivalent here - each page is its own real #api route now, not a shared single-form state machine), sectionframe is expanded per its barant.tag definition, /images/* references are fetched and inlined as data URIs (APIServer has no static-file serving), and session-gated chrome (cart, account/admin, login.menu) is resolved to its anonymous-visitor default state, since no session/DB backend is wired into this conversion. 17 pages convert and serve cleanly end-to-end (verified via a local apiserver-barant-site.json instance on :8081, kept separate from the existing demo server on :8080 to avoid a /main route collision with the earlier hand-cloned barant.jet). Two pages deliberately out of scope for this pass: davidarant.page (whiledir filesystem iteration) and the per-topic sub-pages of jet.page (dispatches on an ARGS query param this conversion does not populate - only the default overview state converts). vst.page/modular1.page/modular1_patches.page also deferred - they pull live product rows via sql, a real backend dependency beyond "pages and their template" scope. Nothing was written back to barant.com - SFTP access was read-only throughout, per explicit instruction to convert locally without touching the original site.
253 lines
10 KiB
Python
253 lines
10 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 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 = selfclose_set(contents)
|
|
contents = flatten_sectionframes(contents)
|
|
contents = flatten_linkbuttons(contents)
|
|
contents = resolve_images(contents)
|
|
|
|
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)
|