Initial commit
This commit is contained in:
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;
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user