diff --git a/common/PAGE_TEMPLATES.ts b/common/PAGE_TEMPLATES.ts index ade1d16f..7386de4d 100644 --- a/common/PAGE_TEMPLATES.ts +++ b/common/PAGE_TEMPLATES.ts @@ -1,19 +1,20 @@ -export const SETTINGS_TEMPLATE = `--- -tags: meta ---- -This page contains settings for configuring SilverBullet and its plugs. A list of built-in settings [[!silverbullet.md/SETTINGS|can be found here]]. +export const SETTINGS_TEMPLATE = `#meta + +This page contains settings for configuring SilverBullet. A list of built-in settings [[!silverbullet.md/SETTINGS|can be found here]]. + +To update \`libraries\` specified here: {[Libraries: Update]} \`\`\`yaml indexPage: index +libraries: +- source: "[[!silverbullet.md/Library/Core/*]]" \`\`\` `; export const INDEX_TEMPLATE = `This is the index page of your fresh SilverBullet space. It is the default page that is loaded when you open a space. In addition, there is also a [[SETTINGS]] page that contains settings for configuring SilverBullet. -For your convenience we're including an on-boarding live template below. Enjoy! +For your convenience we're embedding some on-boarding info below. Feel free to delete it once you're done reading it. -\`\`\`include -raw: "[[!silverbullet.md/Getting Started]]" -\`\`\` +![[!silverbullet.md/Getting Started]] `; diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index 2086fc5b..2e63fadb 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -4,7 +4,7 @@ import { FileMeta, PageMeta, } from "$sb/types.ts"; -import { cacheFileListing } from "../federation/federation.ts"; +import { listFilesCached } from "../federation/federation.ts"; import { queryObjects } from "../index/plug_api.ts"; import { folderName } from "$sb/lib/resolve.ts"; @@ -39,12 +39,15 @@ export async function pageComplete(completeEvent: CompleteEvent) { node.startsWith("FencedCode:template") ) ) { - // Include both pages and templates in page completion in ```include and ```template blocks + // Include both pages and meta in page completion in ```include and ```template blocks allPages = await queryObjects("page", {}, 5); } else { - // Otherwise, just complete non-template pages + // Otherwise, just complete non-meta pages allPages = await queryObjects("page", { - filter: ["!=", ["attr", "tags"], ["string", "template"]], + filter: ["and", ["!=", ["attr", "tags"], ["string", "template"]], ["!=", [ + "attr", + "tags", + ], ["string", "meta"]]], }, 5); // and attachments allPages = allPages.concat( @@ -64,7 +67,7 @@ export async function pageComplete(completeEvent: CompleteEvent) { // Yep const domain = prefix.split("/")[0]; // Cached listing - const federationPages = (await cacheFileListing(domain)).filter((fm) => + const federationPages = (await listFilesCached(domain)).filter((fm) => fm.name.endsWith(".md") ).map(fileMetaToPageMeta); if (federationPages.length > 0) { diff --git a/plugs/federation/federation.plug.yaml b/plugs/federation/federation.plug.yaml index 72ed6c5e..58583631 100644 --- a/plugs/federation/federation.plug.yaml +++ b/plugs/federation/federation.plug.yaml @@ -29,9 +29,12 @@ functions: pattern: "!.+" operation: getFileMeta - # Library management - importLibraryCommand: - path: library.ts:importLibraryCommand + # Library management commands + updateLibrariesCommand: + path: library.ts:updateLibrariesCommand command: - name: "Library: Import" + name: "Libraries: Update" requireMode: rw + updateLibraries: + path: library.ts:updateLibraries + env: server diff --git a/plugs/federation/federation.ts b/plugs/federation/federation.ts index 80695fb3..fe0bccd6 100644 --- a/plugs/federation/federation.ts +++ b/plugs/federation/federation.ts @@ -3,6 +3,7 @@ import { federatedPathToUrl } from "$sb/lib/resolve.ts"; import { readFederationConfigs } from "./config.ts"; import { datastore } from "$sb/syscalls.ts"; import type { FileMeta } from "../../plug-api/types.ts"; +import { wildcardPathToRegex } from "./util.ts"; async function responseToFileMeta( r: Response, @@ -31,7 +32,7 @@ async function responseToFileMeta( } const fileListingPrefixCacheKey = `federationListCache`; -const listingCacheTimeout = 1000 * 30; +const listingCacheTimeout = 1000 * 5; const listingFetchTimeout = 2000; type FileListingCacheEntry = { @@ -44,7 +45,7 @@ export async function listFiles(): Promise { // Fetch them all in parallel try { await Promise.all((await readFederationConfigs()).map(async (config) => { - const items = await cacheFileListing(config.uri); + const items = await listFilesCached(config.uri); fileMetas = fileMetas.concat(items); })); @@ -56,63 +57,78 @@ export async function listFiles(): Promise { } } -export async function cacheFileListing(uri: string): Promise { +export async function listFilesCached( + uri: string, + supportWildcards = false, +): Promise { + const uriParts = uri.split("/"); + const rootUri = uriParts[0]; + const prefix = uriParts.slice(1).join("/"); + console.log( + "Fetching listing from federated", + rootUri, + "with prefix", + prefix, + ); const cachedListing = await datastore.get( - [fileListingPrefixCacheKey, uri], + [fileListingPrefixCacheKey, rootUri], ) as FileListingCacheEntry; + let items: FileMeta[] = []; if ( cachedListing && cachedListing.lastUpdated > Date.now() - listingCacheTimeout ) { - // console.info("Using cached listing", cachedListing); - return cachedListing.items; - } - console.log("Fetching listing from federated", uri); - const uriParts = uri.split("/"); - const rootUri = uriParts[0]; - const prefix = uriParts.slice(1).join("/"); - const indexUrl = `${federatedPathToUrl(rootUri)}/index.json`; - try { - const fetchController = new AbortController(); - const timeout = setTimeout( - () => fetchController.abort(), - listingFetchTimeout, - ); + console.info("Using cached listing", cachedListing.items.length); + items = cachedListing.items; + } else { + const indexUrl = `${federatedPathToUrl(rootUri)}/index.json`; + try { + const fetchController = new AbortController(); + const timeout = setTimeout( + () => fetchController.abort(), + listingFetchTimeout, + ); - const r = await nativeFetch(indexUrl, { - method: "GET", - headers: { - "X-Sync-Mode": "true", - "Cache-Control": "no-cache", - }, - signal: fetchController.signal, - }); - clearTimeout(timeout); + const r = await nativeFetch(indexUrl, { + method: "GET", + headers: { + "X-Sync-Mode": "true", + "Cache-Control": "no-cache", + }, + signal: fetchController.signal, + }); + clearTimeout(timeout); - if (r.status !== 200) { - throw new Error(`Got status ${r.status}`); - } - const jsonResult = await r.json(); - const items: FileMeta[] = jsonResult.filter((meta: FileMeta) => - meta.name.startsWith(prefix) - ).map((meta: FileMeta) => ({ - ...meta, - perm: "ro", - name: `${rootUri}/${meta.name}`, - })); - await datastore.set([fileListingPrefixCacheKey, uri], { - items, - lastUpdated: Date.now(), - } as FileListingCacheEntry); - return items; - } catch (e: any) { - console.error("Failed to process", indexUrl, e); - if (cachedListing) { - console.info("Using cached listing"); - return cachedListing.items; + if (r.status !== 200) { + throw new Error(`Got status ${r.status}`); + } + const jsonResult = await r.json(); + // Transform them a little bit + items = jsonResult.map((meta: FileMeta) => ({ + ...meta, + perm: "ro", + name: `${rootUri}/${meta.name}`, + })); + // Cache the entire listing + await datastore.set([fileListingPrefixCacheKey, rootUri], { + items, + lastUpdated: Date.now(), + } as FileListingCacheEntry); + } catch (e: any) { + console.error("Failed to process", indexUrl, e); + if (cachedListing) { + console.info("Using cached listing"); + return cachedListing.items; + } } } - return []; + // And then filter based on prefix before returning + if (!supportWildcards) { + return items.filter((meta: FileMeta) => meta.name.startsWith(uri)); + } else { + const prefixRegex = wildcardPathToRegex(uri); + return items.filter((meta) => prefixRegex.test(meta.name)); + } } export async function readFile( diff --git a/plugs/federation/library.ts b/plugs/federation/library.ts index b744765c..71927dbc 100644 --- a/plugs/federation/library.ts +++ b/plugs/federation/library.ts @@ -1,57 +1,116 @@ import { editor, space } from "$sb/syscalls.ts"; -import { cacheFileListing, readFile } from "./federation.ts"; +import { readSetting } from "$sb/lib/settings_page.ts"; +import { listFilesCached, readFile } from "./federation.ts"; +import { parsePageRef } from "$sb/lib/page_ref.ts"; +import { invokeFunction } from "$sb/syscalls/system.ts"; +import { federatedPathToLocalPath, wildcardPathToRegex } from "./util.ts"; +import { confirm } from "$sb/syscalls/editor.ts"; -export async function importLibraryCommand(_def: any, uri?: string) { - if (!uri) { - uri = await editor.prompt("Import library (federation URL):"); - } - if (!uri) { - return; - } - uri = uri.trim(); - if (!uri.startsWith("!")) { - uri = `!${uri}`; - } - const allTemplates = (await cacheFileListing(uri)).filter((f) => - f.name.endsWith(".md") - ); +type LibraryDef = { + source: string; + exclude?: string[]; +}; + +export async function updateLibrariesCommand() { if ( - !await editor.confirm( - `You are about to import ${allTemplates.length} templates, want to do this?`, + await confirm( + "Are you sure you want to update all libraries (as specified in SETTINGS)?", ) ) { - return; + await editor.flashNotification("Updating all libraries..."); + const updateStats: UpdateStats = await invokeFunction( + "federation.updateLibraries", + ); + await editor.reloadSettingsAndCommands(); + await editor.flashNotification( + `Updated ${updateStats.libraries} libraries containing a total of ${updateStats.items} items.`, + ); } - for (const template of allTemplates) { - // Clean up file path - let pageName = template.name.replace(/\.md$/, ""); - // Remove the federation part - const pieces = pageName.split("/"); - pageName = pieces.slice(1).join("/"); +} - // Fetch the file - const buf = (await readFile(template.name)).data; +type UpdateStats = { + libraries: number; + items: number; +}; - try { - // Check if it already exists - await space.getPageMeta(pageName); +// Run on the server for efficiency and CORS avoidance +export async function updateLibraries(): Promise { + const updateStats: UpdateStats = { libraries: 0, items: 0 }; + const libraries = (await readSetting("libraries", [])) as LibraryDef[]; + for (const lib of libraries) { + if (!lib.source) { + console.warn("Library source not set, skipping", lib); + continue; + } + const pageUri = parsePageRef(lib.source).page; - if ( - !await editor.confirm( - `Page ${pageName} already exists, are you sure you want to override it?`, - ) - ) { - continue; - } - } catch { - // Expected + if (!pageUri.startsWith("!")) { + console.warn( + "Library source must be a federated page, skipping", + pageUri, + ); + continue; } - // Write to local space - await space.writePage(pageName, new TextDecoder().decode(buf)); + console.log("Now updating library", pageUri); - await editor.flashNotification(`Imported ${pageName}`); + const localLibraryPath = federatedPathToLocalPath(pageUri); + + // Sanity check the `source` pattern to avoid disaster (like wiping out the whole space) + if (!/^Library\/.+/.test(localLibraryPath)) { + console.warn( + "Skipping library", + pageUri, + "as it does not start with Library/", + ); + continue; + } + + // Fetch new list of pages + let newPages = await listFilesCached(pageUri, true); + console.log("All pages", newPages.length); + if (lib.exclude) { + for (const exclude of lib.exclude) { + const excludeUri = parsePageRef(exclude).page; + const excludeRegex = wildcardPathToRegex(excludeUri + ".md"); + newPages = newPages.filter((p) => { + if (excludeRegex.test(p.name)) { + console.info("Excluding", p.name); + return false; + } + return true; + }); + } + } + + // Compile existing page list in local space (to be removed) + const localPages = await space.listPages(); + const localSourceRegex = wildcardPathToRegex(localLibraryPath); + + // Remove pages that match the source pattern, but in their "local" form + const pagesToRemove = localPages.filter((p) => + localSourceRegex.test(p.name) + ); + console.log("Pages to remove", pagesToRemove.length); + for (const page of pagesToRemove) { + console.info("Deleting", page.name); + await space.deletePage(page.name); + } + + // Import the new pages + for (const page of newPages) { + console.info("Importing", page.name); + // Fetch the file + const buf = (await readFile(page.name)).data; + + // Write to local space + await space.writeFile(federatedPathToLocalPath(page.name), buf); + + updateStats.items++; + } + + updateStats.libraries++; + console.log("Done with library", pageUri); } - await editor.reloadSettingsAndCommands(); - await editor.flashNotification("Import complete!"); + return updateStats; } diff --git a/plugs/federation/util.test.ts b/plugs/federation/util.test.ts new file mode 100644 index 00000000..41dd27f6 --- /dev/null +++ b/plugs/federation/util.test.ts @@ -0,0 +1,20 @@ +import { assert, assertEquals } from "$std/testing/asserts.ts"; +import { federatedPathToLocalPath, wildcardPathToRegex } from "./util.ts"; + +Deno.test("Test wildcardPathToRegex", () => { + assert(wildcardPathToRegex("test").test("test")); + assert(wildcardPathToRegex("test").test("test.md")); + assert(wildcardPathToRegex("test*").test("test")); + assert(wildcardPathToRegex("test/*").test("test/bla")); + assert(wildcardPathToRegex("test/*").test("test/bla.md")); + assert(wildcardPathToRegex("test/*").test("test/bla/bla")); + assert(!wildcardPathToRegex("test/*").test("tests/bla/bla")); +}); + +Deno.test("Test federatedPathToLocalPath", () => { + assertEquals(federatedPathToLocalPath("!silverbullet.md"), ""); + assertEquals( + federatedPathToLocalPath("!silverbullet.md/Library/Core/test"), + "Library/Core/test", + ); +}); diff --git a/plugs/federation/util.ts b/plugs/federation/util.ts new file mode 100644 index 00000000..ebe3fedf --- /dev/null +++ b/plugs/federation/util.ts @@ -0,0 +1,14 @@ +export function wildcardPathToRegex(pattern: string): RegExp { + // Escape special characters in the pattern except for the wildcard "*" + const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); + + // Replace the wildcard "*" with ".*" to match any character sequence + const regexPattern = escapedPattern.replace(/\*/g, ".*"); + + // Create a new regular expression with the converted pattern + return new RegExp(`^${regexPattern}(\\.md)?$`); +} + +export function federatedPathToLocalPath(path: string): string { + return path.split("/").slice(1).join("/"); +} diff --git a/server/server_system.ts b/server/server_system.ts index dd0931ee..7861e215 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -117,7 +117,7 @@ export class ServerSystem extends CommonSystem { this.eventHook.addLocalListener( "file:spaceSnapshotted", (snapshot: Record) => { - console.log("Space snapshot updated"); + // console.log("Space snapshot updated"); return this.ds.set(["$spaceCache"], snapshot); }, ); diff --git a/web/cm_plugins/util.ts b/web/cm_plugins/util.ts index d9211300..007e1c4c 100644 --- a/web/cm_plugins/util.ts +++ b/web/cm_plugins/util.ts @@ -9,6 +9,7 @@ type LinkOptions = { href?: string; title: string; cssClass: string; + from: number; callback: (e: MouseEvent) => void; }; export class LinkWidget extends WidgetType { @@ -56,6 +57,7 @@ export class LinkWidget extends WidgetType { eq(other: WidgetType): boolean { return other instanceof LinkWidget && + this.options.from === other.options.from && this.options.text === other.options.text && this.options.href === other.options.href && this.options.title === other.options.title; diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index 42c19e6c..797e633c 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -83,6 +83,7 @@ export function cleanWikiLinkPlugin(client: Client) { cssClass: fileExists ? "sb-wiki-link-page" : "sb-wiki-link-page-missing", + from, callback: (e) => { if (e.altKey) { // Move cursor into the link diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index cfd0aa70..571643c8 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -7,21 +7,22 @@ release. _These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._ * The old **Template Picker** has now been rebranded to [[Meta Picker]] and surfaces pages in your space tagged as `#template` or `#meta`. Read more about this in [[Meta Pages]]. -* [[Transclusion]] has now been implemented, allowing inline embeddings of other pages as well as images (by onespaceman) using the convenient `![[link]]` syntax. +* [[Transclusions]] has now been implemented, allowing inline embedding of other pages as well as images (by onespaceman) using the convenient `![[link]]` syntax. +* [[Libraries]] management has been rethought. You can now decoratively specify them in [[SETTINGS]] and keep them up to date with the {[Libraries: Update]} command. * For new spaces, the default [[SETTINGS]] page is now tagged with `#meta`, which means it will only appear in the [[Meta Picker]]. There is also a new {[Navigate: Open SETTINGS]} command (bound to `Ctrl-,` and `Cmd-,`). * Attachments are now indexed, and smartly moved when pages are renamed (by onespaceman) * Images can now be resized: [[Attachments#Embedding]] (initial work done by [Florent](https://github.com/silverbulletmd/silverbullet/pull/833), later adapted by onespaceman) * To make pure reading and browsing on touch devices a nicer experience, there is now a new **edit toggle** (top right). When _disabled_, you switch to _reader mode_ which makes sure your software keyboard doesn’t block the screen when navigating your space. This button is only visible on mobile devices (no physical keyboard attached) only. Can be disabled via the `hideEditButton` [[SETTINGS]] (and is disabled on this website, that’s why you don’t see it). * Super^script^ and Sub~script~ are now supported (by [MrMugame](https://github.com/silverbulletmd/silverbullet/pull/879)) * Added a {[Delete Line]} command (by [pihentagy](https://github.com/silverbulletmd/silverbullet/pull/866)) -* The `#boot` PWA loading the last opened page feature is back (put `#boot` at the end of your SilverBullet URL to auto load the last opened page) * Improved selection behavior (by [MrMugame](https://github.com/silverbulletmd/silverbullet/pull/904)) * Hide `\` escapes in [[Live Preview]] (by [MrMugame](https://github.com/silverbulletmd/silverbullet/pull/901)) * Added Erlang [[Markdown/Syntax Highlighting]] * Dates (formatted as e.g. `2023-07-01` or `2023-07-01 23:33`) in [[Frontmatter]] are now converted into strings (rather than empty objects) +* **Breaking change:** The legacy handlebars template syntax that supported `{{escapeRegex "something"}}` function calls (rather than `{{escapeRegex("something")}}`) has now been removed. * `task` and `item` objects now have an additional `text` attribute that contains the full text of the item and/or task, with any [[Attributes]] and [[Tags]] intact (whereas they are removed from `name`) +* The `#boot` PWA loading the last opened page feature is back (put `#boot` at the end of your SilverBullet URL to auto load the last opened page) * Numerous other bug fixes (thanks MrMugame and onespaceman) - --- ## 0.7.7 diff --git a/website/Federation.md b/website/Federation.md index c55a19e3..ff5eab2b 100644 --- a/website/Federation.md +++ b/website/Federation.md @@ -1,28 +1,25 @@ -Federation enables _browsing_, and _synchronizing_ (parts of) spaces _outside_ the user’s space into your SilverBullet client. +Federation enables _browsing_ content from spaces _outside_ the user’s space, specified by quasi-URLs in the shape of `!domain.tld/path`. An example would be: [[!silverbullet.md/CHANGELOG]]. This enables a few things: -* **Linking and browsing** to other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]). For instance the [[!silverbullet.md/CHANGELOG|SilverBullet CHANGELOG]] without leaving the comfort of your own SilverBullet client. -* **Reusing** content from externally hosted sources, such as: - * [[Libraries]] synchronization. By federating with `silverbullet.md/Library/Core`, you will get you access to the templates hosted there without copying ({[Library: Import]}‘ing) them and automatically pull in the latest versions. - * _Data_: such as tasks, item, data hosted elsewhere that you want to query from your own space. +* **Browsing** other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]) within the comfort of your own SilverBullet client. One use case of this is [[Transclusions|transcluding]] the [[Getting Started]] page in the user’s automatically generated index page when setting up a fresh space. +* **Referencing** other spaces for other purposes, which is leveraged in [[Libraries]]. -**Note:** Federation does not support authentication yet, so all federated spaces need to be unauthenticated and will be _read-only_. +# How it works +Effectively, any page [[Links|link]] starting with `!` is rewritten as follows: -## Browsing -Browsing other publicly hosted spaces is as simple as navigating to a page starting with `!` such as [[!silverbullet.md/CHANGELOG]]. +* Replace the initial `!` with `https://` +* Append `.md` at the end -## Federating -To synchronize federated content into your client, you need to list these URIs in your [[SETTINGS]] under the `federate` key. For instance: +The resulting URL is then fetched and displayed in the editor in read-only mode. This means that you can navigate to _any markdown_ file on the (public) Internet. -```yaml -federate: -- uri: silverbullet.md/Library/Core/ -``` +For example: `https://raw.githubusercontent.com/silverbulletmd/silverbullet/main/README.md` +Can be written to federation syntax as follows: `!raw.githubusercontent.com/silverbulletmd/silverbullet/main/README` +And used as a link: [[!raw.githubusercontent.com/silverbulletmd/silverbullet/main/README]] -This will synchronize all content under `!silverbullet.md` with a `Library/Core/` prefix (so all templates hosted there) locally. +If the target server supports the SilverBullet [[API]] (specifically its `/index.json` endpoint), page completion will be provided as well. -Currently, content can only be synchronized in read-only mode, so you can not edit the synchronized files. This will likely change in the future. +Upon fetching of the page content, a best effort attempt will be made to rewrite any local page links in the page to the appropriate federated paths. ## Hosting -Tooling to make hosting public spaces is still a work in progress. \ No newline at end of file +Tooling to make hosting public spaces easier is still a work in progress. The way it is enabled on `silverbullet.md` is by running a dedicated SilverBullet instance in read-only mode (see [[Install/Configuration#Run mode]]). \ No newline at end of file diff --git a/website/Getting Started.md b/website/Getting Started.md index 5f97f8d1..efff1a48 100644 --- a/website/Getting Started.md +++ b/website/Getting Started.md @@ -3,40 +3,42 @@ Welcome to the wondrous world of SilverBullet. A world that once you discover an _One of us!_ -Out of the box SilverBullet is fairly minimal in terms of functionality. To give you a good “first run” experience, we recommend you start by importing the [[Library/Core]] library into your space. This will give you a couple of useful pages, slash commands, page templates and widgets such as [[Table of Contents]], [[Linked Mentions]] and [[Linked Tasks]] to start of with. If you have no idea what those are — don’t worry about it. Just keep going, you’ll get the hang of things. +Out of the box SilverBullet is fairly minimal in terms of functionality. To give you a good “first run” experience, we have preconfigured the [[Library/Core]] [[Libraries|library]] for you in your {[Navigate: Open SETTINGS|settings]}. All need to do is import it by pushing this button: -Just push this button: {[Library: Import|Import Core Library]("!silverbullet.md/Library/Core/")} +{[Libraries: Update]} -You know you want to. +Just push that button. You know you want to. Just do it. +You can learn what you actually did later when you feel comfortable diving into [[Libraries]]. + # Next steps -Now that you have some basics stuff in place, it’s time to start playing a bit. +Now that you have some basics stuff in place, it’s time to start playing around a little bit. * Click on the page picker (book icon) icon at the top right, or hit `Cmd-k` (Mac) or `Ctrl-k` (Linux and Windows) to open the [[Page Picker]]. * Type the name of a non-existent page to create it. * Folders are implicitly created by putting slashes (`/`) in the name (even on Windows), e.g. `My Folder/My Page`. Don’t worry about folders existing, we’ll automatically create them if they don’t. * Click on the terminal icon (top right), or hit `Cmd-/` (Mac) or `Ctrl-/` (Linux and Windows), or tap the screen with 3 fingers at the same time (on mobile) to open the [[Command Palette]]. From here you can run various useful and perhaps less useful [[Commands]]. The {[Stats: Show]} one is a safe one to try. -* Select some text and hit `Alt-m` to ==highlight== it, or `Cmd-b` (Mac) or `Ctrl-b` (Windows/Linux) to make it **bold**, or `Cmd-i` (Mac) or `Ctrl-i` (Windows/Linux) to make it _italic_. -* Click a link somewhere on this page to navigate there. When you link to a new page it will initially show up in red (to indicate it does not yet exist), but once you click it — you will create the page automatically (only for real when you actually enter some text). +* Select some text and hit `Cmd-b` (Mac) or `Ctrl-b` (Windows/Linux) to make it **bold**, or `Cmd-i` (Mac) or `Ctrl-i` (Windows/Linux) to make it _italic_. +* Click a link somewhere on this page to navigate there. When you link to a non-existent page it will initially show up in orange (to indicate it does not yet exist), but once you click it — you will create the page automatically (only for real when you actually enter some text). * Start typing `[[` somewhere to insert your own page link (with completion). * Start typing `:party` to trigger the emoji picker 🎉 -* Type `/` somewhere in the text to invoke a **slash command**. +* Type `/` somewhere in the text to invoke a [[Slash Commands|slash command]]. * If this is matching your personality type, you can click this button {[Editor: Toggle Vim Mode]} to toggle Vim mode. If you cannot figure out how to exit it, just click that button again. _Phew!_ -Notice that as you move your cursor around on this page and you get close to or “inside” marked up text, you will get to see the underlying [[Markdown]] code. This experience is what we refer to as “live preview” — generally your text looks clean, but you still can see what’s under the covers and edit it directly, as opposed to [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) that some other applications use. To move your cursor somewhere using your mouse without navigating or activating (e.g. a wiki, regular link, or a button) hold `Alt` when you click. Holding `Cmd` or `Ctrl` when clicking a link will open it in a new tab or window. +Notice that as you move your cursor around on this page and you get close to or “inside” marked up text, you will get to see the underlying [[Markdown]] code. This experience is what we refer to as [[Live Preview]] — generally your text looks clean, but you still can see what’s under the covers and edit it directly, as opposed to [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) that some other applications use. To move your cursor somewhere using your mouse without navigating or activating (e.g. a wiki, regular link, or a button) hold `Alt` when you click. Holding `Cmd` or `Ctrl` when clicking a link will open it in a new tab or window. -Hadn’t we mentioned [[Markdown]] yet? Yeah, that’s the markup language you’ll use to add that dash of markup to your documents. It’s pretty simple to learn if you don’t know it already. +Hadn’t we mentioned [[Markdown]] yet? Yeah, that’s the markup language you’ll use to add that dash of markup to your documents. On top of baseline markdown, SilverBullet supports a number of [[Markdown/Extensions]] as well. -You will notice this whole page section is wrapped in a strange type of block. This is a SilverBullet specific feature called a [[Live Templates]], which embeds another (sometime external) page into the existing one. If you hover over this section, you’ll notice a small _refresh_ and _edit_ button. Hit that edit button to reveal the underlying source that renders this content. +You will notice this whole page section is wrapped in a strange type of block. This is a SilverBullet specific feature called a [[Transclusions]], which embed another (sometime external) page into the existing one. -SilverBullet has even more tricks up its sleeve. Consider [[Live Queries]] which allow you to query [[Objects]] in your space easily. +SilverBullet has more tricks up its sleeve, we are just getting started. Consider [[Live Queries]] which allow you to query [[Objects]] in your space easily. [[Live Templates]] are another source of 🤯. -Don’t believe me? Check this out, here’s a list of (max 10) pages in your space ordered by name, it updates (somewhat) dynamically 🤯. Create some new pages and come back here to see that it works: +As a quick taster, check this out: here’s a list of (max 5) pages in your space ordered by the date of last modification. t updates (somewhat) dynamically. Create some new pages and come back here to see that it works: ```query -page select name order by name limit 10 +page select name order by lastModified limit 5 ``` ## What next? @@ -51,6 +53,6 @@ To keep up with the latest and greatest goings-on in SilverBullet land, keep an Got any more questions? Join our [community](https://community.silverbullet.md/). -Feel a bit more ready in this endeavor? If so, feel to remove the [[Live Templates|live template]] that renders this on-boarding description. You’re on your own now. +Feel a bit more ready in this endeavor? If so, feel to remove [[Transclusions|transclusion]] that renders this on-boarding description. You’re on your own now. -You got this. \ No newline at end of file +You got this. We believe in you. \ No newline at end of file diff --git a/website/Libraries.md b/website/Libraries.md index 31e8b31c..168d4122 100644 --- a/website/Libraries.md +++ b/website/Libraries.md @@ -1,4 +1,4 @@ -A lot of useful functionality in SilverBullet is implemented through [[Templates]], as well as regular [[Pages]]. Some of these you will create yourself for your own specific use, but many are generic and generally useful. Libraries offer a way to _distribute_ sets of templates and pages easily. +A lot of useful functionality in SilverBullet is implemented through [[Templates]], as well as regular [[Pages]]. Some of these you will create yourself for your own specific use, but many are generic and generally useful. Libraries offer a way to _distribute_ sets of templates and pages easily, but also help to give some guidance on how to structure templates for your personal use. # What’s in a library Here are some things that a library may provide: @@ -7,10 +7,62 @@ Here are some things that a library may provide: * Useful widgets such as [[Table of Contents]] or [[Linked Mentions]] * Useful pages that help you perform maintenance on your space, like detecting broken links, such as [[Library/Core/Page/Maintenance]]. -# What libraries are on offer? +# Configuring libraries +Libraries are managed by specifying them in [[SETTINGS]] under the `libraries` attribute. The paths used to libraries are using [[Federation]] page reference syntax. + +When you set up a fresh space, the [[Library/Core]] is automatically configured: + +```yaml +libraries: +- source: "[[!silverbullet.md/Library/Core/*]]" +``` + +If you would like to _exclude_ specific pages, for instance [[Library/Core/Widget/Table of Contents]], you can do so using the library’s `exclude` attribute + +```yaml +libraries: +- source: "[[!silverbullet.md/Library/Core/*]]" + exclude: + - "[[!silverbullet.md/Library/Core/Widget/Table of Contents]]" +``` + +In these paths, you can also use the `*` wildcard: + +```yaml + - "[[!silverbullet.md/Library/Core/Widget/*]]" +``` + +This would exclude all widgets specified in the Core library. + +# Updating libraries +To update all libraries from their original source (we recommend doing this regularly to get the latest updates), run the {[Libraries: Update]} command. + +Updating does the following for each library: + +1. It _deletes_ all pages under its local source prefix in your space. For instance for the Core library, this would be `Library/Core/*`. This means that you should **not make local changes** to these files, because they will be wiped out on every update. +2. It downloads fresh copies from the `source` and puts them in the local source prefix (for `!silverbullet.md/Library/Core/*` this would be `Library/Core/`). +3. It performs a {[System: Reload]} so all new commands and configurations are immediately available. + +# Where are libraries stored? +Libraries are kept in your space’s `Library/` folder. Since these are just regular [[Meta Pages]], the just live along the rest of your content. For instance, the [[Library/Core]] is kept under `Library/Core`. This is _largely_ a convention, but one worth to sticking to. + +If you would like to keep your own templates as a library as well, we recommend putting them in `Library/Personal` as a convention. Then, when you identify a set that may be valuable to other people, you can move them elsewhere under `Library/`. + +# Can I change libraries locally? +Technically there is nothing stopping you from making local changes to library content, however **these changes would be reset on every library update**. + +Therefore, the recommended approach when requiring to make local adjustments is: +1. Make a copy (copy & paste or {[Page: Copy]}) of the page or template you want to change _outside_ the original library path, e.g. under `Library/Personal`. +2. _Exclude_ the page in question using the `exclude` attribute explained under [[#Configuring libraries]] and then run {[Libraries: Update]} again, to remove the excluded (non-modified) version of the page from your space. +3. Profit + +# How to host a library? +This is a topic for exploration. The way it is done here at `silverbullet.md` is by running a SilverBullet instance with the libraries in read-only mode (see [[Install/Configuration#Run mode]]). However, we should explore easier ways to do this. Come talk to us in [the SilverBullet Community](https://community.silverbullet.md/) if you’re interested. + +# What libraries exist? Libraries are still a young concept in SilverBullet and therefore we’re still exploring how to organize and structure these. Currently, we have the following libraries available: - -* [[Library/Core]]: this is the library you want to import _for sure_. Just do it. +* [[Library/Core]]: this is the library you want to use _for sure_. Just do it. * [[Library/Journal]]: for the journalers among us. + diff --git a/website/Library/Core.md b/website/Library/Core.md index 55689152..7efe32b7 100644 --- a/website/Library/Core.md +++ b/website/Library/Core.md @@ -1,4 +1,4 @@ -This library is highly recommended for everybody to import immediately. It provides a lot of functionality you’ll likely appreciate. +This library is highly recommended for everybody to import. It provides a lot of basic functionality in SilverBullet that you’ll appreciate. Some examples: * [[Table of Contents]] @@ -8,9 +8,14 @@ Some examples: * Some useful general purpose pages such as [[Library/Core/Page/Maintenance]], [[Library/Core/Quick Notes]] and [[Library/Core/Page/Template Index]]. # Installation -To import this library, run the {[Library: Import]} command in your SilverBullet space and enter: +In your [[SETTINGS]] list the following under `libraries:` +```yaml +libraries: +- source: "[[!silverbullet.md/Library/Core/*]]" +``` +Then run the {[Libraries: Update]} command to install it. - !silverbullet.md/Library/Core/ +See [[Libraries#Configuring libraries]] for more details. # Included templates ```query diff --git a/website/Library/Journal.md b/website/Library/Journal.md index b2c0ed1b..def8601d 100644 --- a/website/Library/Journal.md +++ b/website/Library/Journal.md @@ -1,9 +1,14 @@ -This library contains some useful page templates for journalers. Want to easily create a daily or weekly note? These templates can get you started. Instantiate them via {[Page: From Template]}. +This [[Libraries|library]] contains some useful page templates for journalers. Want to easily create a daily or weekly note? These templates can get you started. Instantiate them via {[Page: From Template]}. # Installation -To import this library, run the {[Library: Import]} command in your SilverBullet space and enter: +In your [[SETTINGS]] list the following under `libraries:` +```yaml +libraries: +- source: "[[!silverbullet.md/Library/Journal/*]]" +``` +Then run the {[Libraries: Update]} command to install it. - !silverbullet.md/Library/Journal/ +See [[Libraries#Configuring libraries]] for more details. # Included templates ```query diff --git a/website/Markdown/Extensions.md b/website/Markdown/Extensions.md index bf6913c1..b030c612 100644 --- a/website/Markdown/Extensions.md +++ b/website/Markdown/Extensions.md @@ -7,7 +7,7 @@ In addition to supporting [[Markdown/Basics|markdown basics]] as standardized by * [[Live Queries]] * [[Live Templates]] * [[Table of Contents]] -* [[Transclusion]] syntax +* [[Transclusions]] syntax * [[Markdown/Anchors]] * [[Markdown/Admonitions]] * Hashtags, e.g. `#mytag`. diff --git a/website/SETTINGS.md b/website/SETTINGS.md index 39ca5b53..2545ff00 100644 --- a/website/SETTINGS.md +++ b/website/SETTINGS.md @@ -1,13 +1,19 @@ ---- -tags: meta ---- +#meta -This page contains settings for configuring SilverBullet and its Plugs. Changing any of these will go into effect immediately in most cases except `indexPage` which requires a reload. +This page contains settings for configuring SilverBullet and its Plugs. Changing any of these will go into effect immediately in most cases except `indexPage` which requires a page reload. ```yaml # Initial page to load when launching SB, can contain template variables indexPage: "[[SilverBullet]]" +libraries: +# The "Core" library is recommended for all users +- source: "[[!silverbullet.md/Library/Core/*]]" + # You can exclude items from the import using exclude (also supports wildcards): + # exclude: + # - [[!silverbullet.md/Table of Contents]] + # - [[!silverbullet.md/Library/Core/Widget/*]] +## UI TWEAKS # Hide the sync button hideSyncButton: false # Hide the edit button (available on mobile only) diff --git a/website/Transclusion.md b/website/Transclusions.md similarity index 100% rename from website/Transclusion.md rename to website/Transclusions.md