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, "&amp;")
    else
      if c == 60 then
        sb_push(b, "&lt;")
      else
        if c == 62 then
          sb_push(b, "&gt;")
        else
          if c == 34 then
            sb_push(b, "&quot;")
          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 &rarr; 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)