diff --git a/libraries/Library/Std.md b/libraries/Library/Std.md index 125c5b82..9c821d45 100644 --- a/libraries/Library/Std.md +++ b/libraries/Library/Std.md @@ -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. diff --git a/libraries/Library/Std/Infrastructure/Library.md b/libraries/Library/Std/Infrastructure/Library.md index 40294ec8..da737a98 100644 --- a/libraries/Library/Std/Infrastructure/Library.md +++ b/libraries/Library/Std/Infrastructure/Library.md @@ -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/<>):") - 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; +} ``` diff --git a/libraries/Library/Std/Infrastructure/Share.md b/libraries/Library/Std/Infrastructure/Share.md index b0d450c3..9a735d24 100644 --- a/libraries/Library/Std/Infrastructure/Share.md +++ b/libraries/Library/Std/Infrastructure/Share.md @@ -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") diff --git a/libraries/Library/Std/Pages/Library Manager.md b/libraries/Library/Std/Pages/Library Manager.md index 43592922..a219c338 100644 --- a/libraries/Library/Std/Pages/Library Manager.md +++ b/libraries/Library/Std/Pages/Library Manager.md @@ -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()} diff --git a/libraries/Library/Std/Widgets/Widgets.md b/libraries/Library/Std/Widgets/Widgets.md index 48419457..417ad16d 100644 --- a/libraries/Library/Std/Widgets/Widgets.md +++ b/libraries/Library/Std/Widgets/Widgets.md @@ -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) diff --git a/libraries/Repositories/Std.md b/libraries/Repositories/Std.md index 03fb8f29..32b5dba9 100644 --- a/libraries/Repositories/Std.md +++ b/libraries/Repositories/Std.md @@ -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 ``` diff --git a/plugs/index/lint.ts b/plugs/index/lint.ts index 896ade77..4803ec05 100644 --- a/plugs/index/lint.ts +++ b/plugs/index/lint.ts @@ -9,13 +9,16 @@ import type { LintEvent, } from "@silverbulletmd/silverbullet/type/client"; -export async function lintYAML({ tree }: LintEvent): Promise { +export async function lintYAML( + { tree, name }: LintEvent, +): Promise { 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 { 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) { diff --git a/website/Libraries.md b/website/Libraries.md index 1d5dc158..c2a20d9e 100644 --- a/website/Libraries.md +++ b/website/Libraries.md @@ -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: diff --git a/website/Libraries/Development.md b/website/Libraries/Development.md index 3c7e3a3d..0260dff8 100644 --- a/website/Libraries/Development.md +++ b/website/Libraries/Development.md @@ -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/<>`. 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). diff --git a/website/Repositories.md b/website/Repositories.md index e27cf97e..166adfe4 100644 --- a/website/Repositories.md +++ b/website/Repositories.md @@ -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. \ No newline at end of file