Initial commit
This commit is contained in:
6
Library/LogeshG5/excalidraw.plug.js
Normal file
6
Library/LogeshG5/excalidraw.plug.js
Normal file
File diff suppressed because one or more lines are too long
30
Library/LogeshG5/silverbullet-excalidraw.md
Normal file
30
Library/LogeshG5/silverbullet-excalidraw.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: Library/LogeshG5/silverbullet-excalidraw
|
||||
tags: meta/library
|
||||
files:
|
||||
- excalidraw.plug.js
|
||||
share.uri: "https://github.com/LogeshG5/silverbullet-excalidraw/blob/main/PLUG.md"
|
||||
share.hash: 199c6406
|
||||
share.mode: pull
|
||||
---
|
||||
# SilverBullet plug for Excalidraw diagrams
|
||||
|
||||
This plug adds [Excalidraw](https://excalidraw.com/) support to Silverbullet.
|
||||
|
||||
### Create New Diagram
|
||||
|
||||
Run `Excalidraw: Create diagram` command and type in the name of the diagram. (or) Use `/excalidraw` slash command.
|
||||
|
||||
A code widget will be inserted. Move away from the code widget and you will see the diagram.
|
||||
|
||||
If you wish to create SVG/PNG files, Run `Excalidraw: Create SVG/PNG diagram`.
|
||||
|
||||
### Use Document Picker
|
||||
|
||||
Run `Navigate: Document Picker` and select .excalidraw files to open the editor.
|
||||
|
||||
### Edit SVG/PNG Diagram
|
||||
|
||||
Run `Excalidraw: Edit diagram`.
|
||||
|
||||
If multiple diagrams are present in a page, you will be prompted to choose one.
|
||||
111
Library/Malys/CursorPosition.md
Normal file
111
Library/Malys/CursorPosition.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
author: malys
|
||||
description: Generate and copy links to specific cursor positions and headers in markdown documents
|
||||
name: "Library/Malys/CursorPosition"
|
||||
tags: meta/library
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/CursorPosition.md"
|
||||
share.hash: a8544328
|
||||
share.mode: pull
|
||||
---
|
||||
# Cursor Position Link Generator
|
||||
|
||||
This script provides functionality to generate and copy links to specific cursor positions and headers within markdown documents. It's particularly useful for creating precise references within a document or across multiple documents.
|
||||
|
||||
## Features
|
||||
|
||||
- **External Links**: Generate full URLs pointing to specific cursor positions or headers
|
||||
- **Internal Links**: Create markdown-style internal links for use within the same document
|
||||
|
||||
## Usage
|
||||
|
||||
### External Links (Ctrl+Shift+C)
|
||||
Copies a full URL to the current cursor position or header to the clipboard.
|
||||
- If the cursor is on a header line: Creates a URL with the header as an anchor (e.g., `https://your-host/Page#Header Name`)
|
||||
- If not on a header: Creates a URL with the cursor position (e.g., `https://your-host/Page@123`)
|
||||
|
||||
### Internal Links (Ctrl+Shift+L)
|
||||
Copies a markdown-style internal link to the current cursor position or header.
|
||||
- If on a header: Creates a link like `[Header Name@123]`
|
||||
- If not on a header: Creates a link like `[Page Name@123]`
|
||||
---
|
||||
## Source
|
||||
```space-lua
|
||||
-- [[Page#Header]] -> http(s)://host/Page#Header
|
||||
-- [[Page@pos]] -> http(s)://host/Page@pos
|
||||
|
||||
local function replace_space_with_percent20(s)
|
||||
local parts = {}
|
||||
for i = 1, #s do
|
||||
local c = s:sub(i, i)
|
||||
if c == " " then
|
||||
parts[#parts+1] = "%20"
|
||||
else
|
||||
parts[#parts+1] = c
|
||||
end
|
||||
end
|
||||
return table.concat(parts)
|
||||
end
|
||||
|
||||
local function build_page_url(pageName)
|
||||
-- get direct url from js
|
||||
local BASE_URL = js.window.location.origin
|
||||
local path = replace_space_with_percent20(pageName)
|
||||
if BASE_URL:sub(-1) == "/" then
|
||||
return BASE_URL .. path
|
||||
else
|
||||
return BASE_URL .. "/" .. path
|
||||
end
|
||||
end
|
||||
|
||||
command.define {
|
||||
name = "Cursor: Copy external link",
|
||||
key = "Ctrl-Shift-c",
|
||||
run = function()
|
||||
local currentLine = editor.getCurrentLine().text
|
||||
local pageName = editor.getCurrentPage()
|
||||
local pos = editor.getCursor()
|
||||
local headerMarks, headerName = string.match(currentLine, "^(#+) +(.+)$")
|
||||
|
||||
local pageUrl = build_page_url(pageName)
|
||||
local out
|
||||
if headerMarks and headerName and headerName:match("%S") then
|
||||
headerName = headerName:match("^%s*(.+)")
|
||||
headerName = replace_space_with_percent20(headerName)
|
||||
out = string.format("%s#%s", pageUrl, headerName)
|
||||
-- editor.flashNotification("Copied header external link: " .. out, "info")
|
||||
editor.flashNotification("Copied header link: " .. out, "info")
|
||||
else
|
||||
out = string.format("%s@%d", pageUrl, pos)
|
||||
-- editor.flashNotification("Copied cursor external link: " .. out, "info")
|
||||
editor.flashNotification("Copied cursor link: " .. out, "info")
|
||||
end
|
||||
|
||||
editor.copyToClipboard(out)
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
command.define {
|
||||
name = "Cursor: Copy internal link",
|
||||
key = "Ctrl-Shift-l",
|
||||
run = function()
|
||||
local currentLine = editor.getCurrentLine().text
|
||||
local pageName = editor.getCurrentPage()
|
||||
local pos = editor.getCursor()
|
||||
local headerMarks, headerName = string.match(currentLine, "^(#+) +(.+)$")
|
||||
|
||||
local out
|
||||
if headerMarks and headerName and headerName:match("%S") then
|
||||
headerName = headerName:match("^%s*(.+)")
|
||||
out = string.format("[[%s@%s]]", replace_space_with_percent20(headerName), pos)
|
||||
editor.flashNotification("Copied header internal link: " .. out, "info")
|
||||
else
|
||||
out = string.format("[[%s@%d]]", replace_space_with_percent20(pageName), pos)
|
||||
editor.flashNotification("Copied cursor internal link: " .. out, "info")
|
||||
end
|
||||
|
||||
editor.copyToClipboard(out)
|
||||
end
|
||||
}
|
||||
|
||||
```
|
||||
361
Library/Malys/MarkmapMindmap.md
Normal file
361
Library/Malys/MarkmapMindmap.md
Normal file
@@ -0,0 +1,361 @@
|
||||
---
|
||||
author: malys
|
||||
description: MarkMap mindmap integration.
|
||||
name: "Library/Malys/MarkmapMindmap"
|
||||
tags: meta/library
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/MarkmapMindmap.md"
|
||||
share.hash: 65e1843c
|
||||
share.mode: pull
|
||||
---
|
||||
# Markmap mindmap
|
||||
|
||||
This library provides a way to preview your [MarkMap](https://markmap.js.org/) mind map in a panel while you are editing your space.
|
||||
|
||||
With MarkMap Preview, you can:
|
||||
- Preview your slides without leaving the context of your space
|
||||
- See how your slides look in real-time as you modify your markdown
|
||||
- Use the MarkMap Preview panel to navigate through your slides and see them in action
|
||||

|
||||
|
||||
## Disclaimer & Contributions
|
||||
|
||||
This code is provided **as-is**, **without any kind of support or warranty**.
|
||||
I do **not** provide user support, bug-fixing on demand, or feature development.
|
||||
|
||||
If you detect a bug, please **actively participate in debugging it** (analysis, proposed fix, or pull request) **before reporting it**. Bug reports without investigation may be ignored.
|
||||
|
||||
🚫 **No new features will be added.**
|
||||
✅ **All types of contributions are welcome**, including bug fixes, refactoring, documentation improvements, and optimizations.
|
||||
|
||||
By using or contributing to this project, you acknowledge and accept these conditions.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
To easily manage JS code source, we use a dynamic introspection mechanism based on md page path.
|
||||
If you don’t install program to default path `Library/Malys/xxx`,we have to set:
|
||||
|
||||
```lua
|
||||
config.set("markmap.source ","xxxx")
|
||||
-- ex: config.set("markmap.source ","Library/Malys/MarkmapMindmap")
|
||||
-- where xxx is the path of MarkmapMindmap md page
|
||||
```
|
||||
|
||||
> **warning** Caution
|
||||
> **Depends on** [Utilities.md](https://github.com/malys/silverbullet-libraries/blob/main/src/Utilities.md). It will be installed automatically.
|
||||
|
||||
|
||||
## Code
|
||||
|
||||
```space-lua
|
||||
local source=config.get("markmap.source") or "Library/Malys/MarkmapMindmap"
|
||||
local panelSize=config.get("markmap.panelSize") or 4
|
||||
|
||||
if library~=nil and (mls == nil or (mls ~=nil and mls.debug == nil)) then
|
||||
library.install("https://github.com/malys/silverbullet-libraries/blob/main/src/Utilities.md")
|
||||
editor.flashNotification("'Utilities' has been installed", "Info")
|
||||
end
|
||||
|
||||
local function isVisible()
|
||||
local current_panel_id = config.get("markmap.panelPosition") or "rhs"
|
||||
local iframe = js.window.document.querySelector(".sb-panel."..current_panel_id..".is-expanded iframe")
|
||||
if iframe and iframe.contentDocument then
|
||||
local el = iframe.contentDocument.getElementById("mindmap")
|
||||
if el then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function show()
|
||||
local page_content = editor.getText()
|
||||
local contentBase64=encoding.base64Encode(page_content)
|
||||
local content1= mls.getCodeBlock(source,"innerHTML","@CONTENT@", contentBase64)
|
||||
local js = mls.getCodeBlock(source,"jsInject","@CONTENT@",content1)
|
||||
local panel_html= mls.getCodeBlock(source,"template")
|
||||
local current_panel_id = config.get("markmap.panelPosition") or "rhs"
|
||||
editor.showPanel(current_panel_id,panelSize, panel_html, js)
|
||||
end
|
||||
|
||||
local function hide()
|
||||
local current_panel_id = config.get("markmap.panelPosition") or "rhs"
|
||||
editor.hidePanel(current_panel_id)
|
||||
end
|
||||
|
||||
local update = function(mode)
|
||||
if ((not isVisible() and mode) or (not mode and isVisible())) then
|
||||
show()
|
||||
else
|
||||
hide()
|
||||
end
|
||||
end
|
||||
|
||||
-- Define the command
|
||||
command.define({
|
||||
name = "Markmap: Toggle preview",
|
||||
description = "Toggle Marp slides render in a panel",
|
||||
run = function(e)
|
||||
update(true)
|
||||
end
|
||||
})
|
||||
event.listen({
|
||||
name = "editor:pageSaved",
|
||||
run = function(e)
|
||||
update(false)
|
||||
end
|
||||
})
|
||||
```
|
||||
|
||||
## JS templates
|
||||
```js jsInject
|
||||
const scriptId = "markmap-inject";
|
||||
|
||||
// Remove existing script if present
|
||||
const existingScript = document.getElementById(scriptId);
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// Create and inject the script again
|
||||
const script = document.createElement("script");
|
||||
script.type = "module";
|
||||
script.id = scriptId;
|
||||
script.textContent = `@CONTENT@`;
|
||||
document.documentElement.appendChild(script);
|
||||
```
|
||||
```js innerHTML
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(
|
||||
atob(str)
|
||||
.split("")
|
||||
.map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
|
||||
import { Transformer } from "https://esm.sh/markmap-lib?bundle";
|
||||
import { Markmap, deriveOptions } from "https://esm.sh/markmap-view?bundle";
|
||||
import { Toolbar } from "https://esm.sh/markmap-toolbar?bundle";
|
||||
|
||||
const transformer = new Transformer();
|
||||
const { root } = transformer.transform(b64DecodeUnicode("@CONTENT@"));
|
||||
|
||||
const svg = document.getElementById("mindmap");
|
||||
const base = deriveOptions(null) || {};
|
||||
const options = {
|
||||
...base,
|
||||
duration: 0,
|
||||
autoFit: false
|
||||
};
|
||||
svg.innerHTML = "";
|
||||
window.mm = Markmap.create(
|
||||
"svg#mindmap",
|
||||
options,
|
||||
root
|
||||
);
|
||||
|
||||
const existing = document.getElementsByClassName("mm-toolbar");
|
||||
if(existing.length==0){
|
||||
|
||||
// 👉 Toolbar
|
||||
const toolbar = new Toolbar();
|
||||
toolbar.attach(window.mm);
|
||||
|
||||
const el = toolbar.render();
|
||||
el.style.position = "absolute";
|
||||
el.style.bottom = "20px";
|
||||
el.style.right = "20px";
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
|
||||
// 👉 print
|
||||
window.addEventListener("beforeprint", () => {
|
||||
window.mm?.fit();
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## HTML template
|
||||
|
||||
```html template
|
||||
<style>
|
||||
html,
|
||||
html {
|
||||
overflow-y: scroll !important;
|
||||
width: 90% !important;
|
||||
}
|
||||
@media print {
|
||||
.no-print,
|
||||
.mm-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
.markmap-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
.markmap-dark {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
* {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
/* Remove default page margins */
|
||||
@page {
|
||||
margin: 0;
|
||||
}
|
||||
/* 2️⃣ Reset layout sizing */
|
||||
html,
|
||||
body {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
/* Hide all toolbars */
|
||||
.no-print,
|
||||
.markmap-toolbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
}
|
||||
body {
|
||||
overflow: initial !important;
|
||||
color: var(--top-color);
|
||||
font-family: georgia, times, serif;
|
||||
font-size: 13pt;
|
||||
margin-top: revert;
|
||||
margin-bottom: revert;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
ul li p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
background-color: var(--editor-table-head-background-color);
|
||||
color: var(--editor-table-head-color);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
tbody tr:nth-of-type(even) {
|
||||
background-color: var(--editor-table-even-background-color);
|
||||
}
|
||||
|
||||
a[href] {
|
||||
text-decoration: none;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 1px solid var(--editor-blockquote-border-color);
|
||||
margin-left: 2px;
|
||||
padding-left: 10px;
|
||||
background-color: var(--editor-blockquote-background-color);
|
||||
color: var(--editor-blockquote-color);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1em 0 1em 0;
|
||||
text-align: center;
|
||||
border-color: var(--editor-ruler-color);
|
||||
border-width: 0;
|
||||
border-style: dotted;
|
||||
}
|
||||
|
||||
hr:after {
|
||||
content: "···";
|
||||
letter-spacing: 1em;
|
||||
}
|
||||
|
||||
span.highlight {
|
||||
background-color: var(--highlight-color);
|
||||
}
|
||||
|
||||
.markmap {
|
||||
--markmap-a-color: #0097e6;
|
||||
--markmap-a-hover-color: #00a8ff;
|
||||
--markmap-highlight-bg: #ffeaa7;
|
||||
|
||||
--markmap-code-bg: #fff;
|
||||
--markmap-code-color: #555;
|
||||
--markmap-circle-open-bg: #fff;
|
||||
--markmap-text-color: #333;
|
||||
}
|
||||
|
||||
html[data-theme=dark] {
|
||||
.markmap {
|
||||
--markmap-code-bg: #1a1b26;
|
||||
--markmap-code-color: #ddd;
|
||||
--markmap-circle-open-bg: #444;
|
||||
--markmap-text-color: #dddbdb;
|
||||
}
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#mindmap {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.mm-toolbar {display:flex;-webkit-user-select:none;-moz-user-select:none;user-select:none;align-items:center;border-width:1px;--un-border-opacity:1;border-color:rgb(212 212 216 / var(--un-border-opacity));border-radius:0.25rem;border-style:solid;--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity));padding:0.25rem;line-height:1; }
|
||||
.mm-toolbar:hover {--un-border-opacity:1;border-color:rgb(161 161 170 / var(--un-border-opacity)); }
|
||||
.mm-toolbar svg {display:block; }
|
||||
.mm-toolbar a {display:inline-block;text-decoration:none; }
|
||||
.mm-toolbar-brand > img {width:1rem;height:1rem;vertical-align:middle; }
|
||||
.mm-toolbar-brand > span {padding-left:0.25rem;padding-right:0.25rem; }
|
||||
.mm-toolbar-brand:not(:first-child), .mm-toolbar-item:not(:first-child) {margin-left:0.25rem; }
|
||||
.mm-toolbar-brand > *, .mm-toolbar-item > * {min-width:1rem;cursor:pointer;text-align:center;font-size:0.75rem;line-height:1rem;--un-text-opacity:1;color:rgb(161 161 170 / var(--un-text-opacity)); }
|
||||
.mm-toolbar-brand.active,
|
||||
.mm-toolbar-brand:hover,
|
||||
.mm-toolbar-item.active,
|
||||
.mm-toolbar-item:hover {border-radius:0.25rem;--un-bg-opacity:1;background-color:rgb(228 228 231 / var(--un-bg-opacity)); }
|
||||
.mm-toolbar-brand.active > *, .mm-toolbar-brand:hover > *, .mm-toolbar-item.active > *, .mm-toolbar-item:hover > * {--un-text-opacity:1;color:rgb(39 39 42 / var(--un-text-opacity)); }
|
||||
.mm-toolbar-brand.active, .mm-toolbar-item.active {--un-bg-opacity:1;background-color:rgb(212 212 216 / var(--un-bg-opacity)); }
|
||||
|
||||
.markmap-dark .mm-toolbar {--un-border-opacity:1;border-color:rgb(82 82 91 / var(--un-border-opacity));--un-bg-opacity:1;background-color:rgb(39 39 42 / var(--un-bg-opacity));--un-text-opacity:1;color:rgb(161 161 170 / var(--un-text-opacity)); }
|
||||
|
||||
.markmap-dark .mm-toolbar:hover {--un-border-opacity:1;border-color:rgb(113 113 122 / var(--un-border-opacity)); }
|
||||
|
||||
.markmap-dark .mm-toolbar > *:hover {--un-bg-opacity:1;background-color:rgb(82 82 91 / var(--un-bg-opacity)); }
|
||||
|
||||
.markmap-dark .mm-toolbar > *:hover > * {--un-text-opacity:1;color:rgb(228 228 231 / var(--un-text-opacity)); }
|
||||
</style>
|
||||
<div id="root" class="sb-preview">
|
||||
<div class="sb-mindmap-toolbar no-print">
|
||||
<button onclick="window.print()" title="Print">
|
||||
🖨️
|
||||
</button>
|
||||
</div>
|
||||
<svg id="mindmap"></svg>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
* 2026-01-13: fix: multiple SVG visible on refresh
|
||||
* 2025-12-01 fix: html duplicate inserts
|
||||
|
||||
## Community
|
||||
|
||||
[Silverbullet forum]([https://community.silverbullet.md/t/space-lua-addon-with-missing-git-commands-history-diff-restore/3539](https://community.silverbullet.md/t/mindmap-with-markmap-js/1556))
|
||||
|
||||
930
Library/Malys/MdTableRender.md
Normal file
930
Library/Malys/MdTableRender.md
Normal file
@@ -0,0 +1,930 @@
|
||||
---
|
||||
author: malys
|
||||
description: Automatically formats Markdown table cells based on hashtag column tags.
|
||||
tags: userscript
|
||||
name: "Library/Malys/MdTableRender"
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/MdTableRender.md"
|
||||
share.hash: da8e2c70
|
||||
share.mode: pull
|
||||
---
|
||||
# Md table column rendering
|
||||
|
||||
This script enhances Markdown tables inside SilverBullet by applying dynamic
|
||||
formatting rules to columns marked with hashtag-style format tags (e.g. `#euro`,
|
||||
`#percent`, `#stars`). It observes table changes in real time and transforms raw
|
||||
text values into styled, formatted elements — such as currency, percentages,
|
||||
booleans, dates, badges, emojis, trends, and star ratings — without altering the
|
||||
original Markdown source. It is designed to be non-intrusive, editable-friendly,
|
||||
and resilient thanks to mutation observers, debouncing, and a polling fallback.
|
||||
|
||||
## Disclaimer & Contributions
|
||||
|
||||
This code is provided **as-is**, **without any kind of support or warranty**.
|
||||
I do **not** provide user support, bug-fixing on demand, or feature development.
|
||||
|
||||
If you detect a bug, please **actively participate in debugging it** (analysis,
|
||||
proposed fix, or pull request) **before reporting it**. Bug reports without
|
||||
investigation may be ignored.
|
||||
|
||||
🚫 **No new features will be added.**
|
||||
✅ **All types of contributions are welcome**, including bug fixes, refactoring,
|
||||
documentation improvements, and optimizations.
|
||||
|
||||
By using or contributing to this project, you acknowledge and accept these
|
||||
conditions.
|
||||
|
||||
## Supported renderers (via `#tag` in header)
|
||||
|
||||
| Tag | Effect |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
| **#euro** | Formats number as “12 345 €” |
|
||||
| **#usd** | Formats number as “$12,345” |
|
||||
| **#percent** | Converts decimal to percentage (0.15 → “15 %”) |
|
||||
| **#gauge** | Graphical percentage representation ███░ |
|
||||
| **#posneg** | Colored gauge -2 🟥🟥,0 ⬜, +1 🟩 |
|
||||
| **#km** | Formats number as “12 345 km” |
|
||||
| **#kg** | Formats number as “12 345 kg” |
|
||||
| **#watt** | Formats number as “12 345 W” |
|
||||
| **#int** | Parses and formats whole numbers with locale separators |
|
||||
| **#float** | Forces 2 decimal places (e.g. “3.14”) |
|
||||
| **#upper** | Forces uppercase |
|
||||
| **#lower** | Forces lowercase |
|
||||
| **#bold** | Wraps value in `<strong>` |
|
||||
| **#italic** | Wraps value in `<em>` |
|
||||
| **#link** | Turns URL into clickable link |
|
||||
| **#date** | Formats dates (YYYY-MM-DD or ISO) |
|
||||
| **#datetime** | Formats full timestamp |
|
||||
| **#logical** | Converts truthy → `✅` / falsy → `❌` |
|
||||
| **#stars** | Converts number to up to 10 ⭐ stars |
|
||||
| **#evaluation** | Converts 0–5 into ★/☆ rating |
|
||||
| **#badge** | Renders value as a blue pill badge |
|
||||
| **#emoji** | Converts words like “happy”, “cool”, “neutral” → 😃 😎 😐 |
|
||||
| **#mood** | Converts evaluation of mood to emoj 1:bad 5: very well |
|
||||
| **#trend** | Converts + / - / = into 🔼 🔽 ➡️ |
|
||||
| **#histo** | Converts number to █ |
|
||||
|
||||
Just add the renderer as a hashtag tag in your table header:
|
||||
|
||||
```md
|
||||
| Product #wine | Euro #euro | Percent #percent | Logical #logical | Stars #stars | Evaluation #evaluation | Updated | Mood #emoji | Trend #trend |
|
||||
| ------------- | ---------- | ---------------- | ---------------- | ------------ | ---------------------- | -------------------- | ----------- | ------------ |
|
||||
| Widget | 12.99 | 0.15 | 0 | 3 | 4 | 2025-11-06T14:30:00Z | happy | + |
|
||||
| Gadget | 8.50 | 0.23 | false | 5 | 2 | 2024-12-25T10:00:00Z | neutral | - |
|
||||
| Thingamajig | 5.75 | 0.05 | true | 4 | 5 | 2023-05-10T08:15:00Z | cool | = |
|
||||
```
|
||||
|
||||

|
||||

|
||||
## How to
|
||||
|
||||
### Add new renderer
|
||||
|
||||
```lua
|
||||
mls = mls or {}
|
||||
mls.table = mls.table or {}
|
||||
mls.table.renderer = mls.table.renderer or {}
|
||||
|
||||
mls.table.renderer["euro"] = {
|
||||
completion = {
|
||||
{
|
||||
name = "one",
|
||||
value = "1"
|
||||
},
|
||||
{
|
||||
name = "two",
|
||||
value = "2"
|
||||
},
|
||||
},
|
||||
visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} TEST`]],
|
||||
validation = function(v){
|
||||
--...
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
## Code
|
||||
|
||||
```space-lua
|
||||
|
||||
|
||||
-- Table Renderer (Formatter)
|
||||
mls = mls or {}
|
||||
mls.table = mls.table or {}
|
||||
mls.table.renderer = mls.table.renderer or {}
|
||||
|
||||
local cfg = config.get("tableRenderer") or {}
|
||||
local enabled = cfg.enabled ~= false
|
||||
|
||||
--------------------------------------------------
|
||||
-- FUNCTION
|
||||
--------------------------------------------------
|
||||
function mls.table.cleanupRenderer()
|
||||
local scriptId = "sb-table-renderer-runtime"
|
||||
local existing = js.window.document.getElementById(scriptId)
|
||||
if existing then
|
||||
local ev = js.window.document.createEvent("Event")
|
||||
ev.initEvent("sb-table-renderer-unload", true, true)
|
||||
js.window.dispatchEvent(ev)
|
||||
existing.remove()
|
||||
print("Table Renderer: Disabled")
|
||||
else
|
||||
print("Table Renderer: Already inactive")
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------
|
||||
-- Generic validators
|
||||
------------------------------------------------
|
||||
|
||||
local function isNumber(v)
|
||||
return tonumber(v) ~= nil
|
||||
end
|
||||
|
||||
local function isBetween(v, min, max)
|
||||
v = tonumber(v)
|
||||
return v ~= nil and v >= min and v <= max
|
||||
end
|
||||
|
||||
local function isInt(v)
|
||||
v = tonumber(v)
|
||||
return v ~= nil and math.floor(v) == v
|
||||
end
|
||||
|
||||
------------------------------------------------
|
||||
-- Renderers (JS visual + Lua validation)
|
||||
------------------------------------------------
|
||||
mls.table.renderer = {
|
||||
euro = {
|
||||
visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} €`]],
|
||||
validation = isNumber
|
||||
},
|
||||
usd = {
|
||||
visual = [[isNaN(v) ? v : `$${parseFloat(v).toLocaleString()}`]],
|
||||
validation = isNumber
|
||||
},
|
||||
kg = {
|
||||
visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} kg`]],
|
||||
validation = isNumber
|
||||
},
|
||||
km = {
|
||||
visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} km`]],
|
||||
validation = isNumber
|
||||
},
|
||||
watt = {
|
||||
visual = [[isNaN(v) ? v : `${parseFloat(v).toLocaleString()} W`]],
|
||||
validation = isNumber
|
||||
},
|
||||
percent = {
|
||||
visual = [[isNaN(v) ? v : `${(parseFloat(v) * 100).toFixed(0)} %`]],
|
||||
validation = isNumber
|
||||
},
|
||||
int = {
|
||||
visual = [[isNaN(v) ? v : parseInt(v, 10).toLocaleString()]],
|
||||
validation = isInt
|
||||
},
|
||||
float = {
|
||||
visual = [[isNaN(v) ? v : parseFloat(v).toFixed(2)]],
|
||||
validation = isNumber
|
||||
},
|
||||
upper = {
|
||||
visual = [[v.toString().toUpperCase()]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
lower = {
|
||||
visual = [[v.toString().toLowerCase()]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
bold = {
|
||||
visual = [[`<strong>${v}</strong>`]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
italic = {
|
||||
visual = [[`<em>${v}</em>`]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
link = {
|
||||
visual = [[`<a href="${v}" target="_blank">${v.replace(/^https?:\/\//, '')}</a>`]],
|
||||
validation = function(v)
|
||||
return type(v) == "string" and v:match("^https?://")
|
||||
end
|
||||
},
|
||||
logical = {
|
||||
completion = {
|
||||
{
|
||||
name = "❌",
|
||||
value = "false"
|
||||
},
|
||||
{
|
||||
name = "✅",
|
||||
value = "true"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
if (v !== '✅' && v !== '❌') {
|
||||
const val = v.toString().toLowerCase().trim();
|
||||
return (val === '1' || val === 'true' || val === 'yes' || val === 'ok') ? '✅' : '❌';
|
||||
}
|
||||
return v;
|
||||
]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
evaluation = {
|
||||
completion = {
|
||||
{
|
||||
name = "🤍🤍🤍🤍🤍",
|
||||
value = "0"
|
||||
},
|
||||
{
|
||||
name = "❤️🤍🤍🤍🤍",
|
||||
value = "1"
|
||||
},
|
||||
{
|
||||
name = "❤️❤️🤍🤍🤍",
|
||||
value = "2"
|
||||
},
|
||||
{
|
||||
name = "❤️❤️❤️🤍🤍",
|
||||
value = "3"
|
||||
},
|
||||
{
|
||||
name = "❤️❤️❤️❤️🤍",
|
||||
value = "4"
|
||||
},
|
||||
{
|
||||
name = "❤️❤️❤️❤️❤️",
|
||||
value = "5"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
const n = parseInt(v, 10);
|
||||
if (isNaN(n)) return v;
|
||||
return '❤️'.repeat(Math.max(0, Math.min(n, 5)))
|
||||
+ '🤍'.repeat(5 - Math.max(0, Math.min(n, 5)));
|
||||
]],
|
||||
validation = function(v)
|
||||
return isBetween(v, 1, 5)
|
||||
end
|
||||
},
|
||||
histo = {
|
||||
visual = [[
|
||||
const n = parseInt(v, 10);
|
||||
return isNaN(n) ? v : '█'.repeat(n);
|
||||
]],
|
||||
validation = isInt
|
||||
},
|
||||
gauge = {
|
||||
visual = [[
|
||||
const a = 0;
|
||||
const b = 100;
|
||||
const val = parseInt(v, 10);
|
||||
if (isNaN(val)) return v;
|
||||
const clampedValue = Math.max(a, Math.min(val, b));
|
||||
const percentage = (clampedValue - a) / (b - a);
|
||||
const filled = Math.floor(percentage * 10);
|
||||
const empty = 10 - filled;
|
||||
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}]`;
|
||||
]],
|
||||
validation = function(v)
|
||||
return isBetween(v, 0, 100)
|
||||
end
|
||||
},
|
||||
trend = {
|
||||
completion = {
|
||||
{
|
||||
name = "🔼",
|
||||
value = "+"
|
||||
},
|
||||
{
|
||||
name = "🔽",
|
||||
value = "-"
|
||||
},
|
||||
{
|
||||
name = "➡️",
|
||||
value = "="
|
||||
}
|
||||
},
|
||||
visual = [[
|
||||
const val = v.trim();
|
||||
if (val === '+') return '🔼';
|
||||
if (val === '-') return '🔽';
|
||||
if (val === '=') return '➡️';
|
||||
return val;
|
||||
]],
|
||||
validation = function(v)
|
||||
return v == "+" or v == "-" or v == "="
|
||||
end
|
||||
},
|
||||
emoji = {
|
||||
completion = {
|
||||
-- basic emotions
|
||||
{
|
||||
name = "happy 😃",
|
||||
value = "happy"
|
||||
},
|
||||
{
|
||||
name = "sad 😢",
|
||||
value = "sad"
|
||||
},
|
||||
{
|
||||
name = "angry 😠",
|
||||
value = "angry"
|
||||
},
|
||||
{
|
||||
name = "love ❤️",
|
||||
value = "love"
|
||||
},
|
||||
{
|
||||
name = "neutral 😐",
|
||||
value = "neutral"
|
||||
},
|
||||
{
|
||||
name = "cool 😎",
|
||||
value = "cool"
|
||||
},
|
||||
|
||||
-- positive / joyful
|
||||
{
|
||||
name = "smile 😊",
|
||||
value = "smile"
|
||||
},
|
||||
{
|
||||
name = "grin 😁",
|
||||
value = "grin"
|
||||
},
|
||||
{
|
||||
name = "laugh 😂",
|
||||
value = "laugh"
|
||||
},
|
||||
{
|
||||
name = "excited 🤩",
|
||||
value = "excited"
|
||||
},
|
||||
{
|
||||
name = "proud 😌",
|
||||
value = "proud"
|
||||
},
|
||||
{
|
||||
name = "relieved 😮💨",
|
||||
value = "relieved"
|
||||
},
|
||||
{
|
||||
name = "thankful 🙏",
|
||||
value = "thankful"
|
||||
},
|
||||
{
|
||||
name = "party 🥳",
|
||||
value = "party"
|
||||
},
|
||||
{
|
||||
name = "confident 😏",
|
||||
value = "confident"
|
||||
},
|
||||
|
||||
-- negative / difficult
|
||||
{
|
||||
name = "cry 😭",
|
||||
value = "cry"
|
||||
},
|
||||
{
|
||||
name = "disappointed 😞",
|
||||
value = "disappointed"
|
||||
},
|
||||
{
|
||||
name = "worried 😟",
|
||||
value = "worried"
|
||||
},
|
||||
{
|
||||
name = "anxious 😰",
|
||||
value = "anxious"
|
||||
},
|
||||
{
|
||||
name = "scared 😱",
|
||||
value = "scared"
|
||||
},
|
||||
{
|
||||
name = "tired 😴",
|
||||
value = "tired"
|
||||
},
|
||||
{
|
||||
name = "sick 🤒",
|
||||
value = "sick"
|
||||
},
|
||||
{
|
||||
name = "bored 😒",
|
||||
value = "bored"
|
||||
},
|
||||
{
|
||||
name = "frustrated 😤",
|
||||
value = "frustrated"
|
||||
},
|
||||
{
|
||||
name = "confused 😕",
|
||||
value = "confused"
|
||||
},
|
||||
|
||||
-- reactions
|
||||
{
|
||||
name = "surprised 😮",
|
||||
value = "surprised"
|
||||
},
|
||||
{
|
||||
name = "shocked 😲",
|
||||
value = "shocked"
|
||||
},
|
||||
{
|
||||
name = "thinking 🤔",
|
||||
value = "thinking"
|
||||
},
|
||||
{
|
||||
name = "facepalm 🤦",
|
||||
value = "facepalm"
|
||||
},
|
||||
{
|
||||
name = "shrug 🤷",
|
||||
value = "shrug"
|
||||
},
|
||||
{
|
||||
name = "eyeRoll 🙄",
|
||||
value = "eyeroll"
|
||||
},
|
||||
|
||||
-- social / playful
|
||||
{
|
||||
name = "wink 😉",
|
||||
value = "wink"
|
||||
},
|
||||
{
|
||||
name = "kiss 😘",
|
||||
value = "kiss"
|
||||
},
|
||||
{
|
||||
name = "hug 🤗",
|
||||
value = "hug"
|
||||
},
|
||||
{
|
||||
name = "teasing 😜",
|
||||
value = "teasing"
|
||||
},
|
||||
{
|
||||
name = "silly 🤪",
|
||||
value = "silly"
|
||||
},
|
||||
|
||||
-- approval / disapproval
|
||||
{
|
||||
name = "ok 👌",
|
||||
value = "ok"
|
||||
},
|
||||
{
|
||||
name = "thumbsUp 👍",
|
||||
value = "thumbsup"
|
||||
},
|
||||
{
|
||||
name = "thumbsDown 👎",
|
||||
value = "thumbsdown"
|
||||
},
|
||||
{
|
||||
name = "clap 👏",
|
||||
value = "clap"
|
||||
},
|
||||
{
|
||||
name = "respect 🫡",
|
||||
value = "respect"
|
||||
},
|
||||
|
||||
-- status / misc
|
||||
{
|
||||
name = "fire 🔥",
|
||||
value = "fire"
|
||||
},
|
||||
{
|
||||
name = "star ⭐",
|
||||
value = "star"
|
||||
},
|
||||
{
|
||||
name = "check ✅",
|
||||
value = "check"
|
||||
},
|
||||
{
|
||||
name = "cross ❌",
|
||||
value = "cross"
|
||||
},
|
||||
{
|
||||
name = "warning ⚠️",
|
||||
value = "warning"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
const map = {
|
||||
// basic emotions
|
||||
happy: '😃',
|
||||
sad: '😢',
|
||||
angry: '😠',
|
||||
love: '❤️',
|
||||
neutral: '😐',
|
||||
cool: '😎',
|
||||
|
||||
// positive / joyful
|
||||
smile: '😊',
|
||||
grin: '😁',
|
||||
laugh: '😂',
|
||||
excited: '🤩',
|
||||
proud: '😌',
|
||||
relieved: '😮💨',
|
||||
thankful: '🙏',
|
||||
party: '🥳',
|
||||
confident: '😏',
|
||||
|
||||
|
||||
// negative / difficult
|
||||
cry: '😭',
|
||||
disappointed: '😞',
|
||||
worried: '😟',
|
||||
anxious: '😰',
|
||||
scared: '😱',
|
||||
tired: '😴',
|
||||
sick: '🤒',
|
||||
bored: '😒',
|
||||
frustrated: '😤',
|
||||
confused: '😕',
|
||||
|
||||
// reactions
|
||||
surprised: '😮',
|
||||
shocked: '😲',
|
||||
thinking: '🤔',
|
||||
facepalm: '🤦',
|
||||
shrug: '🤷',
|
||||
eyeRoll: '🙄',
|
||||
|
||||
// social / playful
|
||||
wink: '😉',
|
||||
kiss: '😘',
|
||||
hug: '🤗',
|
||||
teasing: '😜',
|
||||
silly: '🤪',
|
||||
|
||||
// approval / disapproval
|
||||
ok: '👌',
|
||||
thumbsUp: '👍',
|
||||
thumbsDown: '👎',
|
||||
clap: '👏',
|
||||
respect: '🫡',
|
||||
|
||||
// status / misc
|
||||
fire: '🔥',
|
||||
star: '⭐',
|
||||
check: '✅',
|
||||
cross: '❌',
|
||||
warning: '⚠️',
|
||||
};
|
||||
const key = v.toString().toLowerCase();
|
||||
return map[key] || v;
|
||||
]],
|
||||
validation = function(v)
|
||||
return type(v) == "string"
|
||||
end
|
||||
},
|
||||
posneg = {
|
||||
completion = {
|
||||
{
|
||||
name = "-2 🟥🟥",
|
||||
value = "-2"
|
||||
},
|
||||
{
|
||||
name = "-1 🟥",
|
||||
value = "-1"
|
||||
},
|
||||
{
|
||||
name = "0 ⬜",
|
||||
value = "0"
|
||||
},
|
||||
{
|
||||
name = "1 🟩",
|
||||
value = "1"
|
||||
},
|
||||
{
|
||||
name = "2 🟩🟩",
|
||||
value = "2"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
if (isNaN(v)) return v;
|
||||
const val = parseInt(v, 10);
|
||||
if (val < 0) return "🟥".repeat(Math.abs(val));
|
||||
if (val > 0) return "🟩".repeat(Math.abs(val));
|
||||
return "⬜";
|
||||
]],
|
||||
validation = function(v)
|
||||
return isBetween(v, -10, 10)
|
||||
end
|
||||
},
|
||||
speed = {
|
||||
visual = [[v + " km/h"]],
|
||||
validation = function(v)
|
||||
return isBetween(v, 0, 300)
|
||||
end
|
||||
},
|
||||
mood = {
|
||||
completion = {
|
||||
{
|
||||
name = "😞",
|
||||
value = "1"
|
||||
},
|
||||
{
|
||||
name = "🙁",
|
||||
value = "2"
|
||||
},
|
||||
{
|
||||
name = "😐",
|
||||
value = "3"
|
||||
},
|
||||
{
|
||||
name = "🙂",
|
||||
value = "4"
|
||||
},
|
||||
{
|
||||
name = "😄",
|
||||
value = "5"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
const n = parseInt(v, 10);
|
||||
const moodScaleSoft = ['😔', '🙁', '😐', '🙂', '😄'];
|
||||
return moodScaleSoft[(n - 1) % 5];
|
||||
]],
|
||||
validation = function(v)
|
||||
return isBetween(v, 1, 5)
|
||||
end
|
||||
},
|
||||
stars = {
|
||||
completion = {
|
||||
{
|
||||
name = "1 ⭐",
|
||||
value = "1"
|
||||
},
|
||||
{
|
||||
name = "2 ⭐⭐",
|
||||
value = "2"
|
||||
},
|
||||
{
|
||||
name = "3 ⭐⭐⭐",
|
||||
value = "3"
|
||||
},
|
||||
{
|
||||
name = "4 ⭐⭐⭐⭐",
|
||||
value = "4"
|
||||
},
|
||||
{
|
||||
name = "5 ⭐⭐⭐⭐⭐",
|
||||
value = "5"
|
||||
},
|
||||
{
|
||||
name = "6 ⭐⭐⭐⭐⭐⭐",
|
||||
value = "6"
|
||||
},
|
||||
{
|
||||
name = "7 ⭐⭐⭐⭐⭐⭐⭐",
|
||||
value = "7"
|
||||
},
|
||||
{
|
||||
name = "8 ⭐⭐⭐⭐⭐⭐⭐⭐",
|
||||
value = "8"
|
||||
},
|
||||
{
|
||||
name = "9 ⭐⭐⭐⭐⭐⭐⭐⭐⭐",
|
||||
value = "9"
|
||||
},
|
||||
{
|
||||
name = "10 ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐",
|
||||
value = "10"
|
||||
},
|
||||
},
|
||||
visual = [[
|
||||
const n = parseInt(v, 10);
|
||||
return isNaN(n) ? v : '⭐'.repeat(Math.max(0, Math.min(n, 10)));
|
||||
]],
|
||||
validation = function(v)
|
||||
return isBetween(v, 1, 10)
|
||||
end
|
||||
},
|
||||
badge = {
|
||||
completion = {},
|
||||
visual = [[
|
||||
return `<span style="background:#2196f3;color:white;padding:2px 6px;border-radius:8px;font-size:0.9em;">${v}</span>`;
|
||||
]],
|
||||
validation = function(v)
|
||||
return v ~= nil
|
||||
end
|
||||
},
|
||||
}
|
||||
|
||||
------------------------------------------------
|
||||
-- Dispatcher
|
||||
------------------------------------------------
|
||||
mls.table.render = function(label, rendererN)
|
||||
local rendererName = rendererN
|
||||
if (type(rendererN) == "table") then
|
||||
for _, tag in ipairs(rendererN) do
|
||||
if mls.table.renderer[tag] then
|
||||
rendererName = tag
|
||||
end
|
||||
end
|
||||
end
|
||||
if label and rendererName and mls.table.renderer[rendererName] then
|
||||
local renderer = mls.table.renderer[rendererName]
|
||||
local input
|
||||
if renderer.completion and # renderer.completion > 0 then
|
||||
input = editor.filterBox(label, renderer.completion)
|
||||
else
|
||||
input = editor.prompt(label)
|
||||
end
|
||||
local value = input
|
||||
if input and input.value then
|
||||
value = input.value
|
||||
end
|
||||
if renderer.validation(value) then
|
||||
return value
|
||||
else
|
||||
editor.flashNotification("Input not valid: " .. tostring(input), "error")
|
||||
end
|
||||
else
|
||||
editor.flashNotification("Missing renderer: " .. tostring(rendererName), "error")
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------
|
||||
-- JS generator
|
||||
------------------------------------------------
|
||||
local function exportJSFormatters(renderers)
|
||||
local lines = {}
|
||||
table.insert(lines, "const formatters = {")
|
||||
for name, def in pairs(renderers) do
|
||||
if def.visual then
|
||||
local js = def.visual
|
||||
-- single line or block
|
||||
if string.match(js, "\n") then
|
||||
table.insert(lines, " " .. name .. ": v => {")
|
||||
table.insert(lines, js)
|
||||
table.insert(lines, " },")
|
||||
else
|
||||
table.insert(lines, " " .. name .. ": v => " .. js .. ",")
|
||||
end
|
||||
end
|
||||
end
|
||||
table.insert(lines, "};")
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- ENABLE
|
||||
--------------------------------------------------
|
||||
|
||||
function mls.table.enableTableRenderer()
|
||||
local scriptId = "sb-table-renderer-runtime"
|
||||
if js.window.document.getElementById(scriptId) then
|
||||
print("Table Renderer: Already active")
|
||||
return
|
||||
end
|
||||
local scriptEl = js.window.document.createElement("script")
|
||||
scriptEl.id = scriptId
|
||||
scriptEl.innerHTML = [[
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const DEBUG = false;
|
||||
const log = (...a) => DEBUG && console.log('[sb-table-renderer]', ...a);
|
||||
|
||||
/* ---------------- FORMATTERS ---------------- */
|
||||
]] .. exportJSFormatters(mls.table.renderer) .. [[
|
||||
/* ---------------- CORE ---------------- */
|
||||
|
||||
function extractFormats(table) {
|
||||
const formats = [];
|
||||
const header =
|
||||
table.querySelector('thead tr') ||
|
||||
table.querySelector('tr');
|
||||
if (!header) return formats;
|
||||
|
||||
[...header.cells].forEach((cell, idx) => {
|
||||
formats[idx] = null;
|
||||
const tags = cell.querySelectorAll('a.hashtag,[data-tag-name]');
|
||||
for (const tag of tags) {
|
||||
const name =
|
||||
tag.dataset?.tagName ||
|
||||
tag.textContent?.replace('#', '');
|
||||
if (formatters[name]) {
|
||||
formats[idx] = name;
|
||||
tag.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return formats;
|
||||
}
|
||||
|
||||
function processTable(table) {
|
||||
if (table.dataset.sbFormatted) return;
|
||||
const formats = extractFormats(table);
|
||||
const rows = table.tBodies[0]?.rows || [];
|
||||
|
||||
[...rows].forEach(row => {
|
||||
[...row.cells].forEach((cell, idx) => {
|
||||
const fmt = formats[idx];
|
||||
if (!fmt) return;
|
||||
|
||||
const raw = cell.textContent.trim();
|
||||
const out = formatters[fmt](raw);
|
||||
if (out !== raw) {
|
||||
cell.textContent = out;
|
||||
cell.dataset.sbformatted = fmt;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
table.dataset.sbFormatted = 'true';
|
||||
}
|
||||
|
||||
function scan() {
|
||||
document
|
||||
.querySelectorAll('#sb-editor table')
|
||||
.forEach(processTable);
|
||||
}
|
||||
|
||||
/* ---------------- OBSERVER ---------------- */
|
||||
|
||||
const observer = new MutationObserver(scan);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
scan();
|
||||
|
||||
/* ---------------- CLEANUP ---------------- */
|
||||
|
||||
window.addEventListener('sb-table-renderer-unload', function cln() {
|
||||
observer.disconnect();
|
||||
document
|
||||
.querySelectorAll('[data-sbformatted]')
|
||||
.forEach(c => {
|
||||
c.removeAttribute('data-sbformatted');
|
||||
});
|
||||
document
|
||||
.querySelectorAll('table[data-sb-formatted]')
|
||||
.forEach(t => delete t.dataset.sbFormatted);
|
||||
window.removeEventListener('sb-table-renderer-unload', cln);
|
||||
});
|
||||
|
||||
})()
|
||||
]]
|
||||
js.window.document.body.appendChild(scriptEl)
|
||||
end
|
||||
|
||||
--------------------------------------------------
|
||||
-- COMMANDS
|
||||
--------------------------------------------------
|
||||
|
||||
command.define{
|
||||
name = "Table: Enable Renderer",
|
||||
run = function()
|
||||
mls.table.enableTableRenderer()
|
||||
end
|
||||
}
|
||||
|
||||
command.define{
|
||||
name = "Table: Disable Renderer",
|
||||
run = function()
|
||||
mls.table.cleanupRenderer()
|
||||
end
|
||||
}
|
||||
--------------------------------------------------
|
||||
-- AUTOSTART
|
||||
--------------------------------------------------
|
||||
if enabled then
|
||||
mls.table.enableTableRenderer()
|
||||
else
|
||||
mls.table.cleanupRenderer()
|
||||
end
|
||||
```
|
||||
|
||||
## Changelog
|
||||
- 2026-02-01
|
||||
- feat: define renderers in lua
|
||||
- feat: add validation mechanism
|
||||
- feat: add completion mechanism
|
||||
- 2026-01-24:
|
||||
- feat: convert to space-lua
|
||||
- feat: add renderers (mood, emoj)
|
||||
- 2026-01-02 feat: add kg, km, watt, histo
|
||||
|
||||
## Community
|
||||
|
||||
[Silverbullet forum](https://community.silverbullet.md/t/md-table-renderers/3545/15)
|
||||
|
||||
424
Library/Malys/Utilities.md
Normal file
424
Library/Malys/Utilities.md
Normal file
@@ -0,0 +1,424 @@
|
||||
---
|
||||
author: malys
|
||||
description: List of reusable functions.
|
||||
name: "Library/Malys/Utilities"
|
||||
tags: meta/library
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/Utilities.md"
|
||||
share.hash: 004bb007
|
||||
share.mode: pull
|
||||
---
|
||||
# Utilities
|
||||
|
||||
* `mls.getmeetingTitle()`: Extracts meeting title from page URL.
|
||||
* `mls.embedUrl(url, w, h)`: Embeds a URL using an iframe (width & height optional).
|
||||
* `mls.debug(message, prefix)`: Prints debug message (if `LOG_ENABLE` is true).
|
||||
* `mls.getCodeBlock(page, blockId, token, text)`: Retrieves code from a Markdown code block by ID, with optional token replacement.
|
||||
* `mls.parseISODate(isoDate)`: Parses an ISO date string to a timestamp (potential error with `xoffset`).
|
||||
* `mls.getStdlibInternal()`: Gets a list of internal API keys (cached).
|
||||
* `mls.positionToLineColumn(text, pos)`: Converts a text position to line/column numbers.
|
||||
* `table.appendArray(a, b)`: Appends array `b` to array `a`.
|
||||
* `table.unique(array)`: Returns unique elements from an array.
|
||||
* `table.uniqueKVBy(array, keyFn)`: Returns unique key-value pairs based on a key function.
|
||||
* `table.mergeKV(t1, t2)`: Merges two tables (recursive for nested tables).
|
||||
* `table.map(t, fn)`: Applies a function to each element of an array.
|
||||
* `table.toMd(data)`: Converts a table (or JSON) to a Markdown table.
|
||||
|
||||
## Debug
|
||||
|
||||
### How to enable debug mode
|
||||
|
||||
* Create space-lua with:
|
||||
```lua
|
||||
LOG_ENABLE=true
|
||||
```
|
||||
|
||||
* Reload system.
|
||||
* Open Chrome Console
|
||||
* Add filter “[Client] [DEBUG]“
|
||||
## Code
|
||||
```space-lua
|
||||
-- priority: 20
|
||||
mls=mls or {}
|
||||
|
||||
-- Convert meeting note title
|
||||
-- Extracts the meeting title from the current page URL.
|
||||
-- Assumes the URL format is like "namespace/Meeting_Note Title".
|
||||
-- Splits the URL by "/", takes the last part, and then splits that by "_"
|
||||
-- to remove the "Meeting" prefix and joins the remaining parts with a space.
|
||||
function mls.getmeetingTitle()
|
||||
local t=string.split(string.split(editor.getCurrentPage(),"/")[#string.split(editor.getCurrentPage(),"/")],"_")
|
||||
table.remove(t,1)
|
||||
t=table.concat(t, " ")
|
||||
return t
|
||||
end
|
||||
|
||||
-- Embed external resources
|
||||
-- Creates an HTML iframe to embed an external URL within the current page.
|
||||
-- Allows specifying width and height, defaulting to "100%" and "400px" respectively.
|
||||
function mls.embedUrl(specOrUrl,w,h)
|
||||
local width = w or "100%"
|
||||
local height = h or "400px"
|
||||
return widget.html(dom.iframe {
|
||||
src=specOrUrl,
|
||||
style="width: " .. width .. "; height: " .. height
|
||||
})
|
||||
end
|
||||
|
||||
---------------------------------------------
|
||||
---- Debug ---
|
||||
---------------------------------------------
|
||||
-- Pretty-print any Lua value (tables included)
|
||||
-- Recursively prints a Lua value, handling tables with indentation.
|
||||
-- Prevents infinite recursion by limiting the depth to 5.
|
||||
local function dump(value, depth)
|
||||
--print("[DEBUG][TYPE]"..type(value)) -- commented out debug line
|
||||
depth = depth or 0
|
||||
if type(value) ~= "table" or (type(value) == "string" and value ~= "[object Object]") then
|
||||
return value
|
||||
end
|
||||
|
||||
-- Prevent going too deep (avoid infinite recursion)
|
||||
if depth > 5 then
|
||||
return "{ ... }"
|
||||
end
|
||||
|
||||
local indent = string.rep(" ", depth)
|
||||
local next_indent = string.rep(" ", depth + 1)
|
||||
|
||||
local parts = {}
|
||||
table.insert(parts, "{")
|
||||
|
||||
for k, v in pairs(value) do
|
||||
local key = tostring(k)
|
||||
local val
|
||||
if type(v) == "table" then
|
||||
val = dump(v, depth + 1)
|
||||
else
|
||||
val = v
|
||||
end
|
||||
|
||||
table.insert(parts, next_indent .. tostring(key) .. " = " .. tostring(val) .. ",")
|
||||
end
|
||||
|
||||
table.insert(parts, indent .. "}")
|
||||
|
||||
return table.concat(parts, "\n")
|
||||
end
|
||||
|
||||
|
||||
-- Debug function
|
||||
-- Prints a debug message to the console if LOG_ENABLE is true.
|
||||
-- Uses the dump function to pretty-print the message.
|
||||
-- Allows specifying a prefix for the debug message.
|
||||
function mls.debug(message, prefix)
|
||||
if not LOG_ENABLE then
|
||||
return message
|
||||
end
|
||||
local log_message = dump(message)
|
||||
|
||||
local result = "[DEBUG]"
|
||||
if prefix then
|
||||
result = result .. "[" .. tostring(prefix) .. "]"
|
||||
end
|
||||
|
||||
result = result .. " " .. tostring(log_message)
|
||||
print(result)
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
---------------------------------------------
|
||||
---- Code Block code extraction ---
|
||||
---------------------------------------------
|
||||
-- Get child of node
|
||||
-- Helper function to find a child node of a given type within a parent node.
|
||||
local getChild = function(node, type)
|
||||
for _, child in ipairs(node.children) do
|
||||
if child.type == type then
|
||||
return child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get text of Node
|
||||
-- Helper function to recursively extract the text content from a node.
|
||||
local getText = function(node)
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
if node.text then
|
||||
return node.text
|
||||
else
|
||||
for _, child in ipairs(node.children) do
|
||||
local text = getText(child)
|
||||
if text then
|
||||
return text
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Find codeblock
|
||||
-- Recursively searches for a code block (FencedCode node) with a specific blockId in the node's children.
|
||||
local findMyFence = function(node,blockId)
|
||||
if not node.children then
|
||||
return nil
|
||||
end
|
||||
for _, child in ipairs(node.children) do
|
||||
--mls.debug(child) -- commented out debug line
|
||||
if child.type == "FencedCode" then
|
||||
local info = getText(getChild(child, "CodeInfo"))
|
||||
--mls.debug(info) -- commented out debug line
|
||||
if info and info:find(blockId) then
|
||||
mls.debug(info)
|
||||
return getChild(child, "CodeText")
|
||||
end
|
||||
end
|
||||
local result= findMyFence(child,blockId)
|
||||
if result ~=nil then
|
||||
return result
|
||||
end
|
||||
--break -- for loop
|
||||
end --for
|
||||
end
|
||||
|
||||
-- Get code source in md codeblock
|
||||
-- Parses a Markdown page, finds a code block with the given blockId, and returns its content.
|
||||
-- Allows replacing a token within the code block with a new text value.
|
||||
function mls.getCodeBlock (page,blockId,token,text)
|
||||
local tree = markdown.parseMarkdown(space.readPage(page))
|
||||
--mls.debug(tree) -- commented out debug line
|
||||
if tree then
|
||||
local fence = findMyFence(tree,blockId)
|
||||
--mls.debug(fence) -- commented out debug line
|
||||
if fence then
|
||||
local result=fence.children[1].text
|
||||
if token == nil or text==nil then
|
||||
return result
|
||||
else
|
||||
return string.gsub(result, token, text)
|
||||
end
|
||||
end
|
||||
end
|
||||
return "Error"
|
||||
end
|
||||
|
||||
-- Parse ISO Date
|
||||
-- Parses an ISO date string into a Unix timestamp.
|
||||
-- This function appears incomplete and has a potential error in the offset calculation.
|
||||
-- The `xoffset` variable is not defined.
|
||||
function mls.parseISODate(isoDate)
|
||||
local pattern = "(%d+)%-(%d+)%-(%d+)%a(%d+)%:%d+:%d+([Z%+%-])(%d?%d?)%:?(%d?%d?)"
|
||||
local year, month, day, hour, minute,
|
||||
seconds, offsetsign, offsethour, offsetmin = json_date:match(pattern)
|
||||
local timestamp = os.time{year = year, month = month,
|
||||
day = day, hour = hour, min = minute, sec = seconds}
|
||||
local offset = 0
|
||||
if offsetsign ~= 'Z' then
|
||||
offset = tonumber(offsethour) * 60 + tonumber(offsetmin)
|
||||
if xoffset == "-" then offset = offset * -1 end
|
||||
end
|
||||
|
||||
return timestamp + offset
|
||||
end
|
||||
|
||||
-- Get Stdlib Internal
|
||||
-- Retrieves a list of internal API keys from a remote file.
|
||||
-- Caches the results to avoid repeated fetching.
|
||||
-- The URL points to a file in the silverbulletmd/silverbullet repository.
|
||||
function mls.getStdlibInternal()
|
||||
if _G ~=nil then
|
||||
return table.keys(_G)
|
||||
end
|
||||
-- get list of internal api TODO: remove
|
||||
local KEY="stdlib_internal"
|
||||
local result=mls.cache.ttl.CacheManager.get(KEY)
|
||||
if result == nil then
|
||||
local url = "https://raw.githubusercontent.com/silverbulletmd/silverbullet/refs/heads/main/client/space_lua/stdlib.ts"
|
||||
local resp = net.proxyFetch(url)
|
||||
if resp.status ~= 200 then
|
||||
error("Failed to fetch file: " .. resp.status)
|
||||
end
|
||||
local content = resp.body
|
||||
local results = {}
|
||||
for line in string.split(content,"\n") do
|
||||
local key = string.match(string.trim(line), 'env%.set%("(.*)"')
|
||||
if key then
|
||||
table.insert(results, key)
|
||||
end
|
||||
end
|
||||
result=table.sort(results)
|
||||
mls.cache.ttl.CacheManager.set(KEY,result)
|
||||
end
|
||||
return table.sort(result)
|
||||
end
|
||||
|
||||
-- Position to Line Column
|
||||
-- Converts a character position within a text string to a line and column number.
|
||||
function mls.positionToLineColumn(text, pos)
|
||||
local line = 1
|
||||
local column = 0
|
||||
local lastNewline = -1
|
||||
|
||||
for i = 0, pos - 1 do
|
||||
if string.sub(text, i + 1, i + 1) == "\n" then
|
||||
line = line + 1
|
||||
lastNewline = i
|
||||
column = 0
|
||||
else
|
||||
column = column + 1
|
||||
end
|
||||
end
|
||||
|
||||
return { line = line, column = column }
|
||||
end
|
||||
|
||||
-----------------------------------------
|
||||
-- TABLE
|
||||
-----------------------------------------
|
||||
-- Append Array
|
||||
-- Appends all elements from one array to another.
|
||||
function table.appendArray(a, b)
|
||||
if a~= nil and b ~= nil then
|
||||
for i = 1, #b do
|
||||
table.insert(a, b[i])
|
||||
end
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
-- Unique
|
||||
-- Returns a new array containing only the unique elements from the input array.
|
||||
function table.unique(array)
|
||||
local seen = {}
|
||||
local result = {}
|
||||
|
||||
for i = 1, #array do
|
||||
local v = array[i]
|
||||
if not seen[v] then
|
||||
seen[v] = true
|
||||
result[#result + 1] = v
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Unique KV By
|
||||
-- Returns a new array containing only the unique key-value pairs from the input array,
|
||||
-- based on a provided key function.
|
||||
function table.uniqueKVBy(array, keyFn)
|
||||
local seen = {}
|
||||
local result = {}
|
||||
|
||||
for i = 1, #array do
|
||||
local key = keyFn(array[i])
|
||||
if not seen[key] then
|
||||
seen[key] = true
|
||||
result[#result + 1] = array[i]
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Merge KV
|
||||
-- Merges two tables, recursively merging nested tables.
|
||||
-- If a key exists in both tables and the values are both tables, the nested tables are merged.
|
||||
-- Otherwise, the value from the second table overwrites the value from the first table.
|
||||
function table.mergeKV(t1, t2)
|
||||
for k, v in pairs(t2) do
|
||||
if type(v) == "table" and type(t1[k]) == "table" then
|
||||
mergeTablesRecursive(t1[k], v)
|
||||
else
|
||||
t1[k] = v
|
||||
end
|
||||
end
|
||||
return t1
|
||||
end
|
||||
|
||||
-- Map
|
||||
-- Applies a function to each element of an array and returns a new array with the results.
|
||||
function table.map(t, fn)
|
||||
local result = {}
|
||||
for i, v in ipairs(t) do
|
||||
result[i] = fn(v, i)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- To Md
|
||||
-- Converts a Lua table to a Markdown table string.
|
||||
-- Handles both arrays of arrays and arrays of objects.
|
||||
-- Can parse JSON or Lua strings as input.
|
||||
function table.toMd(data)
|
||||
local tbl
|
||||
local input=string.trim(data)
|
||||
-- If input is a string, try parsing as JSON
|
||||
if string.startsWith(data,"{") then
|
||||
-- Lua
|
||||
tbl=spacelua.evalExpression(spacelua.parseExpression(data))
|
||||
else
|
||||
--JSON
|
||||
tbl=js.tolua(js.window.JSON.parse(js.tojs(data)))
|
||||
end
|
||||
if #tbl == 0 then return "" end
|
||||
|
||||
local md = {}
|
||||
|
||||
-- Helper to convert a row to Markdown
|
||||
local function rowToMd(row)
|
||||
local cells = {}
|
||||
for _, cell in ipairs(row) do
|
||||
table.insert(cells, tostring(cell))
|
||||
end
|
||||
return "| " .. table.concat(cells, " | ") .. " |"
|
||||
end
|
||||
|
||||
-- Handle array of objects
|
||||
local first = tbl[1]
|
||||
local headers
|
||||
if type(first) == "table" and not (#first > 0) then
|
||||
headers = {}
|
||||
for k in pairs(first) do table.insert(headers, k) end
|
||||
local rows = {headers}
|
||||
for _, obj in ipairs(tbl) do
|
||||
local row = {}
|
||||
for _, h in ipairs(headers) do
|
||||
table.insert(row, obj[h] or "")
|
||||
end
|
||||
table.insert(rows, row)
|
||||
end
|
||||
tbl = rows
|
||||
end
|
||||
|
||||
-- Header
|
||||
table.insert(md, rowToMd(tbl[1]))
|
||||
|
||||
-- Separator
|
||||
local sep = {}
|
||||
for _ = 1, #tbl[1] do table.insert(sep, "---") end
|
||||
table.insert(md, "| " .. table.concat(sep, " | ") .. " |")
|
||||
|
||||
-- Data rows
|
||||
for i = 2, #tbl do
|
||||
table.insert(md, rowToMd(tbl[i]))
|
||||
end
|
||||
|
||||
return table.concat(md, "\n")
|
||||
end
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
* 2026-01-22:
|
||||
* getStdlibInternal compatible with edge version https://community.silverbullet.md/t/risk-audit/3562/14
|
||||
* 2026-01-20:
|
||||
* feat: add table functions
|
||||
* map
|
||||
* mergeKV
|
||||
* uniqueKVBy
|
||||
* unique
|
||||
* appendArray
|
||||
* feat: add `mls.getStdlibInternal` fucntion
|
||||
|
||||
61
Library/Malys/mdPrettier.md
Normal file
61
Library/Malys/mdPrettier.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
author: malys
|
||||
description: Beautify md file.
|
||||
name: "Library/Malys/mdPrettier"
|
||||
tags: meta/library
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/mdPrettier.md"
|
||||
share.hash: f2473a28
|
||||
share.mode: pull
|
||||
---
|
||||
# Md Prettier
|
||||
|
||||
Beautify markdown.
|
||||
|
||||
## Code
|
||||
|
||||
```space-lua
|
||||
local prettier = js.import("https://cdn.jsdelivr.net/npm/prettier/standalone/+esm")
|
||||
local prettierMarkdown = js.import("https://cdn.jsdelivr.net/npm/prettier/plugins/markdown/+esm")
|
||||
|
||||
function formatText(text)
|
||||
return prettier.format(text, {
|
||||
parser = "markdown",
|
||||
plugins = { prettierMarkdown },
|
||||
|
||||
printWidth = 160,
|
||||
proseWrap = "preserve",
|
||||
|
||||
-- These DO NOT affect markdown tables
|
||||
useTabs = true,
|
||||
tabWidth = 4,
|
||||
})
|
||||
end
|
||||
|
||||
function formatDocument()
|
||||
local text = editor.getText()
|
||||
local formattedText = formatText(text)
|
||||
editor.setText(formattedText)
|
||||
end
|
||||
|
||||
command.define {
|
||||
name = "Beautify: markdown",
|
||||
run = formatDocument,
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
|
||||
- **Placement:** `prettier-ignore` _must_ be the very first line within the code
|
||||
fence. Any leading whitespace will cause it to be ignored.
|
||||
- **Scope:** `prettier-ignore` applies to the entire code fence it's placed in.
|
||||
- **Alternative:** If you want to disable formatting for a specific section of
|
||||
code _within_ a code fence, you can use `prettier-ignore` on a single line:
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-01-25:
|
||||
- feat: add option and ignore code
|
||||
|
||||
## Community
|
||||
|
||||
[Silverbullet forum](https://community.silverbullet.md/t/markdown-formatter-for-silverbullet/3071)
|
||||
190
Library/Malys/theme-malys.md
Normal file
190
Library/Malys/theme-malys.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
author: malys
|
||||
description: Dark theme thought for readibility and productivity
|
||||
name: "Library/Malys/theme-malys"
|
||||
tags: meta/library
|
||||
share.uri: "https://github.com/malys/silverbullet-libraries/blob/main/src/Theme/theme-malys.md"
|
||||
share.hash: 0817db84
|
||||
share.mode: pull
|
||||
---
|
||||
# Malys theme
|
||||
|
||||
Dark theme thought for readability and productivity.
|
||||
|
||||
## Example
|
||||
# 1
|
||||
## 2
|
||||
### 3
|
||||
#### 4
|
||||
##### 5
|
||||
###### 6
|
||||
|
||||
`code`
|
||||
|
||||
*emphasis*
|
||||
|
||||
**strong**
|
||||
|
||||
## Editor
|
||||
|
||||
```space-style
|
||||
html {
|
||||
--ui-font: firacode-nf, monospace !important;
|
||||
--editor-font: firacode-nf, monospace !important;
|
||||
--editor-width: 100% !important;
|
||||
|
||||
line-height: 1.4 !important;
|
||||
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
}
|
||||
|
||||
.markmap {
|
||||
--markmap-text-color: #BBDEFB !important;
|
||||
}
|
||||
|
||||
#sb-main .cm-editor {
|
||||
font-size: 15px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.sb-line-h1 {
|
||||
font-size: 1.8em !important;
|
||||
color: #ee82ee !important;
|
||||
}
|
||||
.sb-line-h2 {
|
||||
font-size: 1.6em !important;
|
||||
color: #6a5acd !important;
|
||||
}
|
||||
.sb-line-h3 {
|
||||
font-size: 1.4em !important;
|
||||
color: #4169e1 !important;
|
||||
}
|
||||
.sb-line-h4 {
|
||||
font-size: 1.2em !important;
|
||||
color: #008000 !important;
|
||||
}
|
||||
.sb-line-h5 {
|
||||
font-size: 1em !important;
|
||||
color: #ffff00 !important;
|
||||
}
|
||||
.sb-line-h6 {
|
||||
font-size: 1em !important;
|
||||
color: #ffa500 !important;
|
||||
}
|
||||
.sb-line-h1::before {
|
||||
content: "h1";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
.sb-line-h2::before {
|
||||
content: "h2";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
.sb-line-h3::before {
|
||||
content: "h3";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
.sb-line-h4::before {
|
||||
content: "h4";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
.sb-line-h5::before {
|
||||
content: "h5";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
.sb-line-h6::before {
|
||||
content: "h6";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.5em !important;
|
||||
}
|
||||
|
||||
|
||||
.sb-code {
|
||||
color: grey !important;
|
||||
}
|
||||
.sb-emphasis {
|
||||
color: darkorange !important;
|
||||
}
|
||||
.sb-strong {
|
||||
color: salmon !important;
|
||||
}
|
||||
|
||||
html {
|
||||
--treeview-phone-height: 25vh;
|
||||
--treeview-tablet-width: 25vw;
|
||||
--treeview-tablet-height: 100vh;
|
||||
--treeview-desktop-width: 20vw;
|
||||
}
|
||||
|
||||
.sb-bhs {
|
||||
height: var(--treeview-phone-height);
|
||||
}
|
||||
.cm-diagnosticText {
|
||||
color: black;
|
||||
}
|
||||
```
|
||||
|
||||
## Treeview
|
||||
```space-style
|
||||
.tree__label > span {
|
||||
font-size: calc(11px + 0.1vh);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
#sb-root:has(.sb-lhs) #sb-main,
|
||||
#sb-root:has(.sb-lhs) #sb-top {
|
||||
margin-left: var(--treeview-tablet-width);
|
||||
}
|
||||
|
||||
.sb-lhs {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
height: var(--treeview-tablet-height);
|
||||
width: var(--treeview-tablet-width);
|
||||
border-right: 1px solid var(--top-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
#sb-root:has(.sb-lhs) #sb-main,
|
||||
#sb-root:has(.sb-lhs) #sb-top {
|
||||
margin-left: var(--treeview-desktop-width);
|
||||
}
|
||||
|
||||
.sb-lhs {
|
||||
width: var(--treeview-desktop-width);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
.tree__label > span {
|
||||
padding: 0 5px;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.treeview-root {
|
||||
--st-label-height: auto;
|
||||
--st-subnodes-padding-left: 0.1rem;
|
||||
--st-collapse-icon-height: 2.1rem;
|
||||
--st-collapse-icon-width: 1.25rem;
|
||||
--st-collapse-icon-size: 1rem;
|
||||
}
|
||||
|
||||
## Outline
|
||||
|
||||
```space-style
|
||||
div.cm-scroller {
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
```
|
||||
117
Library/mrmugame/Silverbullet-Math.md
Normal file
117
Library/mrmugame/Silverbullet-Math.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
name: Library/mrmugame/Silverbullet-Math
|
||||
tags: meta/library
|
||||
files:
|
||||
- Silverbullet-Math/katex.mjs
|
||||
- Silverbullet-Math/katex.min.css
|
||||
- Silverbullet-Math/fonts/KaTeX_Typewriter-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Size4-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Size3-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Size2-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Size1-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Script-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_SansSerif-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_SansSerif-Italic.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_SansSerif-Bold.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Math-Italic.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Math-BoldItalic.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Main-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Main-Italic.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Main-Bold.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Main-BoldItalic.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Fraktur-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Fraktur-Bold.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Caligraphic-Regular.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_Caligraphic-Bold.woff2
|
||||
- Silverbullet-Math/fonts/KaTeX_AMS-Regular.woff2
|
||||
share.uri: "https://github.com/MrMugame/silverbullet-math/blob/main/Math.md"
|
||||
share.hash: "13247895"
|
||||
share.mode: pull
|
||||
---
|
||||
|
||||
# Silverbullet Math
|
||||
This library implements two new widgets
|
||||
|
||||
- `latex.block(...)` and
|
||||
- `latex.inline(...)`,
|
||||
|
||||
which can be used to render block and inline level math respectively. Both use ${latex.inline[[\href{https://katex.org/}{\KaTeX}]]} under the hood
|
||||
|
||||
## Examples
|
||||
Let ${latex.inline[[S]]} be a set and ${latex.inline[[\circ : S \times S \to S,\; (a, b) \mapsto a \cdot b]]} be a binary operation, then the pair ${latex.inline[[(S, \circ)]]} is called a *group* iff
|
||||
|
||||
1. ${latex.inline[[\forall a, b \in S, \; a \circ b \in S]]} (completeness),
|
||||
2. ${latex.inline[[\forall a,b,c \in S, \; (ab)c = a(bc)]]} (associativity),
|
||||
3. ${latex.inline[[\exists e \in S]]} such that ${latex.inline[[\forall a \in S,\; ae=a=ea]]} (identity) and
|
||||
4. ${latex.inline[[\forall a \in S,\; \exists b \in S]]} such that ${latex.inline[[ab=e=ba]]} (inverse).
|
||||
|
||||
The Fourier transform of a complex-valued (Lebesgue) integrable function ${latex.inline[[f(x)]]} on the real line, is the complex valued function ${latex.inline[[\hat {f}(\xi )]]}, defined by the integral
|
||||
${latex.block[[\widehat{f}(\xi) = \int_{-\infty}^{\infty} f(x)\ e^{-i 2\pi \xi x}\,dx, \quad \forall \xi \in \mathbb{R}.]]}
|
||||
|
||||
## Quality of life
|
||||
There are two slash commands to make writing math a little easier,
|
||||
|
||||
- `/math` and
|
||||
- `/equation`.
|
||||
|
||||
## Info
|
||||
The current ${latex.inline[[\KaTeX]]} version is ${latex.katex.version}.
|
||||
|
||||
## Implementation
|
||||
```space-lua
|
||||
local location = "Library/mrmugame/Silverbullet-Math"
|
||||
|
||||
-- TODO: Remove the check for system.getURLPrefix as soon as the API has settled
|
||||
latex = {
|
||||
header = string.format("<link rel=\"stylesheet\" href=\".fs/%s/katex.min.css\">", location),
|
||||
katex = js.import(string.format("%s.fs/%s/katex.mjs", system.getURLPrefix and system.getURLPrefix() or "/", location))
|
||||
}
|
||||
|
||||
function latex.inline(expression)
|
||||
local html = latex.katex.renderToString(expression, {
|
||||
trust = true,
|
||||
throwOnError = false,
|
||||
displayMode = false
|
||||
})
|
||||
|
||||
return widget.new {
|
||||
display = "inline",
|
||||
html = "<span>" .. latex.header .. html .. "</span>"
|
||||
}
|
||||
end
|
||||
|
||||
function latex.block(expression)
|
||||
local html = latex.katex.renderToString(expression, {
|
||||
trust = true,
|
||||
throwOnError = false,
|
||||
displayMode = true
|
||||
})
|
||||
|
||||
return widget.new {
|
||||
display = "block",
|
||||
html = "<span>" .. latex.header .. html .. "</span>"
|
||||
}
|
||||
end
|
||||
|
||||
slashcommand.define {
|
||||
name = "math",
|
||||
run = function()
|
||||
editor.insertAtCursor("${latex.inline[[]]}", false, true)
|
||||
editor.moveCursor(editor.getCursor() - 3)
|
||||
end
|
||||
}
|
||||
|
||||
slashcommand.define {
|
||||
name = "equation",
|
||||
run = function()
|
||||
editor.insertAtCursor("${latex.block[[]]}", false, true)
|
||||
editor.moveCursor(editor.getCursor() - 3)
|
||||
end
|
||||
}
|
||||
```
|
||||
|
||||
```space-style
|
||||
.sb-lua-directive-inline:has(.katex-html) {
|
||||
border: none !important;
|
||||
}
|
||||
```
|
||||
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_AMS-Regular.woff2
Normal file
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_AMS-Regular.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Main-Bold.woff2
Normal file
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Main-Bold.woff2
Normal file
Binary file not shown.
Binary file not shown.
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Main-Italic.woff2
Normal file
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Main-Italic.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Math-Italic.woff2
Normal file
BIN
Library/mrmugame/Silverbullet-Math/fonts/KaTeX_Math-Italic.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
Library/mrmugame/Silverbullet-Math/katex.min.css
vendored
Normal file
1
Library/mrmugame/Silverbullet-Math/katex.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
18541
Library/mrmugame/Silverbullet-Math/katex.mjs
Normal file
18541
Library/mrmugame/Silverbullet-Math/katex.mjs
Normal file
File diff suppressed because it is too large
Load Diff
23
Library/mrmugame/Silverbullet-PDF.md
Normal file
23
Library/mrmugame/Silverbullet-PDF.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Library/mrmugame/Silverbullet-PDF
|
||||
tags: meta/library
|
||||
files:
|
||||
- silverbullet-pdf.plug.js
|
||||
share.uri: "ghr:MrMugame/silverbullet-pdf/PLUG.md"
|
||||
share.hash: 3498d9f9
|
||||
share.mode: pull
|
||||
---
|
||||
# Silverbullet PDF
|
||||
This plug adds the ability to [Silverbullet](https://github.com/silverbulletmd/silverbullet) to view and annotate pdfs using a slightly modified version of the [pdfjs](https://github.com/mozilla/pdf.js) viewer. If used with [Silversearch](https://github.com/MrMugame/silversearch), Silverbullet PDF can extract text content from PDFs to help you search through them.
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
Silverbullet PDF is part of the [`Std`](https://silverbullet.md/Repositories/Std) repostitory and can by installed using the [Library Manager](https://silverbullet.md/Library%20Manager). You will have to navigate to `Library/Std/Pages/Library Manager` in *your* space and look for Silverbullet PDF under the available libraries and press `Install`.
|
||||
|
||||
## Internals
|
||||
The build process of this plug is rather weird. The steps are as follows
|
||||
|
||||
1. Uing `deno task download` the pdfjs repo will be cloned and the `pdfjs.patch` patch will be applied. I opted for this approach, because I wanted to avoid an extra repo for the few changes
|
||||
2. Using `deno task install` all npm install commands are run
|
||||
3. Using `deno task build` the build process will be run. This will firstly build pdfjs, copy all the important files over and then do the ~~typical~~ vite (ui) + deno (worker) build.
|
||||
1267
Library/mrmugame/silverbullet-pdf.plug.js
Normal file
1267
Library/mrmugame/silverbullet-pdf.plug.js
Normal file
File diff suppressed because one or more lines are too long
50
Library/silverbulletmd/basic-search/PLUG.md
Normal file
50
Library/silverbulletmd/basic-search/PLUG.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: Library/silverbulletmd/basic-search/PLUG
|
||||
tags: meta/library
|
||||
files:
|
||||
- search.plug.js
|
||||
share.uri: "https://github.com/silverbulletmd/basic-search/blob/main/PLUG.md"
|
||||
share.hash: 59d1e9ad
|
||||
share.mode: pull
|
||||
---
|
||||
# Basic Full Text Search
|
||||
Formerly a built-in plug for SilverBullet, now installable separately for those who don’t want to (for whatever reason) use the far superior [Silversearch](https://github.com/MrMugame/silversearch) instead. Seriously, use Silversearch instead of this.
|
||||
|
||||
No? Alright then.
|
||||
|
||||
## Initial index
|
||||
Perform a `Space: Reindex` after installing this plug.
|
||||
|
||||
## Commands
|
||||
* `Search Space` (`Cmd-Shift-f`/`Ctrl-Shift-f`): performs a full text search across your space.
|
||||
|
||||
# Implementation
|
||||
(part Lua, part Plug)
|
||||
|
||||
```space-lua
|
||||
-- priority: 5
|
||||
command.define {
|
||||
name = "Search Space",
|
||||
key = "Ctrl-Shift-f",
|
||||
mac = "Cmd-Shift-f",
|
||||
run = function()
|
||||
local phrase = editor.prompt "Search for:"
|
||||
if phrase then
|
||||
editor.navigate("search:" .. phrase)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
virtualPage.define {
|
||||
pattern = "search:(.+)",
|
||||
run = function(phrase)
|
||||
-- These are implemented in the plug code
|
||||
local results = search.ftsSearch(phrase)
|
||||
local pageText = "# Search results for '" .. phrase .. "'\n"
|
||||
for r in each(results) do
|
||||
pageText = pageText .. spacelua.interpolate("* [[${r.id}|${r.id}]] (score ${r.score})\n", {r=r})
|
||||
end
|
||||
return pageText
|
||||
end
|
||||
}
|
||||
```
|
||||
1
Library/silverbulletmd/basic-search/search.plug.js
Normal file
1
Library/silverbulletmd/basic-search/search.plug.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user