Source code for k1lib.cli.kjs

# AUTOGENERATED FILE! PLEASE DON'T EDIT HERE. EDIT THE SOURCE NOTEBOOKS INSTEAD
"""
This is for support for transpilation of clis to JS
"""
__all__ = ["JsFunc", "toJsFunc"]
from k1lib.cli.init import BaseCli, fastF; import k1lib.cli.init as init
import k1lib.cli as cli; import k1lib, time, json, base64, math, re
settings = k1lib.settings.cli
settings.add("kjs", k1lib.Settings(), "cli.kjs settings")
kjsSett = settings.kjs
kjsSett.add("jsF",   {}, "All ._jsF() (cli to JS) transpile functions, looks like Dict[type -> _jsF(meta, **kwargs) transpile func]")
kjsSett.add("jsonF", {}, "All ._jsonF() JSON-serialization functions, looks like Dict[type -> _jsonF(obj) func]. See utils.deref.json() docs")
kjsSett.add("pyF",   {}, "(future feature) All ._pyF()   (cli to Python) transpile functions, looks like Dict[type -> _pyF  (meta, **kwargs) transpile func]")
kjsSett.add("cppF",  {}, "(future feature) All ._cppF()  (cli to C++)    transpile functions, looks like Dict[type -> _cppF (meta, **kwargs) transpile func]")
kjsSett.add("javaF", {}, "(future feature) All ._javaF() (cli to Java)   transpile functions, looks like Dict[type -> _javaF(meta, **kwargs) transpile func]")
kjsSett.add("sqlF",  {}, "(future feature) All ._sqlF()  (cli to SQL)    transpile functions, looks like Dict[type -> _sqlF (meta, **kwargs) transpile func]")
kjsSett.jsF[str] = lambda meta: (f"", "String")
kjsSett.jsF[int] = lambda meta: (f"", "parseInt")
kjsSett.jsF[float] = lambda meta: (f"", "parseFloat")
kjsSett.jsF[set] = lambda meta: (f"", "new Set")
kjsSett.jsF[list] = lambda meta: (f"", "Array.from")
kjsSett.jsF[tuple] = lambda meta: (f"", "Array.from")
kjsSett.jsF[abs] = lambda meta: (f"", "Math.abs")
kjsSett.jsF[json.loads] = lambda meta: (f"", "JSON.parse")
kjsSett.jsF[json.dumps] = lambda meta: (f"", "JSON.stringify")
kjsSett.jsF[base64.b64decode] = lambda meta: (f"", "atob")
kjsSett.jsF[base64.b64encode] = lambda meta: (f"", "btoa")
def _jst_round(meta, ndigits=None):                                              # _jst_round
    if ndigits is None: return "", f"Math.round"                                 # _jst_round
    else:                                                                        # _jst_round
        fIdx = init._jsFAuto(); dataIdx = init._jsDAuto()                        # _jst_round
        return f"{fIdx} = ({dataIdx}) => Math.round(({dataIdx})*Math.pow(10, {ndigits})+Number.EPSILON)/Math.pow(10, {ndigits})", fIdx # _jst_round
kjsSett.jsF[round] = _jst_round                                                  # _jst_round
import math; kjsSett.jsF[math.floor] = lambda meta: (f"", "Math.floor")          # _jst_round
import html                                                                      # _jst_round
def _jsF_html_escape(meta):                                                      # _jsF_html_escape
    fIdx = init._jsFAuto(); dataIdx = init._jsDAuto()                            # _jsF_html_escape
    return f"{fIdx} = ({dataIdx}) => {dataIdx}.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#39;');", fIdx # _jsF_html_escape
kjsSett.jsF[html.escape] = _jsF_html_escape                                      # _jsF_html_escape
def _jsonF_range(obj):                                                           # _jsonF_range
    if obj.step != 1: return "[" + ", ".join([f"{e}" for e in obj]) + "]"        # _jsonF_range
    end = f".slice({obj.start})" if obj.start != 0 else ""                       # _jsonF_range
    return f"[...Array({obj.stop}).keys()]{end}"                                 # _jsonF_range
kjsSett.jsonF[range] = _jsonF_range                                              # _jsonF_range
def v(x): return x if isinstance(x, str) else json.dumps(x)                      # v
def vs(xs): return [x if isinstance(x, str) else json.dumps(x) for x in xs]      # vs
class vOld(BaseCli):                                                             # vOld
    def __init__(self, v:str):                                                   # vOld
        """Specifies a variable should be taken on the js side"""                # vOld
        super().__init__(); self.v = v                                           # vOld
    def __ror__(self, it): return NotImplemented                                 # vOld
    def __str__(self): return f"\ue157{self.v}\ue239" # for injection into grep, and other clis that wants to extract this var out of a completely normal string # vOld
    def __repr__(self): return f"<kjs.v v={self.v}>"                             # vOld
    @staticmethod                                                                # vOld
    def getValue(value):                                                         # vOld
        """If it's an instance of :class:`v`, then return its internal value, else
return the same object passed in. Useful for streamlining building out other clis""" # vOld
        return value.v if isinstance(value, v) else value                        # vOld
class Arg:                                                                       # Arg
    def __init__(self, arg):                                                     # Arg
        if isinstance(arg, str):                                                 # Arg
            name = arg; type_ = str; default = ""                                # Arg
        elif isinstance(arg, (tuple, list)):                                     # Arg
            if len(arg) == 3:                                                    # Arg
                name, type_, default = arg                                       # Arg
            elif len(arg) == 2:                                                  # Arg
                name, type_ = arg                                                # Arg
                if type_ == str: default = ""                                    # Arg
                if type_ == int: default = 1                                     # Arg
                if type_ == float: default = 1.0                                 # Arg
                if type_ == bool: default = True                                 # Arg
                if isinstance(type_, (range, k1lib.serve.slider)): default = round((type_.start + type_.stop)/2) # surprisingly no bug here where .step != 1, because <input type="range"> covered us # Arg
                if isinstance(type_, list): default = type_[0]                   # Arg
                if isinstance(type_, k1lib.serve.date): default = time.time() | cli.toIso() # Arg
            else: raise Exception(f"Argument has to specify 3 values: (name:str, type, default value), or 2 values: (name:str, type)") # Arg
        else: raise Exception(f"Can't parse argument `{arg}`. It must be either the argument name alone, or a tuple of (name:str, type, default value)") # Arg
        self.idx = cli.init._jsDAuto(); self.name = name; self.type_ = type_; self.default = default # Arg
    def html(self):                                                              # Arg
        idx = self.idx; name = self.name; t = self.type_; default = self.default # Arg
        if t == str: return f"<input type='text' value='{default}' placeholder='{name}' id='{idx}' style='padding: 4px 4px'>" # Arg
        if t == int or t == float: return f"<input type='number' value='{default}' placeholder='{name}' id='{idx}' style='padding: 4px 4px'>" # Arg
        if t == bool: return f"<input type='checkbox' {'checked' if default else ''} id='{idx}' style='padding: 4px 4px'>" # Arg
        if isinstance(t, (range, k1lib.serve.slider)): return f"<div style='display: flex; flex-direction: row; align-items: center'><input type='range' id='{idx}' min='{t.start}' max='{t.stop}' value='{default}' step='{t.step}' /><span id='{idx}_preview' style='margin-left: 8px'></span></div>" # Arg
        if isinstance(t, list):                                                  # Arg
            if default not in t: raise Exception(f"Dropdown with options {t} does not have the default option {default}") # Arg
            s = "".join([f"<option value='{i}' {'selected' if e == default else ''}>{e}</option>" for i, e in enumerate(t)]) # Arg
            return f"<select id='{idx}'>{s}</select>"                            # Arg
        if isinstance(t, k1lib.serve.date):                                      # Arg
            value = f"value='{default}'" if default else ""; minDate = f"min='{t.minDate}'" if t.minDate else ""; maxDate = f"max='{t.maxDate}'" if t.maxDate else "" # Arg
            return f'<input id="{idx}" type="datetime-local" {value} {minDate} {maxDate} />' # Arg
        raise Exception(f"Don't know type {t} on .html()")                       # Arg
    def value(self): # grab value of interface                                   # Arg
        idx = self.idx; name = self.name; t = self.type_; default = self.default # Arg
        q = f"document.querySelector('#{self.idx}')"                             # Arg
        if t == str: return f"{q}.value"                                         # Arg
        if t == int: return f"parseInt({q}.value || 0)"                          # Arg
        if t == float: return f"parseFloat({q}.value || 0.0)"                    # Arg
        if t == bool: return f"{q}.checked"                                      # Arg
        if isinstance(t, range): return f"parseInt({q}.value)"                   # Arg
        if isinstance(t, k1lib.serve.slider): return f"parseFloat({q}.value)"    # Arg
        if isinstance(t, list): return f"{json.dumps(t)}[{q}.value]"             # Arg
        if isinstance(t, k1lib.serve.date): return f"{q}.value"                  # Arg
        raise Exception(f"Don't know type {t} on .value()")                      # Arg
    def preview(self):                                                           # Arg
        idx = self.idx; name = self.name; t = self.type_; default = self.default # Arg
        if isinstance(t, range): return f"document.querySelector('#{self.idx}_preview').innerHTML = parseInt(document.querySelector('#{self.idx}').value);" # Arg
        if isinstance(t, k1lib.serve.slider):                                    # Arg
            delta = 10**(-math.floor(math.log10(t.step))) # figuring out the correct order of magnitude to round to # Arg
            return f"document.querySelector('#{self.idx}_preview').innerHTML = Math.round(document.querySelector('#{self.idx}').value*{delta})/{delta};" # Arg
        return ""                                                                # Arg
    def onchange(self, cb:str):                                                  # Arg
        idx = self.idx; name = self.name; t = self.type_; default = self.default # Arg
        q = f"document.querySelector('#{self.idx}')"                             # Arg
        if t == str or t == int or t == float: return f"{q}.oninput = {cb};"     # Arg
        if t == bool: return f"{q}.onclick = {cb};"                              # Arg
        if isinstance(t, (range, k1lib.serve.slider, list, k1lib.serve.date)): return f"{q}.oninput = {cb};" # Arg
        raise Exception(f"Don't know type {t} on .onchange()")                   # Arg
class _JsFuncInterface:                                                          # _JsFuncInterface
    def __init__(self, jsFunc, mode, args, debounce, delay):                     # _JsFuncInterface
        self.jsFunc = jsFunc; self.mode = mode; self.args = args; self.debounce = debounce; self.delay = delay # _JsFuncInterface
        if delay and delay < debounce: raise Exception(f"delay value ({delay}) is lower than debounce value ({debounce}), which doesn't make sense. Please raise delay, or lower the debounce value") # _JsFuncInterface
    def _repr_html_(self):                                                       # _JsFuncInterface
        pre = init._jsFAuto(); mode = self.mode; args = self.args; debounce = self.debounce*1000 # _JsFuncInterface
        postprocess = ""                                                         # _JsFuncInterface
        if mode == "html":                                                       # _JsFuncInterface
            res = "val"; postprocess = f"""
containerDiv = document.createElement('div'); containerDiv.innerHTML = val;
for (const scriptElem of containerDiv.querySelectorAll('script')) eval(scriptElem.textContent);""" # _JsFuncInterface
        elif mode == "pre": res = "`<pre>${val}</pre>`"                          # _JsFuncInterface
        elif mode == "json": res = "`<pre>${JSON.stringify(val)}</pre>`"         # _JsFuncInterface
        elif mode == "jsone": res = "`<pre>${JSON.stringify(val, null, 2)}</pre>`" # _JsFuncInterface
        elif mode == "table": res = """ '<table>' + val.map((v) => "<tr>" + v.map((e) => `<td>${e}</td>`).join("") + "</tr>").join("") + '</table>' """ # _JsFuncInterface
        else: raise Exception(f"Unknown interface output mode {mode}")           # _JsFuncInterface
        inps = "".join([f"<div>{a.name}</div><div>{a.html()}</div>" for a in args]) # _JsFuncInterface
        inps = f"""<div style='display: grid; grid-template-columns: 1fr 3fr; width: 400px'>{inps}</div>""" # _JsFuncInterface
        onchanges = "".join([a.onchange(f"{pre}_reload") for a in args])         # _JsFuncInterface
        values = ", ".join([a.value() for a in args])                            # _JsFuncInterface
        preview = "\n".join([a.preview() for a in args])                         # _JsFuncInterface
        delayCode = f"(async () => {{ while (true) {{ await new Promise(r => setTimeout(r, {self.delay*1000})); await {pre}_reload_core(); }} }})();" if self.delay else "" # _JsFuncInterface
        return f"""\
<!-- k1lib.JsFuncInterface -->
<style>
    .{pre}_btn:hover {{
    }}
</style>
{inps}
<pre id="{pre}_error" style="color: red"></pre>
<div id="{pre}_loading" style="margin-top: 4px">&nbsp;</div>
<div id="{pre}_result"></div>
<script src="https://k1js.com/dist/amd/latest.js"></script>
<script>
    {pre}_loadF = () => {{
        {self.jsFunc.fn}
        window.{self.jsFunc.fIdx} = {self.jsFunc.fIdx};
        {pre}_error = document.querySelector("#{pre}_error");
        {pre}_resultDiv = document.querySelector("#{pre}_result");
        {pre}_loadingDiv = document.querySelector("#{pre}_loading");
        const runningCoreHandle = [null];
        const {pre}_reload_core = async (e) => {{
            if (runningCoreHandle[0]) {{ runningCoreHandle[0].cancel = true; }}
            const handle = {{"cancel": false}}; runningCoreHandle[0] = handle;
            try {{
                {pre}_error.innerHTML = "";
                {pre}_loadingDiv.innerHTML = "(loading...)";
                const val = await {self.jsFunc.fIdx}({values});
                if (!handle.cancel) {{
                    {pre}_loadingDiv.innerHTML = "&nbsp;";
                    {pre}_resultDiv.innerHTML = {res};
                    {postprocess}
                }}
            }} catch (e) {{ if (!handle.cancel) {pre}_error.innerHTML = e.stack; }}
            runningCoreHandle[0] = null;
        }};
        let {pre}_reload_timeout = null;
        const {pre}_reload = async (e) => {{
            {preview}
            if ({debounce} <= 0) return {pre}_reload_core();
            else {{
                clearTimeout({pre}_reload_timeout);
                {pre}_reload_timeout = setTimeout({pre}_reload_core, {debounce})
            }}
        }}
        {onchanges}
        {delayCode}
        {pre}_reload(null);
    }};
    // trying multiple loading schemes, because in a live jupyter nb, it's using umd, while in an exported jupyter nb, it's using amd. The madness of JS
    (async () => {{
        try {{
            require(['k1js'], function(k1js) {{ (async () => {{ window.k1js = await k1js; {pre}_loadF(); }})(); }});
        }} catch (e) {{}}
    }})();
    (async () => {{
        try {{
            const res = await (await fetch("https://k1js.com/dist/umd/latest.js")).text();
            eval(res); window.k1js = await k1js; {pre}_loadF();
        }} catch (e) {{}}
    }})();
</script>"""                                                                     # _JsFuncInterface
[docs]class JsFunc: # JsFunc
[docs] def __init__(self, fn:str, fIdx:str, args, _async): # JsFunc """Represents a generated function from :class:`toJsFunc` . More docs are available at that class. This is not supposed to be instantiated by the end user. Please use :class:`toJsFunc` instead. It's here for documentation purposes only""" # JsFunc self.fn = fn; self.fIdx = fIdx; self.args = args; self._async = _async # JsFunc
def __repr__(self): # JsFunc fn = self.fn.split('\n') | cli.head(300).all() | cli.join('\n') # JsFunc return f"""<JsFunc {self.fIdx}>\n\nGenerated JS function:\n\n{fn}""" # JsFunc def _repr_html_(self): # JsFunc fn = self.fn.split('\n') | cli.join('\n') # JsFunc header = html.escape(f"<JsFunc {self.fIdx}>") + "<br>Generated JS function:" # JsFunc try: return f"""{header}<br><br>{k1lib.fmt.js(fn)}""" # JsFunc except: return f"<pre style='overflow-x: auto'>{html.escape(repr(self))}</pre>" # JsFunc
[docs] def interface(self, mode="html", debounce:float=0.03, delay:float=None): # JsFunc """Creates an interface. Different output modes: - html: expected the js function to return html and will display it as-is - pre: wraps the function result inside a "pre" tag, which makes html honor white spaces and whatnot - json: json formats the function result in a compact way on 1 line - jsone: "json expanded", same as above, but in an expanded way on multiple lines - table: expects function result to be a table, then displays the table nicely :param mode: see above :param debounce: if specified, will wait for that many seconds until the user hasn't modified the input UIs. If function is not async (meaning it should executes quite fast), then this param will be ignored""" # JsFunc return _JsFuncInterface(self, mode, self.args, debounce if self._async else 0, delay) # JsFunc
def moveOut(code:str): # move "k1_moveOutStart - k1_moveOutEnd" blocks to the top # moveOut pattern = r'//k1_moveOutStart(.*?)//k1_moveOutEnd'; matches = re.findall(pattern, code, re.DOTALL); heads = [] # moveOut def process_match(match): heads.append(match.group(1).strip()); return '' # moveOut res = re.sub(pattern, process_match, code, flags=re.DOTALL); pre = '\n'.join(heads); return f"//k1_moveOutStart\n{pre}\n//k1_moveOutEnd\n{res}" # moveOut
[docs]class toJsFunc(BaseCli): # toJsFunc
[docs] def __init__(self, *args, delay:float=None): # toJsFunc """Capture cli operations and transpiles all of them to JS, to quickly build functionalities in JS. Example:: # creates an instance of class JsFunc that contains the transpiled js code jsf = range(10) | deref() | (toJsFunc("term") | head(kjs.v("term")) & head(6)) # displays a search interface to jupyter notebook jsf.interface("json") So a little more background on how this works. With clis, it can "capture" other clis to the right of it if it desires. So, ``toJsFunc`` will capture the 2 ``head``s and transpile them to js. So Python data never really gets passed through those later heads. Read more about this at :class:`~k1lib.cli.init.BaseCli` and on the main cli page at https://k1lib.com/latest/cli/index.html The arguments passed in, namely "term" here, specifies what arguments should the JS function have. Then you can reference those JS arguments downstream inside any cli, like ``head(kjs.v("term"))``. The input ``range(10) | deref()`` actually gets converted into json and injected as data directly within the function, so it's pretty sweet. It does require the input to be comprised of relatively simple data structures, with no arbitrary objects. This is a relatively experimental/novel feature. The transpilation mechanism can do a lot, but if it sees that something is too complicated, then it will raise an error and complain, instead of failing silently. When creating an interface, you can supply them with multiple different types of UI: .. pyexec:: html True arg1 = "extraText" arg2 = ("on", bool) arg3 = ("someNum", int) arg4 = ("n", range(0, 10, 2)) arg5 = ("character", ["reimu", "marisa", "cirno"]) jsF = list(range(100)) | (toJsFunc(arg1, arg2, arg3, arg4, arg5) | head("n") & head("on*10") | aS("[*x, character, someNum**2, len(extraText)*2]")) jsF.interface("json") """ # toJsFunc super().__init__(capture=True) # toJsFunc self.args = [Arg(a) for a in args] # toJsFunc
[docs] def __ror__(self, it) -> JsFunc: # toJsFunc header, fn, _async = k1lib.kast.asyncGuard(self.capturedSerial._jsF(("root",))) # used to be (kast, self.args) # toJsFunc header = "\n".join([f" {e}" for e in header.split("\n")]); fIdx = init._jsFAuto(); dataIdx = init._jsDAuto() # toJsFunc return JsFunc(moveOut(f""" {dataIdx} = {it | cli.deref.js()}; async function {fIdx}({', '.join([a.name for a in self.args])}) {{ {header} return {'await ' if _async else ''}{fn}({dataIdx}); }}"""), fIdx, self.args, _async) # toJsFunc
def _jsF(self, meta): # toJsFunc """Hidden feature, cause this is getting way too complicated and this idea has not been fully fleshed out yet .. admonition:: Nested toJsFunc() Surprisingly and mind bendingly, this can transpile itself, but the use case is rather different. Here's a dead simple example:: jsF = 3 | (toJsFunc() | op()+2 | (toJsFunc("args", "are", "ignored", "here") | aS("x*2"))) """ # toJsFunc fIdx = cli.init._jsFAuto(); dataIdx = cli.init._jsDAuto(); argIdx = cli.init._jsDAuto(); pre = cli.init._jsDAuto(); # toJsFunc h1,f1,a1 = k1lib.kast.asyncGuard(self.capturedSerial._jsF(meta)) # toJsFunc return f"""//k1_moveOutStart\nlet {dataIdx} = null;\n{h1}\n//k1_moveOutEnd {fIdx} = ({argIdx}) => {{ // returns html string that will run the function on load {dataIdx} = {argIdx}; return unescape(`<div id='{pre}_div'></div> %3Cscript%3E setTimeout({'async ' if a1 else ''}() => {{ console.log("executed"); document.querySelector('#{pre}_div').innerHTML = {'await ' if a1 else ''}{f1}({dataIdx}) }}, 100); %3C/script%3E`); }};""", fIdx # toJsFunc