packages = ["htag>=0.90.0"] Loading DEMO ;-) ############################################################################### from htag import Tag import urllib.parse import json,sys import ast,os,re import base64,zlib DEFAULT="""from htag import Tag class App(Tag.body): def init(self): def say_hello(o): self <= Tag.li("hello") self <= Tag.button("click",_onclick = say_hello) """ IMG="data:image/png;base64,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" TEMPLATE = base64.b64decode(b"""PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9InV0Zi04Ij4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgaHJlZj0iaHR0cHM6Ly9weXNjcmlwdC5uZXQvbGF0ZXN0L3B5c2NyaXB0LmNzcyIgLz4KICAgIDxzY3JpcHQgZGVmZXIgc3JjPSJodHRwczovL3B5c2NyaXB0Lm5ldC9sYXRlc3QvcHlzY3JpcHQuanMiPjwvc2NyaXB0PgogICAgPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCwgaW5pdGlhbC1zY2FsZT0xIj4KICAgIDxweS1jb25maWc+CiAgICBwYWNrYWdlcyA9ICVzCiAgICA8L3B5LWNvbmZpZz4gICAgCjwvaGVhZD4KPGJvZHk+CjxweS1zY3JpcHQ+CiMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMKJXMKIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIwpmcm9tIGh0YWcucnVubmVycyBpbXBvcnQgUHlTY3JpcHQKZnJvbSBqcyBpbXBvcnQgd2luZG93ClB5U2NyaXB0KCBBcHAgKS5ydW4oIHdpbmRvdyApCjwvcHktc2NyaXB0Pgo8L2JvZHk+CjwvaHRtbD4=""").decode() ################################################################ ## pyodide.pyfetch -> request(url,**args) ################################################################ from pyodide.http import pyfetch async def request(url: str, method: str = "GET", body = None, headers = None, **fetch_kwargs ): kwargs = {"method": method, "mode": "cors"} # CORS: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing if body and method not in ["GET", "HEAD"]: kwargs["body"] = body if headers: kwargs["headers"] = headers kwargs.update(fetch_kwargs) return await pyfetch(url, **kwargs) ################################################################ def zlibB64Encode(x:str) -> str: return base64.urlsafe_b64encode( zlib.compress(x.encode()) ).decode() def b64ZlibDecode(x:str) -> str: return zlib.decompress(base64.urlsafe_b64decode(x.encode())).decode() def get_external_modules(code) -> list: # https://stackoverflow.com/questions/2572582/return-a-list-of-imported-python-modules-used-in-a-script modules = set() def visit_Import(node): for name in node.names: modules.add(name.name.split(".")[0]) def visit_ImportFrom(node): # if node.module is missing it's a "from . import ..." statement # if level > 0 it's a "from .submodule import ..." statement if node.module is not None and node.level == 0: modules.add(node.module.split(".")[0]) node_iter = ast.NodeVisitor() node_iter.visit_Import = visit_Import node_iter.visit_ImportFrom = visit_ImportFrom node_iter.visit(ast.parse(code)) return list(modules - set(sys.stdlib_module_names)) def generate_python(code:str) -> str: top="# -*- coding: utf-8 -*-\n# YOU WILL NEED (at least) HTAG, just pip it:\n# python3 -m pip install htag\n" sep="#"*78 +"\n" end="""if __name__=="__main__": from htag.runners import BrowserHTTP BrowserHTTP( App ).run()""" return top+sep+code+sep+end def generate_pyscript(code:str) -> str: externals = get_external_modules(code) return TEMPLATE % (json.dumps( externals ),code) def dataurlh(data): return 'data:text/html,'+urllib.parse.quote(data) def dataurlt(data): data64 = base64.b64encode(data.encode()) return 'data:text/plain;base64,'+data64.decode() class Ed(Tag.div): """ A class which embed the ace editor (python syntax) """ statics = [ Tag.script(_src="//cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ace.js"), Tag.script(_src="//cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/mode-python.js"), Tag.script(_src="//cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/theme-cobalt.js"), Tag.script(_src="//cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ext-searchbox.js"), Tag.script(_src="//cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ext-language_tools.js"), ] def init(self,value,mode="python",onsave=None,**a): self.value = value oed=Tag.div(self.value,_style="width:100%;height:100%;min-height:20px;") self += oed self.onsave=onsave self.js = """ self.ed=ace.edit( "%s" ); self.ed.setTheme("ace/theme/cobalt"); self.ed.session.setMode("ace/mode/%s"); self.ed.session.setUseWrapMode(false); self.ed.setOptions({"fontSize": "12pt", "enableBasicAutocompletion": true}); self.ed.setBehavioursEnabled(false); self.ed.session.setUseWorker(false); self.ed.getSession().setUseSoftTabs(true); self.ed.commandSave=function() {%s} self.ed.commands.addCommand({ name: "commandSave1", bindKey: {"win": "Ctrl-S", "mac": "Command-S"}, readOnly: "True", exec: self.ed.commandSave, }) self.ed.commands.addCommand({ name: "commandSave2", bindKey: {"win": "Ctrl-Enter", "mac": "Command-Enter"}, readOnly: "True", exec: self.ed.commandSave, }) """ % (id(oed),mode, self.bind._save( b"self.ed.getValue()")) def _save(self,value): self.value = value if self.onsave: self.onsave(self) class Back(Tag.div): def init(self,o,actions=""): # background = Tag.div( content=None,_style="z-index:1000000;position:absolute;top:0px;left:0px;right:0px;bottom:0px;background:#EEE;opacity:0.5;cursor:pointer;",_onclick=self.close ) background = Tag.div( content=None,_style="z-index:1000000;position:absolute;top:0px;left:0px;right:0px;bottom:0px;background:#FFF;cursor:pointer;",_onclick=self.close ) background += Tag.button("      Back      ",_onclick=self.close,_class="right red") background += actions inputs = Tag.div( o,_style="z-index:1000001;position:absolute;top:31px;left:0px;right:0px;bottom:0px;background:white;border:1px solid black" ) # draw ui self += background self += inputs def close(self,o=None): self.remove() class HTDemo(Tag.body): statics="""html, body {width:100%;height:100%;margin:0px;font-family:arial;} * {box-sizing: border-box;} .right {float:right} button { color: #ffffff; text-decoration: none; padding: 4px 14px 4px 14px; border-radius: 4px; display: inline-block; border: none; margin-left:2px; margin-top:1px; margin-bottom:1px; cursor:pointer; font-size:1.1em; } button, button * {vertical-aligm:middle} button.green {background:#1A1;} button.red {background:#A11;} button.blue {background:#22C;} button.white {background:#EEE;color:black} iframe {width:100%;height:100% } """ imports=Ed,Back def init(self, code:str=""): if code: if code.startswith("Z:"): # a zip/b64 string to decode code = b64ZlibDecode(code[2:]) elif re.match(r"\d+-.+\.py",code): # it's a filename example (on mlan.fr/pub/htdemos/*) self.call.preload(code) code=DEFAULT else: code=DEFAULT self._back = None self.ed = Ed(code, onsave=self.do_preview,_style="height:100%;flex:1 1 auto") self.ui = Tag.div() # draw ui self.top=Tag.div(Tag.b("htag demo",_style="font-size:1.6em;padding:4px"),_class="top") self.top += Tag.button("Run",_onclick=lambda o: self.ed.call("self.ed.commandSave()"),_class="right green",_title="Run this (CTRL+Enter)") self.top += Tag.button("Help",_onclick=self.do_help,_class="blue right",_title="Need help") self += Tag.div( self.top + self.ed,_style="display:flex;flex-flow:column;height:100%;width:100%;overflow:hidden") self += self.ui self.call( """ window.addEventListener("message", (event) => { if(event && event.data && event.data.startsWith && event.data.startsWith("Z:")) %s; }) """ % self.bind.updateFromMessageZB64( b"event.data.substr(2)" ) ) def updateFromMessageZB64(self,zb64): self.setCode(b64ZlibDecode(zb64)) async def preload(self,htdemo_filename:str): r=await request("https://www.mlan.fr/pub/htdemos/"+htdemo_filename) if r.status==200: self.setCode( await r.string() ) def setCode(self,code): o = json.dumps( dict(code=code) ) self.ed.call( f""" self.ed.setValue( {o}.code ); self.ed.moveCursorToPosition( {{row:0,column:0}} ); self.ed.scrollToLine( 0,true,true ); self.ed.focus(); """) if self._back: self._back.close() self._back=None def do_help(self,o): self._back = Back(Tag.iframe(_src="https://www.mlan.fr/htdemo")) self.ui+= self._back def do_preview(self,o): html = generate_pyscript(o.value) test=Tag.button( Tag.img(_src=IMG,_width="18px")+"py",_style="float:right",_onclick=self.download_p,_class="button white",_title="Download as python file") test2=Tag.button(Tag.img(_src=IMG,_width="18px")+"demo",_style="float:right",_onclick=self.download_h,_class="button white",_title="Download as pyscript/html demo file") self.ui+=Back(Tag.iframe(_src=dataurlh(html)),test+test2) def download_h(self,o): self._download( dataurlh(generate_pyscript(self.ed.value)),"YourHtagApp.html") def download_p(self,o): self._download( dataurlt(generate_python(self.ed.value)),"YourHtagApp.py") def _download(self,content,name="file.txt"): self.call( f""" let a = document.createElement('a'); a.href = `{content}`; a.target = '_blank'; a.download = `{name}`; a.click(); """) App=HTDemo ############################################################################### from htag.runners import PyScript from js import window PyScript( HTDemo ).run( window )