Library and Repostitory refinements

This commit is contained in:
Zef Hemel
2025-11-12 16:54:55 +01:00
parent f0e6bb243a
commit 391e70139e
10 changed files with 211 additions and 112 deletions
-14
View File
@@ -1,20 +1,6 @@
---
tags: meta/library
version: builtin
files:
# Note: this is for demonstration purposes only. Library/Std ships with SilverBullet and cannot be updated via regular library management.
# Plugs:
- Plugs/core.plug.js
- Plugs/editor.plug.js
- Plugs/emoji.plug.js
- Plugs/image-viewer.plug.js
- Plugs/index.plug.js
- Plugs/search.plug.js
- Plugs/sync.plug.js
- Plugs/tasks.plug.js
# Other pages omitted
---
Welcome to SilverBullet’s standard library. This library (all pages under `Library/Std`) ship with SilverBullet itself. Therefore, you will not find these pages in your space folder, even though they _appear_ to be located there. These pages are all read-only, so you cannot (directly) modify them.
The goal of the standard library is provide a base-level of useful commands, slash commands, page templates and scripts.
+131 -61
View File
@@ -11,7 +11,6 @@ library = {
repositoryTag = "meta/repository"
}
-- Harcoded prefixes
local libraryPrefix = "Library/"
local repositoryPrefix = "Repositories/"
@@ -20,16 +19,21 @@ local repositoryPrefix = "Repositories/"
local librarySchema = {
type = "object",
properties = {
name = schema.string(),
files = schema.array(schema.string())
}
},
required = {"name"}
}
local remoteLibrarySchema = {
type = "object",
properties = {
name = schema.string(),
description = schema.string(),
website = schema.string(),
uri = schema.string(),
}
},
required = {"name", "uri"}
}
-- Repo management commands
@@ -41,11 +45,11 @@ command.define {
return
end
local text = net.readURI(repoUri, {encoding="text/markdown"})
local repoName = editor.prompt("Name:")
if not repoName then
local suggestedName = library.suggestRepoNameFromUri(repoUri)
local repoPage = editor.prompt("Install into:", repositoryPrefix .. suggestedName)
if not repoPage then
return
end
local repoPage = repositoryPrefix .. repoName
if space.pageExists(repoPage) then
editor.flashNotification(repoPage .. " already exists", "error")
return
@@ -81,16 +85,7 @@ command.define {
if not uri then
return
end
local name = editor.prompt("Name (library will be saved under Library/<<Name>>):")
if not name then
return
end
local libPage = libraryPrefix .. name
if space.pageExists(libPage) then
editor.flashNotification(libPage .. " already exists", "error")
return
end
library.install(libPage, uri)
library.install(uri)
editor.flashNotification "Library installed"
reloadEverything()
editor.navigate("Library/Std/Pages/Library Manager")
@@ -100,8 +95,10 @@ command.define {
command.define {
name = "Library: Update All",
run = function()
local installedLibraries = query[[from index.tag(library.libraryTag)]]
for lib in installedLibraries do
for lib in query[[
from index.tag(library.libraryTag)
where _.share and _.share.mode == "pull"
]] do
if library.update(lib.name, false) then
editor.flashNotification("Updated " .. lib.name)
end
@@ -132,7 +129,7 @@ function library.getInstalled(uri)
end
-- Install a library
function library.install(pageName, uri, currentHash)
function library.install(uri, currentHash)
-- Fetch from remote URL
local text = net.readURI(uri, {
encoding = "text/markdown"
@@ -144,12 +141,19 @@ function library.install(pageName, uri, currentHash)
local remoteLibFM = index.extractFrontmatter(text).frontmatter
local remoteHash = share.contentHash(text)
local err = jsonschema.validateObject(librarySchema, remoteLibFM)
if err then
editor.flashNotification("Library frontmatter validation error: " .. err, "error")
return
end
local pageName = remoteLibFM.name
-- If this is a fresh install, don't override existing pages
if not currentHash and space.pageExists(pageName) then
error("Page already exists: " .. pageName)
end
-- Check if update is required based on hash
if currentHash and currentHash == remoteHash then
@@ -157,7 +161,8 @@ function library.install(pageName, uri, currentHash)
end
-- Ok, let's do this
local baseUrl = urlDir(uri)
local sourceBase = urlDir(uri)
local targetBase = urlDir(pageName)
text = share.setFrontmatter({
uri = uri,
hash = remoteHash,
@@ -171,10 +176,10 @@ function library.install(pageName, uri, currentHash)
if remoteLibFM.files then
for file in remoteLibFM.files do
print("Downloading file", file)
local targetPath = pageName .. "/" .. file
local data = net.readURI(baseUrl .. file, {encoding="application/octet-stream"})
local targetPath = targetBase .. file
local data = net.readURI(sourceBase .. file, {encoding="application/octet-stream"})
if not data then
error("Could not fetch file: "..baseUrl..file)
error("Could not fetch file: " .. sourceBase .. file)
end
space.writeFile(targetPath, data)
end
@@ -187,7 +192,7 @@ function library.update(pageName, force)
local text = space.readPage(pageName)
local fm = index.extractFrontmatter(text).frontmatter
if fm.share then
return library.install(pageName, fm.share.uri, fm.share.hash)
return library.install(fm.share.uri, fm.share.hash)
else
print("No 'share' key found in frontmatter for " .. pageName)
end
@@ -216,32 +221,59 @@ end
function library.installedLibrariesWidget()
local rows = {}
for lib in query[[from index.tag "meta/library"]] do
for lib in query[[
from index.tag "meta/library"
]] do
table.insert(rows, dom.tr {
dom.td { "[[" .. lib.name .. "|" .. lib.name .. "]]" },
dom.td {
not lib.share and "" or dom.span{
widgets.button("Update", function()
local updated = library.update(lib.name, true)
if updated then
editor.flashNotification "Updated."
reloadEverything()
else
editor.flashNotification "No update required."
end
end),
widgets.button("Remove", function()
if editor.confirm("Are you sure?") then
library.remove(lib.name)
editor.flashNotification "Done!"
reloadEverything()
end
end)
}
(
not lib.share and ""
) or (
lib.share.mode == "pull" and
dom.span {
widgets.button("Update", function()
local updated = library.update(lib.name, true)
if updated then
editor.flashNotification "Updated."
reloadEverything()
else
editor.flashNotification "No update available."
end
end, { class = "update" }),
widgets.button("Remove", function()
if editor.confirm("Are you sure?") then
library.remove(lib.name)
editor.flashNotification "Done!"
reloadEverything()
end
end, { class = "remove"})
}
) or (
dom.span {
widgets.button("Publish", function()
local updated = library.update(lib.name, true)
if updated then
editor.flashNotification "Published."
reloadEverything()
else
editor.flashNotification "No changes to publish."
end
end, { class = "update" })
}
)
}
})
end
if #rows == 0 then
rows = {
dom.tr {
dom.td { colspan="2", "_Nothing yet_" }
}
}
end
return widget.htmlBlock(dom.table {
class = "manage-library",
dom.thead {
dom.tr {
dom.td {"Library"},
@@ -253,28 +285,35 @@ function library.installedLibrariesWidget()
end
function library.installableLibrariesWidget()
local installableLibraries = query[[from index.tag "meta/library/remote"]]
local rows = {}
for lib in query[[from index.tag "meta/library/remote"]] do
local installed = library.getInstalled(lib.uri)
if not installed then
table.insert(rows, dom.tr {
dom.td { lib.name },
dom.td { "[[" .. lib.page .. "|" .. lib.page .. "]]" },
dom.td {
widgets.button("Install", function()
library.install(library.libPath(lib.name), lib.uri)
editor.flashNotification "Done!"
reloadEverything()
end)
}
})
end
for lib in query[[
from installableLibraries where not library.getInstalled(_.uri)
]] do
table.insert(rows, dom.tr {
dom.td {
lib.website and dom.a {
href = lib.website,
"**" .. lib.name .. "**",
} or "**" .. lib.name .. "**",
" _([[" .. lib.page .. "]])_",
lib.description and dom.p { lib.description } or ""
},
dom.td {
widgets.button("Install", function()
library.install(lib.uri)
editor.flashNotification "Done!"
reloadEverything()
end, { class = "install" })
}
})
end
return widget.htmlBlock(dom.table {
class = "manage-library",
dom.thead {
dom.tr {
dom.td {"Library"},
dom.td {"Repository"},
dom.td {"Action"}
}
},
@@ -303,12 +342,13 @@ function library.installedRepositoriesWidget()
editor.flashNotification "Done!"
reloadEverything()
end
end)
end, { class = "remove" })
} or ""
}
})
end
return widget.htmlBlock(dom.table {
class = "manage-library",
dom.thead {
dom.tr {
dom.td {"Repository"},
@@ -338,10 +378,40 @@ function library.repoName(path)
return path:sub(#repositoryPrefix + 1)
end
function library.suggestRepoNameFromUri(uri)
-- Take the bit after the last :
local uriParts = uri:split(":")
local suggestedName = uriParts[#uriParts]
-- Strip .md
local suggestedName = suggestedName:gsub("%.md$", "")
-- Strip any some generic names
for strippableSuffix in {"/REPO", "/silverbullet-libraries"} do
suggestedName = suggestedName:gsub(strippableSuffix .. "$", "")
end
-- Strip branch names (@something)
suggestedName = suggestedName:gsub("@.+$", "")
-- Take the the last bit
local parts = suggestedName:split("/")
return parts[#parts]
end
function reloadEverything()
mq.awaitEmptyQueue("indexQueue")
editor.reloadConfigAndCommands()
codeWidget.refreshAll()
end
```
# Styles
```space-style
table.manage-library td {
vertical-align: top;
}
table.manage-library thead td:last-child {
text-align: right;
}
table.manage-library tbody td:last-child {
text-align: right;
}
```
@@ -55,17 +55,16 @@ function share.sharePage(name)
print("Current share frontmatter", m)
local remoteText = net.readURI(m.uri, {encoding="text/markdown"})
if not remoteText then
error("Could not read " .. m.uri)
end
local mode = m.mode or "push"
local oldHash = m.hash
local newLocalHash = share.contentHash(text)
local newHash = newLocalHash
if mode == "sync" then
local remoteText = net.readURI(m.uri, {encoding="text/markdown"})
if not remoteText then
error("Could not read " .. m.uri)
end
-- Two-way sync mode
local newRemoteHash = share.contentHash(remoteText)
-- Sync cases
@@ -92,6 +91,10 @@ function share.sharePage(name)
net.writeURI(m.uri, share.cleanFrontmatter(text))
end
elseif mode == "pull" then
local remoteText = net.readURI(m.uri, {encoding="text/markdown"})
if not remoteText then
error("Could not read " .. m.uri)
end
local newRemoteHash = share.contentHash(remoteText)
if oldHash == newRemoteHash then
print("No remote changes: nothing to do")
@@ -1,12 +1,14 @@
#meta
The library manager allows you to manage _repositories_ as well as _libraries_.
From here you can extend SilverBullet with additional functionality throught he use of [libraries](https://silverbullet.md/Libraries).
# Repositories
${widgets.commandButton "Library: Add Repository"} ${widgets.commandButton "Library: Update All Repositories"}
${widgets.commandButton("Add from URI", "Library: Add Repository")} ${widgets.commandButton("Update All", "Library: Update All Repositories")}
${library.installedRepositoriesWidget()}
To discover more repositories, [have a look at the community forums](https://community.silverbullet.md/c/plugs-libraries/14).
# Installed libraries
${widgets.commandButton "Library: Install"} ${widgets.commandButton "Library: Update All"} ${widgets.commandButton "Plugs: Reload"}
${widgets.commandButton("Update All", "Library: Update All")} ${widgets.commandButton("Reload all plugs", "Plugs: Reload")}
${library.installedLibrariesWidget()}
# Available libraries
${widgets.commandButton("Install from URI", "Library: Install")}
${library.installableLibrariesWidget()}
+13 -4
View File
@@ -41,11 +41,20 @@ config.set("std.widgets.linkedTasks.enabled", false)
## Buttons
```space-lua
-- priority: 10
function widgets.button(text, callback)
return widget.html(dom.button {
onclick=callback,
function widgets.button(text, callback, attrs)
local buttonEl = {
onclick = callback,
text
})
}
-- attrs can be used for additional customization
if attrs then
for k, v in pairs(attrs) do
buttonEl[k] = v
end
end
return widget.html(dom.button(buttonEl))
end
function widgets.commandButton(text, commandName, args)
+8 -2
View File
@@ -3,12 +3,18 @@
Here are a few vetted libraries to get you started.
```#meta/library/remote
name: mermaid
uri: github:silverbulletmd/silverbullet-mermaid/README.md
name: silverbullet-mermaid
description: Mermaid diagram support for SilverBullet
website: https://github.com/silverbulletmd/silverbullet-mermaid
uri: github:silverbulletmd/silverbullet-mermaid/REPO.md
---
name: MrMugame/silversearch
description: Powerful full-text search engine for SilverBullet
website: https://github.com/MrMugame/silversearch
uri: ghr:MrMugame/silversearch/PLUG.md
---
name: MrMugame/silverbullet-pdf
website: https://github.com/MrMugame/silverbullet-pdf
description: PDF Viewer for Silverbullet
uri: ghr:MrMugame/silverbullet-pdf/PLUG.md
```
+14 -2
View File
@@ -9,13 +9,16 @@ import type {
LintEvent,
} from "@silverbulletmd/silverbullet/type/client";
export async function lintYAML({ tree }: LintEvent): Promise<LintDiagnostic[]> {
export async function lintYAML(
{ tree, name }: LintEvent,
): Promise<LintDiagnostic[]> {
const diagnostics: LintDiagnostic[] = [];
await traverseTreeAsync(tree, async (node) => {
if (node.type === "FrontMatterCode") {
const lintResult = await lintYaml(
renderToText(node),
node.from!,
name,
);
if (lintResult) {
diagnostics.push(lintResult);
@@ -57,9 +60,18 @@ const errorRegex = /\((\d+):(\d+)\)/;
async function lintYaml(
yamlText: string,
startPos: number,
pageName?: string,
): Promise<LintDiagnostic | undefined> {
try {
await YAML.parse(yamlText);
const parsed = await YAML.parse(yamlText);
if (pageName && parsed.name && parsed.name != pageName) {
return {
from: startPos,
to: startPos + yamlText.length,
severity: "error",
message: "'name' attribute has to match page name",
};
}
} catch (e: any) {
const errorMatch = errorRegex.exec(e.message);
if (errorMatch) {
+3 -3
View File
@@ -3,7 +3,7 @@
SilverBullet is designed to be [[Extensions|extensible]]. In fact, a lot of functionality in SilverBullet is implemented through [[Space Lua]] embedded in [[Meta Pages]], other functionality is implemented using [[Plugs]].
A big part of the fun of SilverBullet is hacking around in it, building your own extensions. Libraries are the way to [[Share]] those extensions. Both for you to publish your own libraries, but also to pull in other people’s creations.
A big part of the fun of SilverBullet is building your own extensions, bit or small. Libraries are the way to [[Share]] those extensions. Both for you to [[Libraries/Development|publish your own]] libraries, and to pull in other people’s creations.
There are a few things at play here:
@@ -13,9 +13,9 @@ There are a few things at play here:
[[Share]] is the general-purpose mechanism built into SilverBullet to both _push_, _pull_ and _sync_ content in your space with the outside world. In the context of Libraries this mechanism is used (partially under the hood, but you’ll recognize traces of it) both for you to _publish_ your libraries somewhere, as well as to _install_ and _update_ other people’s creations.
To make discovery and installation of libraries easier, SilverBullet includes a basic [[Library Manager]].
To make discovery and installation of libraries easier, SilverBullet includes a [[Library Manager]].
Libraries are kept under the `Library/` prefix in your space.
By convention, libraries are kept under the `Library/` prefix (folder) in your space.
# What’s in a library
Here are some things that a library may provide:
+28 -17
View File
@@ -1,9 +1,9 @@
To develop your own SilverBullet library follow the following steps:
To develop your own SilverBullet library follow these steps:
# Write the code
Put all functionality you like in a [[Meta Pages|Meta Page]] somewhere under `Library/`, e.g. `Library/My Library`.
Put all functionality you like in a [[Meta Pages|Meta Page]] somewhere under `Library/`. For namespacing purposes it’s good form to put your (Github) username in the path as well, so create, for instance `Library/myuser/My Library`.
These can, for instance, contain:
A library can contain, for instance:
* [[Space Lua]]
* [[Space Style]]
@@ -15,15 +15,19 @@ However, you can also include
Decorate your library page with some [[Frontmatter]], at the very least:
```
---
name: Library/myuser/My Library
tags: meta/library
---
```
If you want to distribute additional pages or files with the library (such as `.plug.js` files, or additional pages like page templates and slash commands) make sure that they are kept at the same folder level as your library page. For instance if your library page is `Library/My Library` you can create an additional `Library/Page Template` or have a `Library/myplug.plug.js` and then mark them for distribution by adding them to the `files` key in your frontmatter, e.g.
> **note** Important
> The `name` key _has_ to match your page’s full (path) name. Otherwise a validation error will appear on the frontmatter. When another user installs your library, they will install it to the location specified here.
If you want to distribute additional pages or files with the library (such as `.plug.js` files) make sure that they are kept at the same folder level as your library page. For instance if your library page is `Library/myuser/My Library` you can have a `Library/myuser/myplug.plug.js` and then mark it for distribution by adding it to the `files` key in your frontmatter, e.g.
```
---
tags: meta/library
files:
- Page Template.md
- myplug.plug.js
---
```
@@ -33,40 +37,47 @@ At this stage you can put your library file anywhere you like with a URL. Howeve
For the purposes of this example, let’s use Github. Create a [[^Library/Std/Infrastructure/Github]] token and configure it as specified in the [[^Library/Std/Infrastructure/Github#Configuration|instructions]].
[[^Library/Std/Infrastructure/Github]] share support offers two options: gists and Github repo files. Let’s use a Github repo.
[[^Library/Std/Infrastructure/Github]] share support offers two options: Gists and Github repo files. Let’s use a Github repo.
[Create a new github repo](https://github.com/new), you can name it something like `silverbullet-libraries`.
In your library to be shared, run the ${widgets.commandButton "Share: Page"} command, then select “Github file”. This will ask for a few things:
1. The repo name: enter your `user/repo` here
2. The branch name: you can keep this as `main`
3. The file name: while this defaults to using a `Library/` prefix, remove that and just name it e.g. `My Library.md`
3. The file name: while this defaults to using your full page, remove that and just name it e.g. `My Library.md`
4. A commit message: pick whatever you like
If all went well, an initial version of your page should now be uploaded to github, check your repo page. You’ll notice that a few [[Share]] related frontmatter keys were set in your local copy (but not in the remote version). You can now make further changes to your page and run the `Share: Page` command again (Cmd-p or Ctrl-p by default) and after entering another commit message your page will be pushed again. This is how you publish new version for other people to use.
If all went well, an initial version of your page should now be uploaded to github, check your repo page.
If your library consisted of multiple files, do the same for the other files. If you rely on plugs, unfortunately you have to push those in some other way.
You’ll notice that a few [[Share]] related frontmatter keys were set in your local copy (but not in the remote version). You can now make further changes to your page and run the `Share: Page` command again (Cmd-p or Ctrl-p by default) and after entering another commit message your page will be pushed again. This is how you publish new version for other people to use.
Your library is now ready to install, you can test this by running ${widgets.commandButton "Library: Install"}. As URI you can either install the full Github URL of the markdown file, or use the value of what is generated under `share.uri` in your library’s frontmatter. Give it a temporary name, to test it all works. Then delete the page again.
If you used `files` to include additional assets, you have to commit those to the repository through other means.
Your library is now ready to install, you can test this by running ${widgets.commandButton "Library: Install"} in a fresh space. As URI you can either user the full Github URL of the markdown file, or use the value of what is generated under `share.uri` in your library’s frontmatter.
You can now [broadcast that your library is ready to install](https://community.silverbullet.md/c/plugs-libraries/14)!
# Create a repository
If you develop a few libraries and keep adding more, it may be good to make them a bit more discoverable. To do so, you can group them in a [[Repositories|Repository]] (not to be confused with a Github repository).
If you develop a few libraries, it may be good to make them more discoverable. To do so, you can group them in a [[Repositories|Repository]] (not to be confused with a Github repository).
For this, create another page, this time under `Repository/`, e.g. `Repository/<<yourname>>`. In it, put a list of all your awesome libraries, encoded in a [[Objects#data|data object]] as follows.
For this, create another page, this time under `Repository/`, e.g. `Repository/myuser` and tag it with `#meta/repository`. In the page body, put a list of all your libraries, encoded in a [[Objects#data|data object]] as follows.
~~~
```#meta/library/remote
name: yourname/Library1
uri: github:username/silverbullet-libraries/Library1.md
name: myuser/Library1
description: This is my first awesome library
website: http://url.com/to/docs
uri: github:myuser/silverbullet-libraries/Library1.md
---
name: yourname/Library2
uri: github:username/silverbullet-libraries/Library2.md
name: myuser/Library2
description: This is my second awesome library
uri: github:myuser/silverbullet-libraries/Library2.md
```
~~~
Note that it’s a good idea to namespace your libraries a bit, hence the `yourname/` prefix in _name_.
Required keys are:
* `name`: a descriptive name of your library (this may include your username to clearly distinguish it from other, similar libraries). **Note**: this name is _not_ used as an installation location, the installation location is determined by the `name` key in the library itself.
* `uri`: The URI to install the library from.
Now share this repo page using [[Share]] to the same github repo (or any other, it doesn’t really matter), and call it `REPO.md` (by convention).
+1 -1
View File
@@ -8,6 +8,6 @@ Repositories are a collection of _pointers_ to [[Libraries]] to install. Their p
By default, SilverBullet ships with one repository ([[^Repositories/Std]]) but more can be installed via the [[Library Manager]].
Repositories are kept under the `Repository/` prefix in your space.
Repositories are kept under the `Repository/` prefix in your space by convention.
Repositories are implemented on top of a few SilverBullet mechanisms, specifically they are simply [[Meta Pages]] tagged with `#meta/repository` that contain [[Objects#data]] objects tagged with `#meta/library/remote` with a `name` and `uri` attribute. Libraries are kept up to date using the [[Share]] mechanism.