Embed video, audio and pdf (#1008)

pull/1017/head
onespaceman 2024-08-02 10:30:39 -04:00 committed by GitHub
parent 357a50f820
commit 45166ccd93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 106 additions and 46 deletions

View File

@ -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<Decoration>[] = [];
@ -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),
);
},
});

View File

@ -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(),

View File

@ -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;
}
}

View File

@ -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 spaces 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 spaces 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

View File

@ -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