JetCore engine (Tag.cpp/.h, Global.cpp/.h, __*.cpp tag classes): - Numerous __mysql.cpp connection-pooling correctness fixes (stale result sets, double-indirection sessionid bugs, field caching, pool keying) and a session-registration-before-connection-confirmed ordering fix (dangling pointer on a failed connect) - Tag.cpp: large body of fixes to the tag-dispatch/container-scanning logic (bare-name fallback, database aliasing, single-quote handling, @-prefix session variable resolution, TAGDEF caching, nested split-tag scanning for if/while/until/ifrow so an inner one doesn't corrupt an outer one's own boundary scan) plus the process-first rendering order fix (a <process> now runs before <checksession>/page resolution, fixing a real "blank page after successful login" bug) - Global.h/.cpp: added imapSessions (parallel to the existing __mysql* session map) for the new <imap> tag family - __whiledir.cpp/__include.cpp: caching and directory-name-aliasing fixes - __call.cpp/.h: fixed a dangling-pointer bug - __write.cpp: fixed an O_WRONLY-only open that couldn't create a new file - __ifrow.cpp/__sql.cpp: fixed the same sessionid double-indirection bug __mysql.cpp had (resolveKeyword's result was being used as a map key lookup instead of the resolved value itself) jetc compiler (jetc/JetParser.cpp/.h, JetCodegen.cpp/.h, JetAST.h, jetc.cpp, build-shared.sh, build-compiled.sh): - Parser fixes: inline <include>, nested-quote handling, void-tag recognition - Codegen fixes and expanded AST support - build-shared.sh: atomic rename into place for a freshly-built .jtc (a live server dlopen()ing the same path mid-rebuild was corrupting its own already-mapped view of the old file) and a staleness check so a cached JetCore .o doesn't silently keep serving yesterday's fixes forever - Added -lssl to both build scripts' link lines (needed once the new <imap> tag's client-side TLS code existed - previously nothing in this codebase used OpenSSL's SSL/TLS client API, only libcrypto) Site content (jetc/barant-site/): SPA (fetch-and-swap navigation) rollout across essentially every .page/.jet/.section/.tag/.proc/.method file - dynamic-content fixes, fragment-mode stripping, native $[@name] session variables replacing older mechanisms, and this session's own checkout.page redesign, login start-param redirect fix, and per-page side-menu (menu.bar/$[PAGE].menu) refresh support. Completed the earlier main.css/control.css move from assets/ to assets/images/ (git still showed the old paths as tracked-but-deleted until now). Deliberately left out of this commit: dozens of .pre-<description>- <date> manual backup files (this session's and earlier ones' own before/after snapshots, not meant for permanent history), compiled .jtc build outputs, ad hoc test binaries and scripts under jetc/ (login_asan, wcwc_test, optest2_bin, etc.), and jetc/__pycache__/ - none of these are meant to be tracked; a .gitignore covering them would be worth adding separately.
438 lines
21 KiB
Python
438 lines
21 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)
|
|
|
|
def resolve_images(html):
|
|
"""Find any /images/<file> or images/<file> reference (src="...",
|
|
href="...", or CSS url('...')) and repoint it at the live APIServer
|
|
GET /assets/{filename} route instead of inlining it as a data: URI -
|
|
fetching+caching from barant.com into the same assets/images/ cache
|
|
directory on first use (unchanged), just no longer embedding the raw
|
|
bytes into every page's own HTML (undoes the earlier base64-inlining
|
|
approach now that a real static-asset module exists to serve these).
|
|
Matches both the absolute form (/images/x.png) and the relative form
|
|
(images/x.png, no leading slash) - the previous version of this
|
|
function only matched the absolute form, silently leaving several
|
|
real <img src="images/.."> references un-rewritten and pointing
|
|
nowhere (APIServer has no /images/ route at all)."""
|
|
def repl(m):
|
|
path = m.group(1) # e.g. images/grid.png
|
|
fname = path.split("/")[-1]
|
|
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 "/assets/" + fname
|
|
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
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SPA bootstrap script: present once, in the chrome, on every FULL page load
|
|
# (never in a fragment-only response, which has no <head>/<script> of its
|
|
# own). Intercepts clicks on internal <a href="/..."> links and swaps just
|
|
# #jetc-content via fetch('/name?fragment=1') instead of a full navigation;
|
|
# also drives the browser back/forward buttons via popstate. See the
|
|
# FULL_PAGE_TMPL / FRAGMENT_TMPL split in convert_page() for how a single
|
|
# route serves both the full page and the fragment-only response, decided
|
|
# by the ?fragment=1 query flag.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Pages excluded from client-side fragment fetching entirely - a click on
|
|
# one of these falls through to an ORDINARY browser navigation (hitting
|
|
# the full-page route, confirmed safe) instead of being intercepted.
|
|
# Real, deterministic finding: <name>?fragment=1 for a MYSQL_PAGES page
|
|
# crashes the whole apiserver process on the very FIRST such request
|
|
# (reproduced repeatedly - not state-dependent, not "two failures in a
|
|
# row"), while the exact same page's full-page route (no ?fragment=1)
|
|
# fails cleanly with the expected "database and host parameters are not
|
|
# valid." and the server survives. Root cause not isolated (a <mysql>
|
|
# connection failure unwinding through the smaller <tag name="_frag_X">
|
|
# definition specifically, vs surviving fine through the larger
|
|
# <tag name="_full_X"> one, for reasons not chased down further given
|
|
# scope) - this is a mitigation, not a fix, and only matters while these
|
|
# pages fail (no real DB password configured); worth re-testing once a
|
|
# real DB is wired in, since a successful <mysql> connection takes a
|
|
# completely different code path that may not hit this at all.
|
|
FRAGMENT_EXCLUDED_PAGES = sorted(MYSQL_PAGES)
|
|
|
|
SPA_SCRIPT = """<script>
|
|
(function() {
|
|
var FRAGMENT_EXCLUDED = %s;
|
|
function pageNameFromPath(path) {
|
|
var name = path.replace(/^\\//, '').replace(/\\.page$/, '');
|
|
return name || 'main';
|
|
}
|
|
function loadPage(name, push) {
|
|
fetch('/' + name + '?fragment=1').then(function(resp) {
|
|
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
|
return resp.text();
|
|
}).then(function(html) {
|
|
var el = document.getElementById('jetc-content');
|
|
if (el) el.innerHTML = html;
|
|
if (push) history.pushState({page: name}, '', '/' + name);
|
|
}).catch(function(err) {
|
|
console.error('jetc-spa: failed to load page', name, err);
|
|
});
|
|
}
|
|
document.addEventListener('click', function(ev) {
|
|
if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
var target = ev.target.nodeType === 1 ? ev.target : ev.target.parentElement;
|
|
var a = target ? target.closest('a') : null;
|
|
if (!a) return;
|
|
if (a.target === '_blank' || a.hasAttribute('download')) return;
|
|
var href = a.getAttribute('href') || '';
|
|
if (href.indexOf(':') !== -1 || href.indexOf('//') === 0 || href.indexOf('/') !== 0) return;
|
|
if (FRAGMENT_EXCLUDED.indexOf(pageNameFromPath(href)) !== -1) return;
|
|
ev.preventDefault();
|
|
loadPage(pageNameFromPath(href), true);
|
|
});
|
|
window.addEventListener('popstate', function() {
|
|
loadPage(pageNameFromPath(location.pathname), false);
|
|
});
|
|
})();
|
|
</script>""" % (repr(FRAGMENT_EXCLUDED_PAGES).replace("'", '"'))
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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)
|
|
|
|
# mainpagetemplate.temp's own <tag name="title">...</tag> helper is dead
|
|
# code in this conversion - title substitution happens via a plain Python
|
|
# string-replace on <exclude>$[_title]</exclude> below, never by actually
|
|
# invoking this helper. Stripped as genuinely dead code (not a bug fix -
|
|
# see below for the real one).
|
|
template_body = drop_tag_block(template_body, "title")
|
|
|
|
# Real bug found and isolated via a minimal standalone repro (bisecting
|
|
# down from "the whole chrome throws 'comment must have a container'"):
|
|
# a <comment>...</comment> block, when it ends up nested inside one of
|
|
# this script's own <tag name="_full_NAME">/<tag name="_frag_NAME">
|
|
# definitions (see convert_page() below) - needed so the fragment/full
|
|
# <if> branches can hold real HTML content without jetc's compile-time
|
|
# <if> body parser choking on it - fails at RUNTIME with "comment must
|
|
# have a container", even though the exact same <comment> block renders
|
|
# fine as ordinary top-level content. Reproduced with a 4-line synthetic
|
|
# case (<tag name="x">text<comment>plain text, no nested tags</comment>
|
|
# text</tag>) - so this is a genuine jetc/JetCore bug in how a custom tag
|
|
# DEFINITION'S stored text gets re-processed at <dotag> invocation time,
|
|
# not something specific to this page's content. Flagged for the project,
|
|
# not fixed here (root cause not isolated further - the base Tag class's
|
|
# container-boundary bookkeeping is shared by many tags and risky to
|
|
# change without full regression coverage).
|
|
#
|
|
# Worked around instead: this specific <comment> block wraps content
|
|
# gated on $[_login_id] (show a logged-in session var menu) - the EXACT
|
|
# same "session-gated, always empty for an anonymous visitor" category
|
|
# already established as this conversion's design for cart.menu/login.menu/
|
|
# the ACCOUNT+ADMIN nav tabs (see below) - so stripping it entirely is
|
|
# consistent with the rest of this file, not a new compromise.
|
|
template_body = re.sub(r'<comment>\s*<if value1="\$\[_login_id\]".*?</if>\s*</comment>',
|
|
'', template_body, flags=re.DOTALL)
|
|
|
|
# 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):]) + SPA_SCRIPT
|
|
|
|
FULL_PAGE_TMPL = (
|
|
'<set name="jetc_doctype" value="<!DOCTYPE html>" />$[jetc_doctype]'
|
|
'<html lang="en"><head><title>%s</title>%s</head><body>'
|
|
'%s<div id="jetc-content">%s</div>%s</body></html>'
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
full_page = FULL_PAGE_TMPL % (page_title, STYLE_BLOCK, chrome_before, contents, CHROME_AFTER)
|
|
|
|
if name in MYSQL_PAGES:
|
|
# Real, reproducible server crash (heap corruption - "free():
|
|
# invalid pointer" seen live) when a <mysql> connection failure
|
|
# unwinds through the smaller fragment-mode <tag name="_frag_X">
|
|
# definition specifically - the exact same failure through the
|
|
# larger _full_X definition is fine (clean caught exception, 500).
|
|
# The client-side click handler already skips fetching these pages
|
|
# as fragments (FRAGMENT_EXCLUDED, above), but that only protects
|
|
# normal clicking - a stale already-open tab (running JS from
|
|
# before that fix shipped) or anyone hitting the URL directly
|
|
# would still trigger it. This is the real, server-side fix: the
|
|
# fragment response for these pages never attempts the <mysql>
|
|
# connection at all, so the crash is unreachable regardless of
|
|
# how the request arrives. Re-test once a real DB password makes
|
|
# these pages actually work - a successful connection takes a
|
|
# different code path that may not need this at all.
|
|
fragment_page = ('<div id="jetc-content"><p>This page requires a full load - '
|
|
'<a href="/%s">view it directly</a>.</p></div>') % name
|
|
else:
|
|
fragment_page = '<div id="jetc-content">%s</div>' % contents
|
|
|
|
# A direct/full-page load and an SPA-driven fragment fetch both hit this
|
|
# SAME route (/name) - distinguished by ?fragment=1 in the query string,
|
|
# per the user's explicit choice over a separate /name.page route. A
|
|
# normal load has no query string at all, so $[fragment] is simply
|
|
# unset - referencing an unset global variable throws ("global variable
|
|
# is not initialized.").
|
|
#
|
|
# Two real jetc bugs found and worked around while building this (both
|
|
# confirmed via direct minimal reproduction, not guessed):
|
|
# 1. <try> safely captures the possibly-unset $[fragment] into
|
|
# fragflag (always "1" or "" afterward) - but <try> must wrap ONLY
|
|
# the <set>, never an <if>. Nesting <if>/<else> INSIDE <try> silently
|
|
# drops the <else> branch's output specifically when compiled
|
|
# through jetc (works fine in jet-2.0's own interpreter; the bug is
|
|
# jetc-compiled-only). The branching <if> lives entirely OUTSIDE/
|
|
# AFTER <try> instead.
|
|
# 2. A large/complex body (real inlined HTML+CSS+JS, hundreds of KB)
|
|
# placed directly as an <if>'s branch content fails to COMPILE at
|
|
# all ("unterminated <html> - no matching close tag found") - jetc's
|
|
# own compile-time <if>/<else> body scanner isn't robust against
|
|
# real-world markup at this scale. Worked around by keeping the
|
|
# <if>'s own branches tiny (self-closing custom-tag invocations)
|
|
# and moving the real content into <tag name="..."> DEFINITIONS
|
|
# (which jetc never compile-time-parses at all - stored as opaque
|
|
# raw text, exactly like a <try> body, and only actually rendered
|
|
# via <dotag> at runtime).
|
|
body = (
|
|
'<try name="_fragerr"><set name="fragflag" expr="$[fragment]" /><catch><set name="fragflag" value="" /></try>'
|
|
'<tag name="_frag_%s">%s</tag><tag name="_full_%s">%s</tag>'
|
|
'<if value1="$[fragflag]" value2="1" type="eq"><_frag_%s /><else><_full_%s /></if>'
|
|
) % (name, fragment_page, name, full_page, name, name)
|
|
|
|
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)
|