Edit and run javascript code inside Logseq itself

  • The theoretical background and discussion is here.
  • A specific implementation and its discussion is below.
  • It supports other languages as well. Please visit their threads for directions:

Is this for you?

  • Do you want a Jupyter-like experience?
  • Do you need extra functionality but are tired of searching:
    • for the proper plugin
      • and waiting for its author to make changes?
    • which plugin breaks something?
  • Have you messed with file custom.js?
    • Tired of restarting after every change?
    • custom.js getting too big?
  • Initial loading of Logseq too slow?
  • Wished to:
    • be in control of code loading?
      • load code only when actually needed?
      • control the loading order?
    • develop for Logseq from within Logseq itself?
    • comment on code with Logseq notes?

Introducing kits

  • Define a few buttons in file config.edn, inside macros{}:
:evalpage "<button class='kit eval' data-kit='evalpage'>► Evaluate code of open code-blocks</button>"
:evalparent "<button class='kit eval' data-kit='evalparent'>► Evaluate code of parent block</button>"
:runpage "<button class='kit run' data-kit='runpage' data-page-name='$1'>► Run code of page $1</button>"
:togglemsg "<button class='kit' data-kit='togglemsg'>► Toggle messages</button>"
  • Define a few css rules in file custom.css:
button.kit {
  background: var(--ls-tertiary-background-color);
  padding: 2px 4px 2px 4px;
}
button.out {
  background: var(--ls-tertiary-background-color);
  display: inline-table;
  line-height: 12px;
  margin: 0px 3px 0px 3px;
  padding: 2px;
}
div.out button.out {
    visibility: hidden;
}
div.out:hover button.out {
    visibility: visible;
}
div.out {
  font-size: 14px;
}
span.out {
  display: inline-table;
  font-size: 14px;
  padding: 2px;
  white-space: pre-wrap;
}
  • Put the following code in file custom.js (create inside folder logseq if missing):
const AsyncFunction = async function(){}.constructor;
function Concept(_key){
    return {_key, __proto__: Concept.prototype};
}
Concept.prototype.setChild = Concept.setChild = function setChild(name){
    const child = Concept(name);
    this[name] = child;
    return child;
}
Concept.prototype.setStatic = function setStatic(func){
    this[func.name] = func;
    return this;
}

const Language = logseq.Language = Concept.setChild("Language");
const Module = logseq.Module = Concept.setChild("Module");

const Kits = Module.setChild("Kits")
.setStatic(function addClickEventToButton(handler, button){
    button.addEventListener("click", function onClicked(e){
        e.preventDefault();
        e.stopPropagation();
        handler(e);
    });
})
.setStatic(function createElementOfClass(tag, className, ...children){
    const elem = document.createElement(tag);
    elem.classList.add(className);
    elem.append(...children);
    return elem;
})
.setStatic(function evalDiv(div){
    const blockId = div && div.getAttribute("blockid");
    const block = logseq.api.get_block(blockId);
    const divRow = Kits.onParentEvalStarted(div);
    Kits.runRoot(block).then(Kits.onParentEvalFinished.bind(null, divRow));
})
.setStatic(function getKitByName(name){
    var handler = kits[name];
    if (typeof handler === "function") return Promise.resolve(handler);

    return Kits.runPageByName(name).then( ()=>kits[name] );
})
.setStatic(function loadDependencies(dependencies){
    const langs = Object.values(dependencies);
    if (langs.length < 1) return;

    return Promise.all(langs.map( (lang)=>lang.load() ))
        .then( ()=>(new Promise( (resolve)=>setTimeout(resolve, 1000) )) );
})
.setStatic(function onLoadFailed(module, er){
    Msg.ofStatus("could not load " + module, "error");
    throw(er);
})
.setStatic(function onObserverFinished(nofFound, missing){
    if (nofFound) Msg.info("handled " + nofFound + " macro(s)");
    if (missing.length > 1) Msg.warning(missing.join("\n"));
})
.setStatic(function onParentEvalFinished(divRow, res){
    if (typeof res === "string" && res.slice(0, 10) === "data:image") {
        const img = Kits.createElementOfClass("img", "out");
        img.setAttribute("src", res);
        res = img;
    }

    divRow.lastChild.remove();
    divRow.lastChild.after("=>", Kits.createElementOfClass("span", "out", res));
})
.setStatic(function onParentEvalStarted(container){
    const btnRemove = Kits.createElementOfClass("button", "out", "X");
    const divRow = Kits.createElementOfClass("div", "out", btnRemove, "...running...");

    const wrapper = container.getElementsByClassName("block-content-wrapper")[0];
    wrapper.append(divRow);

    Kits.addClickEventToButton(Kits.onRemoveClicked.bind(null, wrapper), btnRemove);
    return divRow;
})
.setStatic(function onRemoveClicked(wrapper, e){
    if (!e.ctrlKey) return e.target.parentElement.remove();
    if (!e.shiftKey) {
        return Kits.removeElems(wrapper.querySelectorAll("div.out"));
    }

    const divs = document.querySelectorAll("div.ls-block div[data-lang]");
    Array.prototype.forEach.call(divs, (div)=>{
        const container = div.closest("div.ls-block");
        Kits.removeElems(container.querySelectorAll("div.out"));
    });
})
.setStatic(function onRootRunFailed(er){
    Msg.error("run failed");
    throw(er);
})
.setStatic(function onRootRunFinished(nofCodeblocks, res){
    Msg.success("ran " + nofCodeblocks + " codeblock(s)");
    return res;
})
.setStatic(function removeElems(elems){
    Array.prototype.forEach.call(elems, (elem)=>elem.remove() );
})
.setStatic(function runRoot(root){
    var begin;
    const dependencies = {};
    var prom = new Promise( (resolve)=>begin=resolve )
        .then(Kits.loadDependencies.bind(null, dependencies));

    var nofCodeblocks = 0;
    const blocks = (root.children) ? [root] : logseq.api.get_page_blocks_tree(root.name);
    blocks.forEach(Kits.traverseBlocksTree, (block)=>{
        const content = block.content;
        const codeStart = content.indexOf("```") + 3;
        if (codeStart < 3) return;

        const langEnd = content.search(/\w\W/);
        const strLang = content.slice(codeStart, langEnd + 1);
        var lang = logseq.Language[strLang];
        if (!lang) return;

        if (!lang.eval) dependencies[lang._key] = lang;
        const lineEnd = content.indexOf("\n", codeStart);
        const codeEnd = content.indexOf("```", lineEnd);
        if (codeEnd < 0) return;

        nofCodeblocks += 1;
        const code = content.slice(lineEnd, codeEnd);
        prom = prom.then( ()=>lang.eval(code) );
    });

    begin();
    return prom
        .then(Kits.onRootRunFinished.bind(null, nofCodeblocks))
        .catch(Kits.onRootRunFailed);
})
.setStatic(function runPageByName(pageName){
    var page = logseq.api.get_page(pageName);
    if (page) return Kits.runRoot(page);

    Msg.warning("page not found");
    return Promise.resolve();
})
.setStatic(function traverseBlocksTree(block){
    if (Array.isArray(block)) return;

    this(block);
    block.children.forEach(traverseBlocksTree, this);
});

const Msg = Module.setChild("Msg")
.setStatic(function cond(status, msg){
    if (Msg.state === "on") Msg.ofStatus(msg, status);
});
Msg.error = Msg.cond.bind(null, "error");
Msg.info = Msg.cond.bind(null, "info");
Msg.success = Msg.cond.bind(null, "success");
Msg.warning = Msg.cond.bind(null, "warning");
Msg.ofStatus = logseq.api.show_msg;
Msg.state = "off";

const JS = Language.setChild("javascript")
.setStatic(function eval(code){
    return AsyncFunction(code)();
});

const Python = Language.setChild("python")
.setStatic(function load(uri){
    Msg.ofStatus("Preparing python...", "info");
    return import(uri || Python.pyodideUri)
        .then(Python.onLoaderFetched)
        .then(Python.onPyodideLoaded)
        .catch(Python.onFail);
})
.setStatic(function onLoaderFetched(loader){
    return loader.loadPyodide();
})
.setStatic(function onPyodideLoaded(Pyodide){
    Python.Pyodide = Pyodide;
    Python.eval = Pyodide.runPythonAsync.bind(Pyodide);
    Msg.ofStatus("Python ready", "success");
});
Python.onFail = Kits.onLoadFailed.bind(null, "pyodide");
Python.pyodideUri = "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.mjs";

const R = Language.setChild("r")
.setStatic(function load(uri){
    Msg.ofStatus("Preparing R...", "info");
    return import(uri || R.webrUri)
        .then(R.onModuleFetched)
        .then(R.onWebrLoaded)
        .catch(R.onFail);
})
.setStatic(function onModuleFetched(module){
    R.module = module;
    const webR = new module.WebR();
    return webR.init().then( ()=>webR );
})
.setStatic(function arrayFromRes(res){
    if (res.toArray) return res.toArray().then(arrayFromRes);
    if (!Array.isArray(res)) return Promise.resolve(res);
    return Promise.all(res.map(arrayFromRes));
})
.setStatic(function onWebrLoaded(Webr){
    R.Webr = Webr;
    R.eval = function(code){
        return Webr.evalR(code).then(R.arrayFromRes);
    }
    Msg.ofStatus("R ready", "success");
});
R.onFail = Kits.onLoadFailed.bind(null, "webr");
R.webrUri = "https://webr.r-wasm.org/latest/webr.mjs";

const kits = logseq.kits = Concept.setChild("kits")
kits.evalpage = Kits.addClickEventToButton.bind(null, function onEvalPageClicked(e){
    document.querySelectorAll("div.ls-block div[data-lang]").forEach( (div)=>{
        Kits.evalDiv(div.closest("div.ls-block"));
    });
});
kits.evalparent = Kits.addClickEventToButton.bind(null, function onEvalParentClicked(e){
    const child = e.target.closest("div.ls-block");
    Kits.evalDiv(child.parentElement.closest("div.ls-block"));
});
kits.runpage = Kits.addClickEventToButton.bind(null, function onRunPageClicked(e){
    var pageName = e.target.dataset.pageName || "";
    if (pageName === "current page") {
        const page = logseq.api.get_current_page();
        if (page) pageName = page.name;
    }
    Kits.runPageByName(pageName);
});
kits.togglemsg = Kits.addClickEventToButton.bind(null, function onToggleMsgClicked(e){
    Msg.state = (Msg.state === "on") ? "off" : "on";
    Msg.ofStatus("Messages " + Msg.state, "success");
});

const kitelems = document.getElementsByClassName("kit");
const kitsObserver = new MutationObserver(function onMutated(){
    var nofFound = 0;
    const missing = ["Missing kit(s): "];

    const proms = Array.prototype.map.call(kitelems, (elem)=>{
        const data = elem.dataset;
        const status = data.kitStatus;
        if (status === "handled") return;

        if (data.pageName === "$1") {
            data.pageName = "current page";
            elem.textContent = elem.textContent.replace("page $1", "current page");
        }

        data.kitStatus = "handled";
        const kitName = data.kit;
        return Kits.getKitByName(kitName).then( (handler)=>{
                if (typeof handler !== "function") {
                    missing.push(kitName);
                    return;
                }

                handler(elem);
                nofFound += 1;
            });
    });

    Promise.all(proms).then( ()=>{
        Kits.onObserverFinished(nofFound, missing);
    });
});
kitsObserver.observe(document.getElementById("app-container"), {
    attributes: true,
    subtree: true,
    attributeFilter: ["class"]
});
Msg.ofStatus("kits ok", "success");
  • This sets a single MutationObserver for all your needs.
    • No need to edit this file again.
  • Restart Logseq and accept running custom.js.
    • Before continuing, ensure that you get a success message “kits ok”:

image

  • Put the rest of your code directly in Logseq:
    • Turn any block into code editor with /code block and then javascript (or python, r etc.) Either:
      • put some quick code in a temporary place
      • use one page per module
    • No need to restart again (unless for cleaning the memory).
    • Run your code either:
      • one block at a time
        • Add a child-block.
        • Type {{evalparent}} to get a button for running the parent’s code and showing its result.
          image
          • The results:
            • are not saved anywhere
            • are lost when moving away
            • can be selected and copied
            • can be removed by:
              • click on the X-button (appears on hover) for a single one.
              • Ctrl + click for all the results of a code-block.
              • Ctrl + Shift + click for every result everywhere.
          • If a result is a base-64 string starting with data:image, it is rendered as an image.
      • one page at a time
        • Type {{runpage}} to get a button for running all code in the current page (of the main pane).
          • from top to bottom:
            • ignoring any content outside code-blocks of supported languages
            • auto-awaiting each code-block before moving to the next one
          • Some other page can be specified by its name, e.g. {{runpage mypage}}
        • Type {{evalpage}} to get a button for running all visible code-blocks in parallel and showing their results.
    • Type {{togglemsg}} to get a button for toggling helpful (but tiring) messages.
    • Have your code run when needed from anywhere in Logseq:
      • Inside your code, register the entry function of each module with logseq.kits.mypage = the function.
        • mypage should be the actual name of the module’s page
      • Define your own macros (like the predefined above) by adding to their attributes:
        • class='kit'
        • data-kit='mypage'
      • The system will:
        • run once the page’s code, if needed
        • call the entry function once for each macro, passing it the macro’s element
          • The macro can provide arguments to the element, read from your code with .dataset or .getAttribute

Amazing, this actually works! I made a template with a script that calculates ingredient amounts for sourdough. Now I’ll actually have an incentive to add it to my journal. (Since I never make it twice exactly the same, I do need some equations to tell me how much of what to add.)

2 Likes