Embed video, audio and pdf (#1008)
parent
357a50f820
commit
45166ccd93
|
@ -6,8 +6,9 @@ import { decoratorStateField } from "./util.ts";
|
||||||
import type { Client } from "../client.ts";
|
import type { Client } from "../client.ts";
|
||||||
import { isFederationPath, isLocalPath, resolvePath } from "$sb/lib/resolve.ts";
|
import { isFederationPath, isLocalPath, resolvePath } from "$sb/lib/resolve.ts";
|
||||||
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
import { parsePageRef } from "$sb/lib/page_ref.ts";
|
||||||
|
import { mime } from "mimetypes";
|
||||||
|
|
||||||
type ImageDimensions = {
|
type ContentDimensions = {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
@ -16,7 +17,7 @@ class InlineContentWidget extends WidgetType {
|
||||||
constructor(
|
constructor(
|
||||||
readonly url: string,
|
readonly url: string,
|
||||||
readonly title: string,
|
readonly title: string,
|
||||||
readonly dim: ImageDimensions | undefined,
|
readonly dim: ContentDimensions | undefined,
|
||||||
readonly client: Client,
|
readonly client: Client,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
@ -28,45 +29,95 @@ class InlineContentWidget extends WidgetType {
|
||||||
}
|
}
|
||||||
|
|
||||||
get estimatedHeight(): number {
|
get estimatedHeight(): number {
|
||||||
const cachedHeight = this.client.getCachedWidgetHeight(`image:${this.url}`);
|
const cachedHeight = this.client.getCachedWidgetHeight(
|
||||||
|
`content:${this.url}`,
|
||||||
|
);
|
||||||
return cachedHeight;
|
return cachedHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM() {
|
toDOM() {
|
||||||
const img = document.createElement("img");
|
const div = document.createElement("div");
|
||||||
// console.log("Creating DOM", this.url);
|
div.className = "sb-inline-content";
|
||||||
const cachedImageHeight = this.client.getCachedWidgetHeight(
|
div.style.display = "block";
|
||||||
`image:${this.url}`,
|
const mimeType = mime.getType(
|
||||||
|
this.url.substring(this.url.lastIndexOf(".") + 1),
|
||||||
);
|
);
|
||||||
img.onload = () => {
|
|
||||||
// console.log("Loaded", this.url, "with height", img.height);
|
if (!mimeType) {
|
||||||
if (img.height !== cachedImageHeight) {
|
return div;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
// Formats supported: "alias", "alias|100", "alias|100x200", "100", "100x200"
|
||||||
function parseAlias(
|
function parseAlias(
|
||||||
text: string,
|
text: string,
|
||||||
): { alias?: string; dim?: ImageDimensions } {
|
): { alias?: string; dim?: ContentDimensions } {
|
||||||
let alias: string | undefined;
|
let alias: string | undefined;
|
||||||
let dim: ImageDimensions | undefined;
|
let dim: ContentDimensions | undefined;
|
||||||
if (text.includes("|")) {
|
if (text.includes("|")) {
|
||||||
const [aliasPart, dimPart] = text.split("|");
|
const [aliasPart, dimPart] = text.split("|");
|
||||||
alias = aliasPart;
|
alias = aliasPart;
|
||||||
|
@ -94,7 +145,7 @@ function parseAlias(
|
||||||
return { alias, dim };
|
return { alias, dim };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inlineImagesPlugin(client: Client) {
|
export function inlineContentPlugin(client: Client) {
|
||||||
return decoratorStateField((state: EditorState) => {
|
return decoratorStateField((state: EditorState) => {
|
||||||
const widgets: Range<Decoration>[] = [];
|
const widgets: Range<Decoration>[] = [];
|
||||||
|
|
||||||
|
@ -106,7 +157,7 @@ export function inlineImagesPlugin(client: Client) {
|
||||||
|
|
||||||
const text = state.sliceDoc(node.from, node.to);
|
const text = state.sliceDoc(node.from, node.to);
|
||||||
let [url, alias]: (string | null)[] = [null, null];
|
let [url, alias]: (string | null)[] = [null, null];
|
||||||
let dim: ImageDimensions | undefined;
|
let dim: ContentDimensions | undefined;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
if ((match = /!?\[([^\]]*)\]\((.+)\)/g.exec(text))) {
|
if ((match = /!?\[([^\]]*)\]\((.+)\)/g.exec(text))) {
|
||||||
[/* fullMatch */, alias, url] = match;
|
[/* fullMatch */, alias, url] = match;
|
||||||
|
@ -173,7 +224,7 @@ export function inlineImagesPlugin(client: Client) {
|
||||||
Decoration.widget({
|
Decoration.widget({
|
||||||
widget: new InlineContentWidget(url, alias, dim, client),
|
widget: new InlineContentWidget(url, alias, dim, client),
|
||||||
block: true,
|
block: true,
|
||||||
}).range(node.to),
|
}).range(node.to + 1),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,7 +27,7 @@ import {
|
||||||
import { vim } from "@replit/codemirror-vim";
|
import { vim } from "@replit/codemirror-vim";
|
||||||
import { markdown } from "@codemirror/lang-markdown";
|
import { markdown } from "@codemirror/lang-markdown";
|
||||||
import type { Client } from "./client.ts";
|
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 { cleanModePlugins } from "./cm_plugins/clean.ts";
|
||||||
import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
|
import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
|
||||||
import { createSmartQuoteKeyBindings } from "./cm_plugins/smart_quotes.ts";
|
import { createSmartQuoteKeyBindings } from "./cm_plugins/smart_quotes.ts";
|
||||||
|
@ -114,7 +114,7 @@ export function createEditorState(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inlineImagesPlugin(client),
|
inlineContentPlugin(client),
|
||||||
codeCopyPlugin(client),
|
codeCopyPlugin(client),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
history(),
|
history(),
|
||||||
|
|
|
@ -32,21 +32,27 @@
|
||||||
background-color: var(--editor-directive-background-color);
|
background-color: var(--editor-directive-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-line-h1, h1 {
|
.sb-line-h1,
|
||||||
|
h1 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-line-h2, h2 {
|
.sb-line-h2,
|
||||||
|
h2 {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-line-h3, h3 {
|
.sb-line-h3,
|
||||||
|
h3 {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-line-h4, h4,
|
.sb-line-h4,
|
||||||
.sb-line-h5, h5,
|
h4,
|
||||||
.sb-line-h6, h6 {
|
.sb-line-h5,
|
||||||
|
h5,
|
||||||
|
.sb-line-h6,
|
||||||
|
h6 {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +67,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-inline-img {
|
.sb-inline-content * {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-panels-bottom .cm-vim-panel {
|
.cm-panels-bottom .cm-vim-panel {
|
||||||
|
@ -639,4 +646,4 @@
|
||||||
|
|
||||||
div:not(.cm-focused).cm-fat-cursor {
|
div:not(.cm-focused).cm-fat-cursor {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
|
@ -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.
|
* 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
|
# 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)`
|
* `![alternate text](image.png)`
|
||||||
* `![[image.png]]`
|
* `![[image.png]]`
|
||||||
|
|
||||||
These follow the same relative/absolute path rules as links described before.
|
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 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]]`
|
* Specifying both width and height: `![Hello|300x300](image.png)` or `![[image.png|300x300]]`
|
||||||
|
|
||||||
# Management
|
# Management
|
||||||
|
|
|
@ -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:
|
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.
|
Syntax: `![[path/to/image.jpg]]` see [[Attachments#Embedding]] for more details.
|
||||||
|
|
||||||
Image resizing is also supported:
|
Media resizing is also supported:
|
||||||
![[Attachments#Image resizing]]
|
![[Attachments#Media resizing]]
|
||||||
# Pages
|
# Pages
|
||||||
Syntax:
|
Syntax:
|
||||||
* `![[page name]]` embed an entire page
|
* `![[page name]]` embed an entire page
|
||||||
|
|
Loading…
Reference in New Issue