- 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”:
- 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.
- 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-
await
ing 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