This file is special in that it is parsed and any !plugins
it contains are pre-populated for all other .lit
documents in this .lit
notebook.
Table of Contents
Settings
Not yet Implemented the below are mocks while we decide on the interface. Just another kind of !plugin
? data exports could work the same...
GitHub
Set up a GitHub repository to save your updates to. Just run the Cell below. Requires a GitHub Access Token↗.
return lit.config.setupGithubAccess(token.value)
All set up.
{ token: '••••••••••', username: 'dotlitdev', repository: 'dotlit', branch: 'main', prefix: 'src' }
Theme
body {
--bg: white;
--bg-secondary-color: #efefef;
--text-color: black;
--text-secondary-color: grey;
--text-primary-color: #9999f7;
--text-highlight-bg: yellow;
--text-highlight-color: black;
--divider-subtle: #efefef;
--medium-space: 0.4em;
--code-bg-color: black;
--code-text-color: white;
--box-bg-opacity: 0.05;
}
Custom plugins
See plugin system for information on how to create !plugins
and the various types.
const all = lit.file.data.plugins
return Object.keys(all).map(t=>`${t} (${Object.keys(all[t]).length})`)
ℹ️ Move, copy or transclude any document specific !plugin
here to have it apply to all documents.
export const viewer = ({ node, React }) => {
const rce = React.createElement;
const { useState, useEffect } = React;
const [resp, setResp] = useState(null);
const [run, setRun] = useState(0);
const exec = (ev) => {
ev && ev.stopPropagation();
setRun(run + 1);
load(node.data.value);
return false;
};
useEffect((args) => {
if (run === 0 && node.properties.meta.exec === "onload") exec();
}, []);
async function load(src) {
const val = `//run: ${run}\n${src}`;
const module = await import(`data:text/javascript;base64,${btoa(src)}`);
if (typeof module.returns === "function")
setResp(await module.returns(run));
else if (module.returns) setResp(module.returns);
}
const btn = rce("button", { onClick: exec }, "Run " + run);
const t = rce("div", null, [btn, resp]);
return t;
};
if (typeof module !== "undefined") module.exports.viewer = viewer;
Register Service Worker
Prettier js
transformer
Upload file
export const viewer = ({ node, React }) => {
const { useState } = React;
const [uploads, setUploads] = useState([]);
const handleFiles = (ev) => {
const files = ev.target.files;
for (let i = 0; i < files.length; i++) {
const file = files[i];
const reader = new FileReader();
reader.onload = ((i, file) => async (e) => {
const filename = prompt(
"Enter a file name and path:",
`/testing/uploads/${file.name}`
);
if (!filename || !filename.trim()) return;
const data = e.target.result;
try {
await lit.fs.writeFile(filename, data);
} catch (err) {
alert(err.message);
}
setUploads([...uploads, [filename, data]]);
})(i, file);
// reader.readAsDataURL(file);
reader.readAsBinaryString(file);
}
};
return (
<div>
<input onChange={handleFiles} type="file" />
<div>
{uploads.map((upload) => (
<span>{upload[0]}/</span>
))}
</div>
</div>
);
};
Detach/delete attached output
export const cellmenu = (ctx, { React, Menu }) => {
const sel = lit.utils.unist.selectPosition.selectAll;
const pos = ctx.selectedCell;
const tree = lit.file.data.ast;
const [attached] = pos
? sel("code,mdblock", pos, tree).filter(
(code) => code?.data?.meta?.attrs?.attached
)
: [];
const detach = (ctx) => (del) => (ev) => {
if (del) ctx.setSrc(attached.position, "");
else {
const { toMarkdown, ungroupSections } = lit.parser.utils;
attached.meta = attached.meta.replace(/\s?attached=true/, "");
const root = { type: "root", children: [attached] };
const md = toMarkdown(root);
ctx.setSrc(attached.position, md);
}
};
return (
<Menu disabled={!attached} title="Output">
<span disabled={!ctx.selectedCell} onClick={detach(ctx)()}>
Detach Output
</span>
<span disabled={!ctx.selectedCell} onClick={detach(ctx)(true)}>
Delete Output
</span>
</Menu>
);
};
Sections depths
export const sectionmenu = (ctx, { React, Menu }) => {
const rc = React.createElement;
const { visit } = lit.utils.unist;
const update = () => ctx.setSrc(lit.ast.position, ctx.ast2md(lit.ast));
const withPos = (tree, pos, visitor) =>
visit(
lit.ast,
() => true,
(node) => {
if (node?.position?.start?.offset === pos.start?.offset) {
console.log("found match: ", node.type);
visitor(node);
return visit.SKIP;
}
}
);
const push = () =>
withPos(lit.ast, ctx.selectedCell, (node) => {
visit(node, "heading", (node) => (node.depth += 1));
update();
});
const pull = () =>
withPos(lit.ast, ctx.selectedCell, (node) => {
visit(node, "heading", (node) => (node.depth -= 1));
update();
});
const Push = rc("span", { onClick: push }, "Push");
const Pull = rc("span", { onClick: pull }, "Pull");
return rc(
Menu,
{
title: "Depth",
disabled: !ctx.selectedCell,
},
[Push, Pull]
);
};
json2
viewer
Implementation:
// see https://github.com/mac-s-g/react-json-view
import reactJsonView from 'https://cdn.skypack.dev/react-json-view'
export const viewer = ({node, React}) => {
const rc = React.createElement
let obj
try { obj = JSON.parse(node.data.value)
} catch(err) {}
return rc( 'div', {className: 'json-viewer', onClick:ev=>{ev.preventDefault(); ev.stopPropagation(); return false;}}, rc(reactJsonView, {
src: obj,
collapseStringsAfterLength: 20,
collapsed: node.properties.meta.collapse || 1,
}))
}
Example usage:
import reactJsonTree from 'https://cdn.skypack.dev/react-json-tree'
export const viewer = ({node, React}) => {
const rc = React.createElement
console.log('reactJsonTree', reactJsonTree)
let obj
try { obj = JSON.parse(node.data.value)
} catch(err) {}
return rc( 'div', {className: 'json-viewer',xonClick:ev=>{ev.preventDefault(); ev.stopPropagation(); return false;}}, rc(reactJsonTree, {
data: { "test" : "foo" },
}))
}
CORS Proxy
Cors proxy see testing/Cors proxy
export const proxy = async (returnEndpoint) => {
const setup = (resolve, reject) => {
if (typeof lit === "undefined") reject("No lit");
else if (!window.__runkitCORSProxyEnpoint) {
(async (fn) => {
const rkEmbed = document.createElement("script");
rkEmbed.onload = async (fn) => {
const el = document.createElement("div");
document.body.appendChild(el);
el.setAttribute("style", "height:0;");
RunKit.createNotebook({
element: el,
mode: "endpoint",
onLoad: async (rk) => {
const endpoint = await rk.getEndpointURL();
window.__runkitCORSProxyEnpoint = endpoint;
document.body.removeChild(el);
},
evaluateOnLoad: true,
source: await lit.fs.readFile(
"/testing/runkit-express-cors-proxy.js",
{
encoding: "utf8",
}
),
});
};
rkEmbed.setAttribute("src", "https://embed.runkit.com");
document.body.appendChild(rkEmbed);
})();
} else {
resolve(window.__runkitCORSProxyEnpoint);
}
};
const endpoint = await new Promise(setup).then((e) => e);
if (false && !window.__runkitCORSProxyEnpoint) {
return "Still setting up proxy endpoint";
} else {
if (returnEndpoint) return endpoint;
const getAndReplaceDomain = (originalUrl, newDomain) => {
return newDomain + originalUrl.replace(/^https?:\/\//, "/");
};
const proxyFetch = async (url, opts = {}) => {
const proxyUrl = getAndReplaceDomain(url, endpoint);
return fetch(proxyUrl, opts);
};
return proxyFetch;
}
};
Plantuml
viewer & repl
Uses plantuml.com↗ to create svg
images from uml
source. Not included as a default viewer due to the external dependency, but it's great! See plugins/viewers/plantuml/plantuml for more.
Implementation:
async function encodePlantUML(src) {
console.log("encoding", src);
const module = await import("https://cdn.skypack.dev/plantuml-encoder");
const encoded = module.encode(src); //.replace(/\n/g, '\\n'))
console.log(encoded);
return encoded;
}
const getEndpoint = (format, encoded) =>
`https://plantuml.com/plantuml/${format}/` + encoded;
export const repl = async (src, meta, node) => {
const format = (meta && meta.format) || "svg";
try {
const encoded = await encodePlantUML(src);
const url = getEndpoint(format, encoded);
const proxy = lit?.file?.data?.plugins?.proxy?.corsProxy;
const fetchApi = proxy ? await proxy() : fetch;
const resp = await fetchApi(url);
return await resp.text();
} catch (err) {
return "Error: " + err.message;
}
};
export const viewer = ({ node, React }) => {
const rc = React.createElement;
const { useState, useEffect } = React;
const meta = node.properties && node.properties.meta;
const format = (meta && meta.format) || "svg";
const [url, setUrl] = useState(null);
const src = node.value;
useEffect(async () => {
const encoded = await encodePlantUML(node.data.value);
const url = getEndpoint(format, encoded);
setUrl(url);
}, [src]);
return rc(
"div",
{
className: "lit-viewer-plantuml2",
},
url
? rc("img", {
src: url,
})
: "Loading..."
);
};
Example usage:
@startmindmap
* .lit
* Digital Gardens
* Tools for Thought
* Learning in Public
* Thinking in Public
* Knowledge Graph
* Literate Programming
* Read Eval Print Loop
* Runbooks
* Interactive Notebooks
* Guided Learning
* Show don't Tell
* No Code
* Low Code
* More Code
* Rapid Prototyping
@endmindmap
@startmindmap
* root node
* some first level node
* second level node
* another second level node
* another first level node
@endmindmap
Urgh, CORS...
So to bypass CORS we need a proxy, see testing/runkit for an example nodejs repl, which could achieve this proxying.
Fixed see testing/Cors Proxy for example.
Search
const sortBy = (keys) => (a, b) => {
for (const key of keys) {
if (a[key] !== b[key]) break;
else return a[key] > b[key] ? 1 : -1;
}
};
const abs = (url) => /^https?:\/\//.test(url)
const prefix = (url) => abs(url) ? url : "/" + url
const itemBuilder = (React) => (item) => {
const rc = React.createElement;
return rc(
"li",
{ className: "item" },
rc(
"a",
{ className: item.exists ? "local exists": "local", href: prefix(lit.parser.utils.links.decorateLinkNode({url: item.id }).url) },
item.title || item.id
)
);
};
export const viewer = ({ node, React }) => {
const rc = React.createElement;
const { useState, useEffect } = React;
const meta = node.properties && node.properties.meta;
const [src, setSrc] = useState(meta.search || node.data.value.trim());
const [content, setContent] = useState("");
const item = itemBuilder(React);
useEffect(async () => {
if (!src) return;
const json = lit.manifest;
let regex;
try {
regex = new RegExp(src, "i");
} catch (err) {}
const res = json.nodes
.map((x) => x)
.filter((x) => {
return (
x.id.indexOf(src) >= 0 ||
(regex && regex.test(x.id)) ||
(x.title &&
(x.title.indexOf(src) >= 0 || (regex && regex.test(x.title))))
);
})
.sort()
.map((x) => item(x));
//.join("\n")
setContent(rc("ol", null, res));
}, [src]);
return rc(
"div",
{
className: "custom-react-view",
},
[
rc("input", {
style: { width: "100%", fontSize: "1.2em" },
value: src,
onChange: (e) => setSrc(e.target.value),
}),
content,
]
);
};
Miscellaneous
import diff from "https://cdn.skypack.dev/react-diff-viewer";
export const filemenu = (ctx, { React, Menu, toggleModal }) => {
const rc = React.createElement;
const showDiff = async () => {
const close = rc("button", { onClick: () => toggleModal() }, ["Close"]);
const styles = {
lineNumber: {
fontWeight: "bold",
},
gutter: {
minWidth: "0.6em",
padding: "0 0.2em",
},
marker: {
width: "auto",
padding: "0 0.2em",
},
contentText: {
lineHeight: "1em !important",
wordBreak: "break-all",
},
content: {
width: "auto",
padding: 0,
},
};
const stats = await lit.fs.readStat(lit.location.src, { encoding: "utf8" });
if (stats.local.value === stats.remote.value) {
toggleModal(rc("div", {}, [close, "Local and Remote match. (No Diff)"]));
return;
}
const view = rc(diff, {
newValue: stats.local.value,
rightTitle: "local",
oldValue: stats.remote.value,
leftTitle: "remote",
splitView: false,
styles,
compareMethod: diff["CHARS"],
});
const modal = rc("div", {}, [close, view]);
toggleModal(modal);
};
return rc("span", { onClick: showDiff }, "Show Diff");
};