diff --git a/.gitignore b/.gitignore index 479c846..0e5d25a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ dist_bundle dist *.js.map website_build -data.db +data.db* publish-data.db /index.json .idea diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index c735ae8..7a105b9 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -42,3 +42,11 @@ export type CompleteEvent = { linePrefix: string; pos: number; }; + +export type WidgetContent = { + html?: string; + script?: string; + url?: string; + height?: number; + width?: number; +}; diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 04f7c6f..1063946 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -409,6 +409,10 @@ functions: events: - unfurl:title-unfurl + embedWidget: + path: ./embed.ts:embedWidget + codeWidget: embed + # Random stuff statsCommand: path: ./stats.ts:statsCommand diff --git a/plugs/core/embed.ts b/plugs/core/embed.ts new file mode 100644 index 0000000..1173ff2 --- /dev/null +++ b/plugs/core/embed.ts @@ -0,0 +1,47 @@ +import * as YAML from "yaml"; +import type { WidgetContent } from "$sb/app_event.ts"; + +type EmbedConfig = { + url: string; + height?: number; + width?: number; +}; + +function extractYoutubeVideoId(url: string) { + let match = url.match(/youtube\.com\/watch\?v=([^&]+)/); + if (match) { + return match[1]; + } + match = url.match(/youtu.be\/([^&]+)/); + if (match) { + return match[1]; + } + + return null; +} + +export function embedWidget( + bodyText: string, +): WidgetContent { + try { + const data: EmbedConfig = YAML.parse(bodyText) as any; + let url = data.url; + const youtubeVideoId = extractYoutubeVideoId(url); + if (youtubeVideoId) { + url = `https://www.youtube.com/embed/${youtubeVideoId}`; + // Sensible video defaults + data.width = data.width || 560; + data.height = data.height || 315; + } + return { + url, + height: data.height, + width: data.width, + }; + } catch (e: any) { + return { + html: `ERROR: Could not parse body as YAML: ${e.message}`, + script: "", + }; + } +} diff --git a/plugs/markdown/widget.ts b/plugs/markdown/widget.ts index 1011775..9efe10f 100644 --- a/plugs/markdown/widget.ts +++ b/plugs/markdown/widget.ts @@ -1,9 +1,10 @@ import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts"; +import type { WidgetContent } from "$sb/app_event.ts"; import { renderMarkdownToHtml } from "./markdown_render.ts"; export async function markdownWidget( bodyText: string, -): Promise<{ html: string; script: string }> { +): Promise { const mdTree = await parseMarkdown(bodyText); const html = renderMarkdownToHtml(mdTree, { diff --git a/web/cm_plugins/fenced_code.ts b/web/cm_plugins/fenced_code.ts index ba7bcea..500618b 100644 --- a/web/cm_plugins/fenced_code.ts +++ b/web/cm_plugins/fenced_code.ts @@ -1,3 +1,4 @@ +import { WidgetContent } from "../../plug-api/app_event.ts"; import { panelHtml } from "../components/panel.tsx"; import { Decoration, EditorState, syntaxTree, WidgetType } from "../deps.ts"; import type { Editor } from "../editor.tsx"; @@ -20,6 +21,7 @@ class IFrameWidget extends WidgetType { } toDOM(): HTMLElement { + console.log("toDOM"); const iframe = document.createElement("iframe"); iframe.srcdoc = panelHtml; // iframe.style.height = "0"; @@ -60,17 +62,31 @@ class IFrameWidget extends WidgetType { iframe.onload = () => { // Subscribe to message event on global object (to receive messages from iframe) globalThis.addEventListener("message", messageListener); - this.codeWidgetCallback(this.bodyText).then(({ html, script }) => { - iframe.contentWindow!.postMessage({ - type: "html", - html, - script, - }); - iframe.contentWindow!.onunload = () => { - // Unsubscribing from events - globalThis.removeEventListener("message", messageListener); - }; - }); + // Only run this code once + iframe.onload = null; + this.codeWidgetCallback(this.bodyText).then( + (widgetContent: WidgetContent) => { + if (widgetContent.html) { + iframe.contentWindow!.postMessage({ + type: "html", + html: widgetContent.html, + script: widgetContent.script, + }); + // iframe.contentWindow!.onunload = () => { + // // Unsubscribing from events + // globalThis.removeEventListener("message", messageListener); + // }; + } else if (widgetContent.url) { + iframe.contentWindow!.location.href = widgetContent.url; + if (widgetContent.height) { + iframe.style.height = widgetContent.height + "px"; + } + if (widgetContent.width) { + iframe.style.width = widgetContent.width + "px"; + } + } + }, + ); }; return iframe; } diff --git a/web/components/panel.tsx b/web/components/panel.tsx index 1193256..9c9cd7e 100644 --- a/web/components/panel.tsx +++ b/web/components/panel.tsx @@ -89,7 +89,7 @@ function loadJsByUrl(url) { -Send me HTML +Loading... `; diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index edf53e4..d618851 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -6,6 +6,7 @@ release. ## Next * Fixed copy & paste, drag & drop of attachments in the [[Desktop]] app * Continuous [[Sync]] +* Support for embedding [[Markdown/Code Widgets]]. --- ## 0.2.8 diff --git a/website/Markdown.md b/website/Markdown.md index 5728a49..11d36d7 100644 --- a/website/Markdown.md +++ b/website/Markdown.md @@ -8,6 +8,7 @@ We mentioned markdown _extensions_, here are the ones currently supported: * Double-bracketed wiki links: `[[link to page]]`, optionally with aliases `[[link to page|alias]]`. * Hashtags, e.g. `#mytag`. * Command link syntax: `{[Stats: Show]}` rendered into a clickable button {[Stats: Show]}. +* [[Markdown/Code Widgets]] * [Tables](https://www.markdownguide.org/extended-syntax/#tables) * [Fenced code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) * [Task lists](https://www.markdownguide.org/extended-syntax/#task-lists) diff --git a/website/Markdown/Code Widgets.md b/website/Markdown/Code Widgets.md new file mode 100644 index 0000000..8e59591 --- /dev/null +++ b/website/Markdown/Code Widgets.md @@ -0,0 +1,40 @@ +Code widgets are a SilverBullet specific “extensions” to [[Markdown]]. Technically, it’s not an extension — it just gives new semantics to markdown’s native fenced code blocks — code blocks that start with a triple backtick, specifying a programming language. + +Currently, SilverBullet provides two code widgets as part of its built in [[🔌 Plugs]]: + +* `embed` +* `markdown` + +In addition, plugs like [[🔌 KaTeX]] and [[🔌 Mermaid]] add additional ones. + +## Embed +This allows you to embed internet content into your page inside of an iframe. This is useful to, for instance, embed youtube videos. In fact, there is specific support for those. + +Two examples. + +First, embedding the silverbullet.md website into the silverbullet.md website (inception!): + +```embed +url: https://silverbullet.md +``` + +and a Youtube video: + +```embed +url: https://www.youtube.com/watch?v=VemS-cqAD5k +``` + +Note, there is specific support for youtube videos — it automatically will set width, height and replace the URL with an embed URL. + +The body of an `embed` block is written in [[YAML]] and supports the following attributes: + +* `url` (mandatory): the URL of the content to embed +* `height` (optional): the height of the embedded page in pixels +* `width` (optional): the width of the embedded page in pixels + +## Markdown +You can embed markdown inside of markdown and live preview it. Is this useful? 🤷 not really, it’s more of a demo of how this works. Nevertheless, to each their own, here’s an example: + +```markdown +This is going to be **bold** +``` \ No newline at end of file diff --git a/website/SilverBullet.md b/website/SilverBullet.md index 8c58b75..f86f19c 100644 --- a/website/SilverBullet.md +++ b/website/SilverBullet.md @@ -20,8 +20,11 @@ Now that we got that out of the way, let’s have a look at some of SilverBullet * **Self-hosted**: you own your data. All content is stored as plain files in a folder on disk. Back up, sync, edit, publish, script with any additional tools you like. * SilverBullet is [open source, MIT licensed](https://github.com/silverbulletmd/silverbullet) software. -![Screencast screenshot](demo-video-screenshot.png) -To get a good feel of what SilverBullet is capable of, [have a look at this introduction video](https://youtu.be/VemS-cqAD5k). +To get a good feel of what SilverBullet is capable of, have a look at this introduction video. + +```embed +url: https://youtu.be/VemS-cqAD5k +``` ## Try it Here’s the kicker: