render

Put a demo on a page: the bundle, a static page, show for notebooks, md for Markdown, and Demo for FastHTML.

One runtime draws a spec everywhere. A host element carries the spec as inert JSON, and the runtime mounts it:

<div data-rupaka><script type="application/json">{...spec...}</script></div>

The runtime boots on load and after each htmx swap. Nothing is templated into the script.

The bundle

bundle joins the core, the fns, the blocks and the boot script in a fixed order. There is no bundler and no build step.


source

bundle

def bundle(
    kind:str='js'
)->str:

The runtime as one script (js) or one style sheet (css).


source

assets

def assets()->fastcore.basics.AttrDict:

The files of the runtime, in bundle order.

The script starts with the core and ends with the boot. Each block is in it.

js = bundle()
assert js.startswith('/* static/rupaka.js */') and js.rstrip().endswith('})();')
for n in manifests(): assert f"Rupaka.block('{n}'" in js, n
assert 'eval(' not in js and 'new Function' not in js and 'fetch(' not in js

Each block style uses only the --rk-* vars for colour, so a demo takes the look of the page that holds it.

import re
for p in assets().css[1:]:
    cols = re.findall(r'(?:#[0-9a-fA-F]{3,6}\b|rgba?\(|hsl\()', p.read_text())
    test_eq((p.name, cols), (p.name, []))

The host element

host writes the element that carries a spec. JSON in a <script> must not hold </script>, so every < becomes \u003c. JSON.parse reads it back as <.


source

host

def host(
    spec
)->str:

The host element: a div that carries its spec as inert JSON.


source

spec_json

def spec_json(
    spec
)->str:

The spec as JSON that is safe inside a <script> element.

h = host(dict(id='x', ask='Do not stop at </script> here.'))
assert '</script>' not in h.split('<script type="application/json">')[1][:-len('</script></div>')]
test_eq(json.loads(h.split('json">')[1].split('</script>')[0]), dict(id='x', ask='Do not stop at </script> here.'))

A static page

page writes one self-contained HTML file: the runtime, the style, the specs, and abhyasa’s palette. The fonts argument picks how the page gets its fonts:

fonts what it does
'link' links Literata and Libre Baskerville from ./fonts (save copies them)
'embed' puts the fonts in the page as data URIs, about 160 KB more
'none' uses the system serif; for a sandboxed srcdoc frame, which cannot load files

source

font_css

def font_css(
    fonts:str='link', base:str='fonts/'
)->str:

The @font-face rules: linked from base, embedded as data URIs, or none.

page takes one spec or a list of specs. A list makes a gallery page with a rule between the demos.


source

page

def page(
    specs, title:str=None, fonts:str='link', lead:str=''
)->str:

One self-contained HTML document that draws specs, with no network.

A page loads nothing from the network: no src, href or url() points at http. (The SVG namespace string is not a request.) With fonts='none' it has no @font-face rule.

spec = dict(rupaka=1, id='t', title='T', state=dict(a=1), blocks=[dict(type='readout', text='{a}')])
p = page(spec)
assert not re.search(r'''(src|href)=["']?https?:|url\(\s*["']?https?:''', p)
assert '@font-face' in p and '@font-face' not in page(spec, fonts='none')
assert 'data:font/woff2;base64,' in page(spec, fonts='embed')

save writes a page to a file. With linked fonts, it copies the font files beside it, so the file works from file://.


source

save

def save(
    specs, path, fonts:str='link', **kw
)->pathlib.Path:

Write page(specs) to path; with fonts='link', copy the fonts to fonts/ beside it.

import tempfile
out = save(spec, Path(tempfile.mkdtemp())/'t.html')
assert (out.parent/'fonts'/'literata-latin-wght-normal.woff2').exists()

Notebooks: show

show(spec) returns an object that a notebook draws. It gives two mime types:

  • application/vnd.rupaka+json: the spec, for a frontend that knows rupaka (a later leela change).
  • text/html: the whole page with fonts='none'. leela, Jupyter and VS Code draw it in a sandboxed frame. This works in leela today.

source

show

def show(
    spec
)->__main__.Show:

Draw spec in a notebook output.


source

Show

def Show(
    spec
):

A demo for a notebook output: the rupaka mime and a full HTML page.

mb = show(spec)._repr_mimebundle_()
test_eq(mb[MIME], spec)
assert '<script' in mb['text/html'] and '@font-face' not in mb['text/html']

Markdown: md

md(spec) writes a ```rupaka fence with readable JSON. A renderer that knows rupaka mounts it; others show the JSON as code. md(spec, frame=True) writes a ```{=html} fence that holds a sandboxed frame. leela draws this in a Markdown cell today, with no change to leela.


source

md

def md(
    spec, frame:bool=False, height:int=760
)->str:

A Markdown fence for spec: readable JSON, or a sandboxed frame.

assert md(spec).startswith('```rupaka\n{') and json.loads(md(spec)[10:-4]) == spec
f = md(spec, frame=True)
assert f.startswith('```{=html}\n<iframe') and '<script' not in f and '&lt;script' in f

FastHTML: Demo and Assets

Demo(spec) is the host element as an FT component. Assets(prefix) gives the Link and the Script for a server that serves the bundle at prefix.


source

Assets

def Assets(
    prefix:str='/rupaka', v:str=''
):

The style and the script of the runtime, served at prefix; v busts the cache.


source

Demo

def Demo(
    spec, **kw
):

The host element of spec for a FastHTML page.

x = to_xml(Demo(dict(id='d', ask='a </script> b')))
assert '<script type="application/json">' in x and 'data-rupaka' in x and 'a \\u003c/script> b' in x
test_eq(to_xml(Assets(v='1')[1]), '<script src="/rupaka/rupaka.js?v=1"></script>')

version() is a short hash of the bundle, for a cache-busting v.


source

version

def version()->str:

A short hash of the script and the style.

test_eq(len(version()), 10)