@@ -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" ;
1417import mime from "mime" ;
1518import { LuaStackFrame , LuaTable } from "../space_lua/runtime.ts" ;
1619import { 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}
0 commit comments