diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index 8e8655fd..f3bf30fb 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -30,3 +30,9 @@ export type IndexTreeEvent = { name: string; tree: ParseTree; }; + +export type PublishEvent = { + uri: string; + // Page name + name: string; +}; diff --git a/plug-api/lib/frontmatter.ts b/plug-api/lib/frontmatter.ts index 3e4517e7..75d72395 100644 --- a/plug-api/lib/frontmatter.ts +++ b/plug-api/lib/frontmatter.ts @@ -35,7 +35,8 @@ export function extractFrontmatter( } // Find FrontMatter and parse it if (t.type === "FrontMatter") { - const yamlText = renderToText(t.children![1].children![0]); + const yamlNode = t.children![1].children![0]; + const yamlText = renderToText(yamlNode); try { const parsedData: any = YAML.parse(yamlText); const newData = { ...parsedData }; @@ -50,7 +51,7 @@ export function extractFrontmatter( } } if (removedOne) { - t.children![0].text = YAML.stringify(newData); + yamlNode.text = YAML.stringify(newData); } } // If nothing is left, let's just delete this whole block diff --git a/plugs/collab/collab.plug.yaml b/plugs/collab/collab.plug.yaml index 9cd35c72..e89ab0bc 100644 --- a/plugs/collab/collab.plug.yaml +++ b/plugs/collab/collab.plug.yaml @@ -15,6 +15,12 @@ functions: path: "./collab.ts:shareCommand" command: name: "Share: Collab" + shareNoop: + path: "./collab.ts:shareNoop" + events: + - share:collab + + # Space extension readPageCollab: path: ./collab.ts:readFileCollab env: client diff --git a/plugs/collab/collab.ts b/plugs/collab/collab.ts index 6b9de90c..079c38f1 100644 --- a/plugs/collab/collab.ts +++ b/plugs/collab/collab.ts @@ -15,7 +15,6 @@ import { collab, editor, markdown, - space, } from "$sb/silverbullet-syscall/mod.ts"; import { nanoid } from "https://esm.sh/nanoid@4.0.0"; @@ -122,6 +121,10 @@ export async function detectPage() { } } +export function shareNoop() { + return true; +} + export async function readFileCollab( name: string, encoding: FileEncoding, diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 871bf7cf..90929d76 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -386,17 +386,18 @@ functions: path: ./stats.ts:statsCommand command: name: "Stats: Show" - key: "Ctrl-s" - mac: "Cmd-s" + key: "Shift-Alt-s" # Cloud pages readPageCloud: path: ./cloud.ts:readFileCloud + env: server pageNamespace: pattern: "💭 .+" operation: readFile getPageMetaCloud: path: ./cloud.ts:getFileMetaCloud + env: server pageNamespace: pattern: "💭 .+" operation: getFileMeta diff --git a/plugs/directive/command.ts b/plugs/directive/command.ts index dcf7895b..13dee70b 100644 --- a/plugs/directive/command.ts +++ b/plugs/directive/command.ts @@ -4,7 +4,9 @@ import { replaceAsync } from "$sb/lib/util.ts"; import { directiveRegex, renderDirectives } from "./directives.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; -export async function updateDirectivesOnPageCommand() { +export async function updateDirectivesOnPageCommand(arg: any) { + // If `arg` is a string, it's triggered automatically via an event, not explicitly via a command + const explicitCall = typeof arg !== "string"; const pageName = await editor.getCurrentPage(); const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); @@ -14,6 +16,22 @@ export async function updateDirectivesOnPageCommand() { return; } + // If this page is shared ($share) via collab: disable directives as well + // due to security concerns + if (metaData.$share) { + for (const uri of metaData.$share) { + if (uri.startsWith("collab:")) { + if (explicitCall) { + await editor.flashNotification( + "Directives are disabled for 'collab' pages (safety reasons).", + "error", + ); + } + return; + } + } + } + // Collect all directives and their body replacements const replacements: { fullMatch: string; text?: string }[] = []; @@ -62,10 +80,15 @@ export async function updateDirectivesOnPageCommand() { ); continue; } + const from = index, to = index + replacement.fullMatch.length; + if (text.substring(from, to) === replacement.text) { + // No change, skip + continue; + } await editor.dispatch({ changes: { - from: index, - to: index + replacement.fullMatch.length, + from, + to, insert: replacement.text, }, }); diff --git a/plugs/directive/directives.ts b/plugs/directive/directives.ts index 2f39b84d..eea5b7a9 100644 --- a/plugs/directive/directives.ts +++ b/plugs/directive/directives.ts @@ -15,7 +15,7 @@ export const directiveRegex = /** * Looks for directives in the text dispatches them based on name */ -export async function directiveDispatcher( +export function directiveDispatcher( pageName: string, text: string, tree: ParseTree, diff --git a/plugs/markdown/share.ts b/plugs/markdown/share.ts index e2d4b3c1..823e7a3d 100644 --- a/plugs/markdown/share.ts +++ b/plugs/markdown/share.ts @@ -1,8 +1,8 @@ import { markdown, space } from "$sb/silverbullet-syscall/mod.ts"; import { fs } from "$sb/plugos-syscall/mod.ts"; import { asset } from "$sb/plugos-syscall/mod.ts"; -import type { PublishEvent } from "../share/publish.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts"; +import { PublishEvent } from "$sb/app_event.ts"; export async function sharePublisher(event: PublishEvent) { const path = event.uri.split(":")[1]; diff --git a/plugs/share/publish.ts b/plugs/share/publish.ts index 5acdea1f..999db111 100644 --- a/plugs/share/publish.ts +++ b/plugs/share/publish.ts @@ -1,26 +1,26 @@ import { events } from "$sb/plugos-syscall/mod.ts"; import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; - -export type PublishEvent = { - uri: string; - // Page name - name: string; -}; +import { PublishEvent } from "$sb/app_event.ts"; export async function publishCommand() { await editor.save(); const text = await editor.getText(); const pageName = await editor.getCurrentPage(); const tree = await markdown.parseMarkdown(text); - let { $share } = extractFrontmatter(tree); + const { $share } = extractFrontmatter(tree); if (!$share) { - await editor.flashNotification("No $share directive found", "error"); + await editor.flashNotification("Saved."); return; } if (!Array.isArray($share)) { - $share = [$share]; + await editor.flashNotification( + "$share front matter must be an array.", + "error", + ); + return; } + await editor.flashNotification("Sharing..."); // Delegate actual publishing to the server try { await system.invokeFunction("server", "publish", pageName, $share); diff --git a/plugs/share/share.plug.yaml b/plugs/share/share.plug.yaml index 4cce13ed..acae9d70 100644 --- a/plugs/share/share.plug.yaml +++ b/plugs/share/share.plug.yaml @@ -4,6 +4,8 @@ functions: path: publish.ts:publishCommand command: name: "Share: Publish" + key: "Ctrl-s" + mac: "Cmd-s" publish: path: publish.ts:publish env: server diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index d17bef11..baa83660 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -5,8 +5,13 @@ release. ## 0.2.1 -* New `Plugs: Add` command -* When holding `Shift` while pasting, rich test paste will be disabled. +* New `Plugs: Add` command to quickly add a new plug (will create a `PLUGS` page if you don't have one yet). +* **Paste without formatting**: holding `Shift` while pasting will disable "rich text paste." +* General purpose, extensible, **share support** ([RFC](https://github.com/silverbulletmd/silverbullet/discussions/117)): using the `$share` key in frontmatter (with the {[Share: Publish]} bound to `Cmd-s`/`Ctrl-s`). This will enable plugs to support various types of page sharing. Two types of sharing are supported initially: + * `file:/full/path/to/file.html` will render the current page as HTML and write it to given path. + * `collab:*` will share the page via the new real-time collaboration feature (see next bullet) + * `gh-gist:` via the [[🔌 Github]] plug, which has been updated to add support for publishing to Gists. +* An initial version of **real-time collaboration** on individual pages: This will allow concurrent "Google Doc" style editing. To enable this, a central collaboration server is used (there is currently one deployed at `wss://collab.silverbullet.md`) which will become the source of truth for pages shared this way. To share an existing page, run the {[Share: Collab]} command. A random ID will be assigned to the page, which functions as a token for others to join the editing session. Copy and paste the resulting `collab:...` URI and send it your collaborator, who can join using {[Share: Join Collab]}. **Note:** the security of this is to be improved, it currently relies on "security through obscurity": if you guess an existing ID, you will have full access. Be careful what you share this way. ## 0.2.0 * The editor is now in "live preview" mode where a lot of markdown is hidden unless the cursor is present. This will take some getting used to, but results in a much more distraction free look. diff --git a/website/🔌 Github.md b/website/🔌 Github.md index 571c57e6..ab23df94 100644 --- a/website/🔌 Github.md +++ b/website/🔌 Github.md @@ -7,7 +7,10 @@ author: Zef Hemel # SilverBullet plug for Github -Provides Github events, notifications and pull requests as query sources using SB's query mechanism +Provides various integrations with Github: + +* Query sources for events, notifications and pull requests +* Ability to load and share pages as Gists ## Installation Open your `PLUGS` note in SilverBullet and add this plug to the list: @@ -19,8 +22,6 @@ Open your `PLUGS` note in SilverBullet and add this plug to the list: Then run the `Plugs: Update` command and off you go! ## Configuration -This step is optional for anything but the `gh-notification` source, but without it you may be rate limited by the Github API, - To configure, add a `githubToken` key to your `SECRETS` page, this should be a [personal access token](https://github.com/settings/tokens): ```yaml @@ -37,9 +38,14 @@ To configure, add a `githubToken` key to your `SECRETS` page, this should be a [ * `query`: [the search query](https://docs.github.com/en/rest/search#search-issues-and-pull-requests) * `gh-notification` requires a `githubToken` to be configured in `SECRETS`. +## Share as Gist support + +To use: navigate to a page, and run the {[Share: Gist: Public Gist]} command, this will perform an initial publish, and add a `$share` attribute to your page's front matter. Subsequent updates can be performed via {[Share: Publish]}. + +To pull an *existing* gist into your space, use the {[Share: Gist: Load]} command and paste the URL to the gist. ## Example -Example uses: +Example uses of the query providers: ## Recent pushes