Browser GUI generator
PatLang computing data (a fibonacci table) and generating a well-formed, interactive HTML+JS page around it โ the browser as PatLang's cross-platform GUI. Running live below: the iframe is the actual page this PatLang program just generated, in this browser, right now.
PatLang source
# =============================================================================
# HTML/JS page builder (Stage 1 compilable subset).
# Produces well-formed HTML5 documents with embedded JavaScript, for using
# the browser as PatLang's cross-platform GUI. Write pages with write_file
# or serve them with the tcp_* hosts.
#
# Conventions: use single quotes inside embedded JS/CSS/attributes so the
# escape-free Stage 1 dialect can express them directly; q() gives a double
# quote when one is unavoidable.
# =============================================================================
# Self-contained helpers (duplicated from lib/codegen.patlang so GUI programs
# need not pull in the compiler libraries)
make a function called q returns s
return chr(34)
end
make a function called nl returns s
return chr(10)
end
make a function called html_escape takes s returns out
let b = sb_new()
let i = 0
while i < s.length do
let c = char_code(s, i)
if c == 38 then
sb_push(b, "&")
else
if c == 60 then
sb_push(b, "<")
else
if c == 62 then
sb_push(b, ">")
else
if c == 34 then
sb_push(b, """)
else
sb_push(b, s[i])
end
end
end
end
let i = i + 1
end
return sb_str(b)
end
# Standard page chrome: dark-and-light friendly minimal styling
make a function called html_default_style returns s
let b = sb_new()
sb_push(b, ":root { color-scheme: light dark; }" + nl())
sb_push(b, "body { font-family: system-ui, sans-serif; max-width: 40rem; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }" + nl())
sb_push(b, "h1 { font-size: 1.4rem; }" + nl())
sb_push(b, "button, input { font: inherit; padding: 0.3rem 0.7rem; }" + nl())
sb_push(b, "ul { padding-left: 1.2rem; }" + nl())
sb_push(b, ".card { border: 1px solid color-mix(in srgb, currentColor 25%, transparent); border-radius: 8px; padding: 1rem; margin: 1rem 0; }" + nl())
sb_push(b, "pre { overflow-x: auto; background: color-mix(in srgb, currentColor 8%, transparent); border-radius: 6px; padding: 0.6rem; font-size: 0.85rem; }" + nl())
sb_push(b, "pre.src { max-height: 22rem; }" + nl())
sb_push(b, "pre.out { min-height: 1.5rem; }" + nl())
sb_push(b, ".label { margin-bottom: 0.2rem; opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, ".ms { opacity: 0.75; font-size: 0.9rem; }" + nl())
sb_push(b, "summary { cursor: pointer; }" + nl())
return sb_str(b)
end
# Well-formed HTML5 document: html_page(title, body_html, script_js)
make a function called html_page takes title, body, script returns out
let b = sb_new()
sb_push(b, "<!DOCTYPE html>" + nl())
sb_push(b, "<html lang='en'>" + nl())
sb_push(b, "<head>" + nl())
sb_push(b, "<meta charset='utf-8'>" + nl())
sb_push(b, "<meta name='viewport' content='width=device-width, initial-scale=1'>" + nl())
sb_push(b, "<title>" + html_escape(title) + "</title>" + nl())
sb_push(b, "<style>" + nl() + html_default_style() + "</style>" + nl())
sb_push(b, "</head>" + nl())
sb_push(b, "<body>" + nl())
sb_push(b, body + nl())
sb_push(b, "<script>" + nl())
sb_push(b, "'use strict';" + nl())
sb_push(b, script + nl())
sb_push(b, "</script>" + nl())
sb_push(b, "</body>" + nl())
sb_push(b, "</html>" + nl())
return sb_str(b)
end
# HTTP response wrapper for serving pages via the tcp_* hosts
make a function called http_ok takes content_type, body returns out
let sep = chr(13) + chr(10)
let b = sb_new()
sb_push(b, "HTTP/1.1 200 OK" + sep)
sb_push(b, "Content-Type: " + content_type + "; charset=utf-8" + sep)
sb_push(b, "Content-Length: " + body.length + sep)
sb_push(b, "Connection: close" + sep)
sb_push(b, sep)
sb_push(b, body)
return sb_str(b)
end
# GUI demo in the Stage 1 self-hosted language.
# PatLang computes data (fibonacci table), then generates a well-formed
# interactive HTML+JS page: the browser is the cross-platform GUI.
# Compiled natively (or to WASM) via the PatLang front-end; writes
# gui_demo.html next to the working directory.
make a function called fib takes n returns r
if n < 2 then
return n
else
return fib(n - 1) + fib(n - 2)
end
end
make a function called fib_json takes count returns out
let b = sb_new()
sb_push(b, "[")
let i = 0
while i < count do
if i > 0 then
sb_push(b, ", ")
end
sb_push(b, "" + fib(i))
let i = i + 1
end
sb_push(b, "]")
return sb_str(b)
end
make a function called build_body returns out
let b = sb_new()
sb_push(b, "<h1>PatLang → Browser GUI</h1>" + nl())
sb_push(b, "<p>The data below was computed by native PatLang code at build time; the interactivity is generated JavaScript.</p>" + nl())
sb_push(b, "<div class='card'>" + nl())
sb_push(b, "<label>Show values above: <input type='number' id='threshold' value='0' min='0'></label>" + nl())
sb_push(b, "<button id='refresh'>Filter</button>" + nl())
sb_push(b, "<ul id='out'></ul>" + nl())
sb_push(b, "</div>" + nl())
return sb_str(b)
end
make a function called build_script takes data returns out
let b = sb_new()
sb_push(b, "const fib = " + data + ";" + nl())
sb_push(b, "function render() {" + nl())
sb_push(b, " const min = Number(document.getElementById('threshold').value) || 0;" + nl())
sb_push(b, " const out = document.getElementById('out');" + nl())
sb_push(b, " out.innerHTML = '';" + nl())
sb_push(b, " fib.forEach((v, i) => {" + nl())
sb_push(b, " if (v >= min) {" + nl())
sb_push(b, " const li = document.createElement('li');" + nl())
sb_push(b, " li.textContent = 'fib(' + i + ') = ' + v;" + nl())
sb_push(b, " out.appendChild(li);" + nl())
sb_push(b, " }" + nl())
sb_push(b, " });" + nl())
sb_push(b, "}" + nl())
sb_push(b, "document.getElementById('refresh').addEventListener('click', render);" + nl())
sb_push(b, "document.getElementById('threshold').addEventListener('input', render);" + nl())
sb_push(b, "render();" + nl())
return sb_str(b)
end
let data = fib_json(16)
let page = html_page("PatLang GUI Demo", build_body(), build_script(data))
write_file("gui_demo.html", page)
print("WROTE: gui_demo.html (" + page.length + " chars)")
Native run on the build machine (writes a real file instead of rendering into an iframe):
WROTE: gui_demo.html (1880 chars)