diff --git a/web/cm_plugins/inline_content.ts b/web/cm_plugins/inline_content.ts index 6a714944..e987f6d6 100644 --- a/web/cm_plugins/inline_content.ts +++ b/web/cm_plugins/inline_content.ts @@ -6,8 +6,9 @@ import { decoratorStateField } from "./util.ts"; import type { Client } from "../client.ts"; import { isFederationPath, isLocalPath, resolvePath } from "$sb/lib/resolve.ts"; import { parsePageRef } from "$sb/lib/page_ref.ts"; +import { mime } from "mimetypes"; -type ImageDimensions = { +type ContentDimensions = { width?: number; height?: number; }; @@ -16,7 +17,7 @@ class InlineContentWidget extends WidgetType { constructor( readonly url: string, readonly title: string, - readonly dim: ImageDimensions | undefined, + readonly dim: ContentDimensions | undefined, readonly client: Client, ) { super(); @@ -28,45 +29,95 @@ class InlineContentWidget extends WidgetType { } get estimatedHeight(): number { - const cachedHeight = this.client.getCachedWidgetHeight(`image:${this.url}`); + const cachedHeight = this.client.getCachedWidgetHeight( + `content:${this.url}`, + ); return cachedHeight; } toDOM() { - const img = document.createElement("img"); - // console.log("Creating DOM", this.url); - const cachedImageHeight = this.client.getCachedWidgetHeight( - `image:${this.url}`, + const div = document.createElement("div"); + div.className = "sb-inline-content"; + div.style.display = "block"; + const mimeType = mime.getType( + this.url.substring(this.url.lastIndexOf(".") + 1), ); - img.onload = () => { - // console.log("Loaded", this.url, "with height", img.height); - if (img.height !== cachedImageHeight) { - this.client.setCachedWidgetHeight(`image:${this.url}`, img.height); - } - }; - img.src = this.url; - img.alt = this.title; - img.title = this.title; - img.style.display = "block"; - img.className = "sb-inline-img"; - if (this.dim) { - img.style.height = this.dim.height ? `${this.dim.height}px` : ""; - img.style.width = this.dim.width ? `${this.dim.width}px` : ""; - } else if (cachedImageHeight > 0) { - img.height = cachedImageHeight; + + if (!mimeType) { + return div; } - return img; + if (mimeType.startsWith("image/")) { + const img = document.createElement("img"); + img.src = this.url; + img.alt = this.title; + this.setDim(img, "load"); + div.appendChild(img); + } else if (mimeType.startsWith("video/")) { + const video = document.createElement("video"); + video.src = this.url; + video.title = this.title; + video.controls = true; + video.autoplay = false; + this.setDim(video, "loadeddata"); + div.appendChild(video); + } else if (mimeType.startsWith("audio/")) { + const audio = document.createElement("audio"); + audio.src = this.url; + audio.title = this.title; + audio.controls = true; + audio.autoplay = false; + this.setDim(audio, "loadeddata"); + div.appendChild(audio); + } else if (mimeType === "application/pdf") { + const embed = document.createElement("object"); + embed.type = mimeType; + embed.data = this.url; + embed.style.width = "100%"; + embed.style.height = "20em"; + this.setDim(embed, "load"); + div.appendChild(embed); + } + + return div; + } + + setDim(el: HTMLElement, event: string) { + const cachedContentHeight = this.client.getCachedWidgetHeight( + `content:${this.url}`, + ); + + el.addEventListener(event, () => { + if (el.clientHeight !== cachedContentHeight) { + this.client.setCachedWidgetHeight( + `content:${this.url}`, + el.clientHeight, + ); + } + }); + + el.style.maxWidth = "100%"; + + if (this.dim) { + if (this.dim.height) { + el.style.height = `${this.dim.height}px`; + } + if (this.dim.width) { + el.style.width = `${this.dim.width}px`; + } + } else if (cachedContentHeight > 0) { + el.style.height = cachedContentHeight.toString(); + } } } -// Parse an alias, possibly containing image dimensions into an object +// Parse an alias, possibly containing dimensions into an object // Formats supported: "alias", "alias|100", "alias|100x200", "100", "100x200" function parseAlias( text: string, -): { alias?: string; dim?: ImageDimensions } { +): { alias?: string; dim?: ContentDimensions } { let alias: string | undefined; - let dim: ImageDimensions | undefined; + let dim: ContentDimensions | undefined; if (text.includes("|")) { const [aliasPart, dimPart] = text.split("|"); alias = aliasPart; @@ -94,7 +145,7 @@ function parseAlias( return { alias, dim }; } -export function inlineImagesPlugin(client: Client) { +export function inlineContentPlugin(client: Client) { return decoratorStateField((state: EditorState) => { const widgets: Range[] = []; @@ -106,7 +157,7 @@ export function inlineImagesPlugin(client: Client) { const text = state.sliceDoc(node.from, node.to); let [url, alias]: (string | null)[] = [null, null]; - let dim: ImageDimensions | undefined; + let dim: ContentDimensions | undefined; let match: RegExpExecArray | null; if ((match = /!?\[([^\]]*)\]\((.+)\)/g.exec(text))) { [/* fullMatch */, alias, url] = match; @@ -173,7 +224,7 @@ export function inlineImagesPlugin(client: Client) { Decoration.widget({ widget: new InlineContentWidget(url, alias, dim, client), block: true, - }).range(node.to), + }).range(node.to + 1), ); }, }); diff --git a/web/editor_state.ts b/web/editor_state.ts index aa31684e..ff8290c0 100644 --- a/web/editor_state.ts +++ b/web/editor_state.ts @@ -27,7 +27,7 @@ import { import { vim } from "@replit/codemirror-vim"; import { markdown } from "@codemirror/lang-markdown"; import type { Client } from "./client.ts"; -import { inlineImagesPlugin } from "./cm_plugins/inline_content.ts"; +import { inlineContentPlugin } from "./cm_plugins/inline_content.ts"; import { cleanModePlugins } from "./cm_plugins/clean.ts"; import { lineWrapper } from "./cm_plugins/line_wrapper.ts"; import { createSmartQuoteKeyBindings } from "./cm_plugins/smart_quotes.ts"; @@ -114,7 +114,7 @@ export function createEditorState( } }, }), - inlineImagesPlugin(client), + inlineContentPlugin(client), codeCopyPlugin(client), highlightSpecialChars(), history(), diff --git a/web/styles/editor.scss b/web/styles/editor.scss index 9e3ceddb..44748865 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -32,21 +32,27 @@ background-color: var(--editor-directive-background-color); } - .sb-line-h1, h1 { + .sb-line-h1, + h1 { font-size: 1.5em; } - .sb-line-h2, h2 { + .sb-line-h2, + h2 { font-size: 1.2em; } - .sb-line-h3, h3 { + .sb-line-h3, + h3 { font-size: 1.1em; } - .sb-line-h4, h4, - .sb-line-h5, h5, - .sb-line-h6, h6 { + .sb-line-h4, + h4, + .sb-line-h5, + h5, + .sb-line-h6, + h6 { font-size: 1em; } @@ -61,8 +67,9 @@ } } - .sb-inline-img { + .sb-inline-content * { max-width: 100%; + display: block; } .cm-panels-bottom .cm-vim-panel { @@ -639,4 +646,4 @@ div:not(.cm-focused).cm-fat-cursor { outline: none !important; -} +} \ No newline at end of file diff --git a/website/Attachments.md b/website/Attachments.md index 56fec389..bf9f5e1c 100644 --- a/website/Attachments.md +++ b/website/Attachments.md @@ -15,17 +15,19 @@ Attachments can be linked to in two ways: * Via the wiki link syntax: `[[attachment.pdf]]`. These paths are absolute and relative to your space’s root, just like regular page links. That is: on a page `MyFolder/Hello` an attachment link `[[attachment.pdf]]` would link to the file `attachment.pdf` in the space’s root folder. # Embedding -Images can also be embedded using the [[#Linking]] syntax, but prefixed with an `!`: +Media can also be embedded using the [[#Linking]] syntax, but prefixed with an `!`: +Images, videos, audio and PDFs are currently supported. * `![alternate text](image.png)` * `![[image.png]]` These follow the same relative/absolute path rules as links described before. -## Image resizing +## Media resizing -In addition, images can be _sized_ using the following syntax: +In addition, media can be _sized_ using the following syntax: * Specifying only a width: `![Alt text|300](image.png)` or `![[image.png|300]]` +* Specifying only a height: `![Alt text|x300](image.png)` or `![[image.png|x300]]` * Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]` # Management diff --git a/website/Transclusions.md b/website/Transclusions.md index bfbf59c2..8b10f5c1 100644 --- a/website/Transclusions.md +++ b/website/Transclusions.md @@ -2,11 +2,11 @@ Transclusions are an extension of the [[Markdown]] syntax enabling inline embedd The general syntax is `![[path]]`. Two types of transclusions are currently supported: -# Images +# Media Syntax: `![[path/to/image.jpg]]` see [[Attachments#Embedding]] for more details. -Image resizing is also supported: -![[Attachments#Image resizing]] +Media resizing is also supported: +![[Attachments#Media resizing]] # Pages Syntax: * `![[page name]]` embed an entire page