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.)

3 Likes

Inspired by mentaloid’s work I have created a graph and video that demonstrate how to execute code within logseq. It includes many python examples such as Jupyter like behaviour, accessing pages and blocks and sending code execution results to logseq pages, the console, javascript alerts and logseq messages.

Video url: https://youtu.be/u1hi7HjG66A
Github repo where you can download the graph:

GitHub - adxsoft/logseq-code-execution-demo-graph: A logseq graph that demonstrates how to use python and/or javascript code within a logseq graph giving Jupyter like behaviours - includes using logseq API calls to get pages and blocks and run queries

Hope you find this useful

7 Likes

in Macros examples you use logseq.kits.setStatic(function mypage(x) { } ). Is it the same as logseq.kits.mypage = ...?

  • The system will:
    • run once the page’s code, if needed

Also, if I change code of mypage function on mypage page, how to make loqseq to update it?

Welcome.

They are 99% the same. setStatic facilitates chaining.

  • To update:
    • Use a macro to render one of the provided buttons:
      • {{evalpage}}
      • {{evalparent}}
      • {{runpage}}
        • or {{runpage mypage}}
    • Click the button each time you are ready.
  • For each button’s:
    • description: read the respective bullet in the OP
    • details of what it does: read the code of its kit
      • those buttons are kits themselves
1 Like

Bug: JavaScript code blocks will always result in undefined if the block has any properties e.g. it has the id property from making it into a block reference.

1 Like

Thank you for reporting. Try filtering properties out by updating this line:

const content = block.content;

…like this:

const content = block.content.replace(/\n?.*[:][:].*\n?/g, "\n").trim()
1 Like

Works great, thanks!

Bug: In JavaScript a return statement is required on the top-level otherwise it’ll output as undefined.

Outputs undefined:

function test1() {
    return 'hello world';
};

test1();

Outputs hello world, but I don’t think this is correct JS in any other environment:

function test1() {
    return 'hello world';
};

return test1();

This code is not top-level. Before running, every code-block gets wrapped within an asynchronous function (thus needing return), to allow for smooth evaluation of multiple macros at the same time. This is by design.

Ah okay, I understand now.

Bug: In JavaScript objects get printed as [object Object] rather than their actual value.

However this is likely a limitation of the results output being limited to one line - a workaround is that instead of returning the object directly you can stringify it instead e.g. return JSON.stringify(myObject);

  • This is not meant to act as a console.
    • The real console is just a Ctrl + Shift + i away.
      • To print in the console, call the usual methods of console
  • Here the output expects a DOM element (or a string).
    • There is no unified HTML representation for arbitrary objects.
      • As about JSON, it:
        • is just one of many possible string-only representations
          • limited to a single line
        • doesn’t support many datatypes
    • To get some printing for a returned object, could define its toString method.
      • Could even define it as JSON.stringify
      • For multi-line printing, use character \n
    • For serious output, prefer creating and returning actual DOM elements, e.g.
      const btn = document.createElement("button");
      btn.textContent = "click";
      btn.onclick = ()=>logseq.api.show_msg("clicked")
      return btn
      
1 Like