Skip to content

Commit 28a07df

Browse files
committed
markdown: Render inline images as small lightbox previews in place.
Images added using the `![alt text](url)` syntax are rendered in a fixed size, expandable via lightbox, exactly like the linked image previews, except inline. For drafts which are only rendered by the frontend markdown processor, images are rendered as a regular link, for now.
1 parent d797e53 commit 28a07df

File tree

7 files changed

+61
-6
lines changed

7 files changed

+61
-6
lines changed

web/src/compose.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,10 @@ export function render_and_show_preview($preview_spinner, $preview_content_box,
341341
rendered_preview_html = rendered_content;
342342
}
343343

344-
$preview_content_box.html(util.clean_user_content_links(rendered_preview_html));
344+
rendered_preview_html = util.make_inline_images_lightbox_previewable(rendered_preview_html);
345+
rendered_preview_html = util.clean_user_content_links(rendered_preview_html);
346+
347+
$preview_content_box.html(rendered_preview_html);
345348
rendered_markdown.update_elements($preview_content_box);
346349
}
347350

web/src/markdown.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ export function parse({
709709
return "<br>\n";
710710
};
711711

712+
// Treat images as links, because only the backend markdown processor
713+
// can handle them correctly.
714+
renderer.image = renderer.link;
715+
712716
function preprocess_code_blocks(src: string): string {
713717
return fenced_code.process_fenced_code(src);
714718
}

web/src/templates.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,11 @@ Handlebars.registerHelper("tr", function (options) {
110110
return new Handlebars.SafeString(result);
111111
});
112112

113-
Handlebars.registerHelper(
114-
"rendered_markdown",
115-
(content) => new Handlebars.SafeString(util.clean_user_content_links(content)),
116-
);
113+
Handlebars.registerHelper("rendered_markdown", (content) => {
114+
content = util.make_inline_images_lightbox_previewable(content);
115+
content = util.clean_user_content_links(content);
116+
return new Handlebars.SafeString(content);
117+
});
117118

118119
Handlebars.registerHelper("numberFormat", (number) => number.toLocaleString());
119120

web/src/util.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,36 @@ export function canonicalize_stream_synonyms(text: string): string {
300300

301301
let inertDocument: Document | undefined;
302302

303+
export function make_inline_images_lightbox_previewable(html: string): string {
304+
if (inertDocument === undefined) {
305+
inertDocument = new DOMParser().parseFromString("", "text/html");
306+
}
307+
const template = inertDocument.createElement("template");
308+
template.innerHTML = html;
309+
310+
for (const img of template.content.querySelectorAll("img")) {
311+
if (img.parentElement?.tagName === "A") {
312+
// skip images that are already previewable
313+
continue;
314+
}
315+
const anchor = inertDocument.createElement("a");
316+
const src = img.getAttribute("src");
317+
if (src) {
318+
anchor.setAttribute("href", src);
319+
}
320+
const alt = img.getAttribute("alt");
321+
if (alt) {
322+
anchor.setAttribute("title", alt);
323+
}
324+
anchor.append(img.cloneNode(true));
325+
const span = inertDocument.createElement("span");
326+
span.classList.add("message_inline_image", "true_inline");
327+
span.append(anchor);
328+
img.parentNode?.replaceChild(span, img);
329+
}
330+
return template.innerHTML;
331+
}
332+
303333
export function clean_user_content_links(html: string): string {
304334
if (inertDocument === undefined) {
305335
inertDocument = new DOMParser().parseFromString("", "text/html");

web/styles/rendered_markdown.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@
397397
}
398398
}
399399

400+
.message_inline_image.true_inline {
401+
margin-bottom: unset;
402+
}
403+
400404
&.rtl .twitter-image,
401405
&.rtl .message_inline_image {
402406
margin-left: unset;
@@ -408,7 +412,8 @@
408412
formed with EDITED/MOVED markers and the
409413
timestamp when the first child of the rendered
410414
markdown is a media element. */
411-
&:has(> .message_inline_image:first-child) {
415+
&:has(> .message_inline_image:first-child),
416+
&:has(> p:first-child > .message_inline_image) {
412417
align-self: center;
413418
}
414419

web/tests/util.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,17 @@ run_test("clean_user_content_links", () => {
326326
);
327327
});
328328

329+
run_test("make_inline_images_lightbox_previewable", () => {
330+
assert.equal(
331+
util.make_inline_images_lightbox_previewable(
332+
'<img src="/user_uploads/2/ae/7BJKQei4DMm0UDL7EBxECPL0/zebra.gif" alt="zebra">\n\n' +
333+
'<span class="message_inline_image"><a href="/user_uploads/test.png" title="linked"><img src="/user_uploads/test.png" alt="linked"></a></span>',
334+
),
335+
'<span class="message_inline_image true_inline"><a href="/user_uploads/2/ae/7BJKQei4DMm0UDL7EBxECPL0/zebra.gif" title="zebra"><img src="/user_uploads/2/ae/7BJKQei4DMm0UDL7EBxECPL0/zebra.gif" alt="zebra"></a></span>\n\n' +
336+
'<span class="message_inline_image"><a href="/user_uploads/test.png" title="linked"><img src="/user_uploads/test.png" alt="linked"></a></span>',
337+
);
338+
});
339+
329340
run_test("filter_by_word_prefix_match", () => {
330341
const strings = ["stream-hyphen_underscore/slash", "three word stream"];
331342
const values = [0, 1];

web/third/marked/lib/marked.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export class Renderer {
1010
code: (code: string) => string;
1111
link: (href: string, title: string, text: string) => string;
1212
br: () => string;
13+
image: (href: string, title: string, text: string) => string;
1314
}
1415

1516
export type RegExpOrStub = RegExp | {

0 commit comments

Comments
 (0)