Skip to content

Commit d9f4b93

Browse files
committed
Eliminate (most) uses of the client global
Also an opportunity to refactor the transclusion code, which had become very messy
1 parent 6b461aa commit d9f4b93

File tree

10 files changed

+300
-304
lines changed

10 files changed

+300
-304
lines changed

client/codemirror/inline_content.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ import {
1010
import type { Client } from "../client.ts";
1111
import { LuaWidget } from "./lua_widget.ts";
1212
import {
13+
createMediaElement,
1314
expandMarkdown,
14-
inlineContentFromURL,
15+
readTransclusionContent,
1516
} from "../markdown_renderer/inline.ts";
17+
import {
18+
isLocalURL,
19+
resolveMarkdownLink,
20+
} from "@silverbulletmd/silverbullet/lib/resolve";
1621
import { parseMarkdown } from "../markdown_parser/parser.ts";
1722
import { renderToText } from "@silverbulletmd/silverbullet/lib/tree";
1823
import {
@@ -60,24 +65,40 @@ export function inlineContentPlugin(client: Client) {
6065
text,
6166
text,
6267
async () => {
63-
try {
64-
const result: any = await inlineContentFromURL(
65-
client.space,
66-
transclusion,
68+
// Resolve local URLs
69+
if (isLocalURL(transclusion.url)) {
70+
transclusion.url = resolveMarkdownLink(
71+
client.currentName(),
72+
decodeURI(transclusion.url),
6773
);
68-
const content =
69-
result.text !== undefined
70-
? {
71-
markdown: renderToText(
72-
await expandMarkdown(
73-
client.space,
74-
nameFromTransclusion(transclusion),
75-
parseMarkdown(result.text, result.offset),
76-
client.clientSystem.spaceLuaEnv,
77-
),
78-
),
79-
}
80-
: { html: result };
74+
}
75+
76+
try {
77+
let content;
78+
try {
79+
const result = await readTransclusionContent(
80+
client.space,
81+
transclusion,
82+
);
83+
content = {
84+
markdown: renderToText(
85+
await expandMarkdown(
86+
client.space,
87+
nameFromTransclusion(transclusion),
88+
parseMarkdown(result.text, result.offset),
89+
client.clientSystem.spaceLuaEnv,
90+
),
91+
),
92+
};
93+
} catch {
94+
const element = createMediaElement(transclusion);
95+
if (!element) {
96+
throw new Error(
97+
`Unsupported content: ${transclusion.url}`,
98+
);
99+
}
100+
content = { html: element };
101+
}
81102

82103
return {
83104
_isWidget: true,

client/codemirror/table.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ class TableViewWidget extends WidgetType {
5454
});
5555

5656
void expandMarkdown(
57-
client.space,
58-
client.currentName(),
57+
this.client.space,
58+
this.client.currentName(),
5959
this.t,
60-
client.clientSystem.spaceLuaEnv,
60+
this.client.clientSystem.spaceLuaEnv,
6161
).then((t) => {
6262
dom.innerHTML = renderMarkdownToHtml(t, {
6363
// Annotate every element with its position so we can use it to put

client/markdown_renderer/inline.ts

Lines changed: 99 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import {
1010
isMarkdownPath,
1111
parseToRef,
1212
} from "@silverbulletmd/silverbullet/lib/ref";
13-
import { isLocalURL } from "@silverbulletmd/silverbullet/lib/resolve";
13+
import {
14+
isLocalURL,
15+
resolveMarkdownLink,
16+
} from "@silverbulletmd/silverbullet/lib/resolve";
1417
import mime from "mime";
1518
import { LuaStackFrame, LuaTable } from "../space_lua/runtime.ts";
1619
import { parseMarkdown } from "../markdown_parser/parser.ts";
@@ -62,13 +65,25 @@ export async function expandMarkdown(
6265
return n;
6366
}
6467

65-
try {
66-
const result = await inlineContentFromURL(space, transclusion);
68+
// Resolve local URLs
69+
if (isLocalURL(transclusion.url)) {
70+
transclusion.url = resolveMarkdownLink(
71+
pageName,
72+
decodeURI(transclusion.url),
73+
);
74+
}
6775

68-
// We don't transclude anything that's not markdown
69-
if (!("text" in result)) {
70-
return n;
71-
}
76+
// We don't transclude anything that's not markdown
77+
const mimeType = getMimeTypeFromUrl(
78+
transclusion.url,
79+
transclusion.linktype !== "wikilink",
80+
);
81+
if (mimeType && mimeType !== "text/markdown") {
82+
return n;
83+
}
84+
85+
try {
86+
const result = await readTransclusionContent(space, transclusion);
7287

7388
// We know it's a markdown page and we know we are transcluding it. "Mark"
7489
// it so we won't touch it down the line and cause endless recursion
@@ -155,54 +170,57 @@ export async function expandMarkdown(
155170
return mdTree;
156171
}
157172

158-
type OffsetText = {
173+
export type OffsetText = {
159174
text: string;
160175
offset: number;
161176
};
162177

163178
/**
164-
* Function to generate HTML or markdown for a ![[<link>]] type transclusion.
165-
* @param space space object to use to retrieve content (readRef)
166-
* @param transclusion transclusion object to process
167-
* @returns a string for a markdown transclusion, or html for everything else
179+
* Determine the MIME type for a transclusion URL.
168180
*/
169-
export function inlineContentFromURL(
170-
space: Space,
171-
transclusion: Transclusion,
172-
): HTMLElement | OffsetText | Promise<HTMLElement | OffsetText> {
173-
const allowExternal = transclusion.linktype !== "wikilink";
174-
if (!client) {
175-
return { text: "", offset: 0 };
176-
}
177-
let mimeType: string | null | undefined;
178-
if (!isLocalURL(transclusion.url) && allowExternal) {
179-
// Remote URL
180-
// Realistically we should dertermine the mine type by sending a HEAD
181-
// request, this poses multiple problems
182-
// 1. This makes `async` a hard requirement here
183-
// 2. We would need to proxy the request (because of CORS)
184-
// 3. It won't work "offline" (i.e. away from the SB instance, because it
185-
// can't proxy the request anymore)
186-
// 4. It can be pretty heavy. If your internet connection is bad you will
187-
// have to wait for all HEAD request, for your `markdownToHtml` to
188-
// complete. This could take a noticeable amount of time.
189-
// For this reason we will stick to doing it the `dumb` way by just getting
190-
// it from the URL extension
191-
const extension = URL.parse(transclusion.url)?.pathname.split(".").pop();
181+
export function getMimeTypeFromUrl(
182+
url: string,
183+
allowExternal: boolean,
184+
): string | null {
185+
if (!isLocalURL(url) && allowExternal) {
186+
// Remote URL: determine mime type from the URL extension
187+
const extension = URL.parse(url)?.pathname.split(".").pop();
192188
if (extension) {
193-
mimeType = mime.getType(extension);
194-
}
195-
} else {
196-
const ref = parseToRef(transclusion.url);
197-
if (!ref) {
198-
throw Error(`Failed to parse url: ${transclusion.url}`);
189+
return mime.getType(extension);
199190
}
191+
return null;
192+
}
200193

201-
mimeType = mime.getType(getPathExtension(ref.path));
194+
const ref = parseToRef(url);
195+
if (!ref) {
196+
throw Error(`Failed to parse url: ${url}`);
202197
}
203198

199+
return mime.getType(getPathExtension(ref.path));
200+
}
201+
202+
/**
203+
* Sanitize a transclusion URL for use in HTML elements.
204+
* Local URLs get prefixed with the fs endpoint.
205+
*/
206+
function sanitizeTransclusionUrl(url: string): string {
207+
return isLocalURL(url)
208+
? `${fsEndpoint.slice(1)}/${url.replace(":", "%3A")}`
209+
: url;
210+
}
211+
212+
/**
213+
* Create an HTML element for media transclusions (image/video/audio/pdf).
214+
* Returns null for markdown content or unknown MIME types.
215+
*/
216+
export function createMediaElement(
217+
transclusion: Transclusion,
218+
): HTMLElement | null {
219+
const allowExternal = transclusion.linktype !== "wikilink";
220+
const mimeType = getMimeTypeFromUrl(transclusion.url, allowExternal);
221+
204222
if (!mimeType) {
205-
throw Error(`Failed to determine mime type for ${transclusion.url}`);
223+
return null;
206224
}
207225

208226
const style =
@@ -214,28 +232,25 @@ export function inlineContentFromURL(
214232
? `height: ${transclusion.dimension.height}px;`
215233
: "");
216234

217-
// If the URL is a local, prefix it with /.fs and encode the : so that it's not interpreted as a protocol
218-
const sanitizedFsUrl = isLocalURL(transclusion.url)
219-
? `${fsEndpoint.slice(1)}/${transclusion.url.replace(":", "%3A")}`
220-
: transclusion.url;
235+
const sanitizedUrl = sanitizeTransclusionUrl(transclusion.url);
221236

222237
if (mimeType.startsWith("image/")) {
223238
const img = document.createElement("img");
224-
img.src = sanitizedFsUrl;
239+
img.src = sanitizedUrl;
225240
img.alt = transclusion.alias;
226241
img.style = style;
227242
return img;
228243
} else if (mimeType.startsWith("video/")) {
229244
const video = document.createElement("video");
230-
video.src = sanitizedFsUrl;
245+
video.src = sanitizedUrl;
231246
video.title = transclusion.alias;
232247
video.controls = true;
233248
video.autoplay = false;
234249
video.style = style;
235250
return video;
236251
} else if (mimeType.startsWith("audio/")) {
237252
const audio = document.createElement("audio");
238-
audio.src = sanitizedFsUrl;
253+
audio.src = sanitizedUrl;
239254
audio.title = transclusion.alias;
240255
audio.controls = true;
241256
audio.autoplay = false;
@@ -244,26 +259,45 @@ export function inlineContentFromURL(
244259
} else if (mimeType === "application/pdf") {
245260
const embed = document.createElement("object");
246261
embed.type = mimeType;
247-
embed.data = sanitizedFsUrl;
262+
embed.data = sanitizedUrl;
248263
embed.style.width = "100%";
249264
embed.style.height = "20em";
250265
embed.style = style;
251266
return embed;
252-
} else if (mimeType === "text/markdown") {
253-
if (!isLocalURL(transclusion.url) && allowExternal) {
254-
throw Error(`Transcluding markdown from external sources is not allowed`);
255-
}
267+
}
256268

257-
const ref = parseToRef(transclusion.url);
258-
if (!ref || !isMarkdownPath(ref.path)) {
259-
// We can be fairly sure this can't happen, but just be sure
260-
throw Error(
261-
`Couldn't transclude markdown, invalid path: ${transclusion.url}`,
262-
);
263-
}
269+
return null;
270+
}
264271

265-
return space.readRef(ref);
266-
} else {
267-
return { text: `File has unsupported mimeType: ${mimeType}`, offset: 0 };
272+
/**
273+
* Read markdown transclusion content from space.
274+
* Throws for non-markdown MIME types or invalid paths.
275+
*/
276+
export async function readTransclusionContent(
277+
space: Space,
278+
transclusion: Transclusion,
279+
): Promise<OffsetText> {
280+
const allowExternal = transclusion.linktype !== "wikilink";
281+
const mimeType = getMimeTypeFromUrl(transclusion.url, allowExternal);
282+
283+
if (!mimeType) {
284+
throw Error(`Failed to determine mime type for ${transclusion.url}`);
285+
}
286+
287+
if (mimeType !== "text/markdown") {
288+
throw Error(`File has unsupported mimeType: ${mimeType}`);
268289
}
290+
291+
if (!isLocalURL(transclusion.url) && allowExternal) {
292+
throw Error(`Transcluding markdown from external sources is not allowed`);
293+
}
294+
295+
const ref = parseToRef(transclusion.url);
296+
if (!ref || !isMarkdownPath(ref.path)) {
297+
throw Error(
298+
`Couldn't transclude markdown, invalid path: ${transclusion.url}`,
299+
);
300+
}
301+
302+
return space.readRef(ref);
269303
}

client/markdown_renderer/markdown_render.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import * as TagConstants from "../../plugs/index/constants.ts";
1818
import { extractHashtag } from "@silverbulletmd/silverbullet/lib/tags";
1919
import { justifiedTableRender } from "./justified_tables.ts";
2020
import type { PageMeta } from "@silverbulletmd/silverbullet/type/index";
21-
import { inlineContentFromURL } from "./inline.ts";
21+
import { createMediaElement } from "./inline.ts";
2222
import { parseTransclusion } from "@silverbulletmd/silverbullet/lib/transclusion";
2323

2424
export type MarkdownRenderOptions = {
@@ -263,22 +263,14 @@ function render(t: ParseTree, options: MarkdownRenderOptions = {}): Tag | null {
263263
}
264264

265265
try {
266-
const result = inlineContentFromURL(client.space, transclusion);
267-
if (result instanceof Promise) {
268-
// console.warn(
269-
// `Unsupported inline in markdown render context: ${transclusion.url}`,
270-
// );
271-
// Can't support promises in this context, returning original text
272-
return text;
273-
}
274-
// Running in non-browser context
275-
if (!globalThis.HTMLElement || !(result instanceof HTMLElement)) {
266+
const element = createMediaElement(transclusion);
267+
if (!element) {
276268
return text;
277269
}
278270

279271
return {
280-
name: result.tagName,
281-
attrs: Array.from(result.attributes).reduce(
272+
name: element.tagName,
273+
attrs: Array.from(element.attributes).reduce(
282274
(obj, attr) => {
283275
obj[attr.name] = attr.value;
284276
return obj;

0 commit comments

Comments
 (0)