commit e20af5c68b21048cf0a2cbbc082c3b02fbccbd33
parent 42cbc5736aa6f9293d6013d8a98988aca1a58d45
Author: breadcat <breadcat@users.noreply.github.com>
Date: Fri, 24 Jul 2026 16:37:06 +0100
Support markdown and bare links in description
Diffstat:
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/readme.md b/readme.md
@@ -31,6 +31,7 @@ Then access the server via a web browser, the default port is 8080.
* Regenerate video thumbnails via web interface
* Add files via local upload, remote upload or `yt-dlp` directly
* Clickable [rotate90](## "Rotates video/image contents by angle on click"), [l45](## "Jumps to line number in text viewer on click"), [01:23](## "Jumps video playback to specified timestamp on click") and [file/1234](## "Clickable link to that file ID") shortcodes in file descriptions
+* Clickable markdown-style links and bare URLs in file descriptions
* Artbitrary searchable descriptions on files
* Raw file URI copying for external application access
* In browser file management (delete, rename)
diff --git a/static/timestamps.js b/static/timestamps.js
@@ -18,16 +18,41 @@ function makeTimestampsClickable(containerId, videoId, imageId) {
const timestampRegex = /\[(\d{1,2}(?::\d{2}){0,2})\]/g;
// Regex for rotations: [rotate90], [rotate180], [rotate270], [rotate0]
const rotateRegex = /\[rotate(0|90|180|270)\]/g;
+ // Regex for URLs: http(s):// or www. links
+ const urlRegex = /\b((?:https?:\/\/|www\.)[^\s<]+)/gi;
+ // Regex for markdown-style links: [link text](https://example.com)
+ const markdownLinkRegex = /\[([^\[\]]+)\]\(((?:https?:\/\/|www\.)[^)\s]+)\)/gi;
- // Replace timestamps
- container.innerHTML = container.innerHTML
+ // Pull out markdown links first and swap in placeholders, so the raw URL
+ // inside them doesn't get double-processed by the bare-URL regex below.
+ const mdLinks = [];
+ let html = container.innerHTML.replace(markdownLinkRegex, (match, text, url) => {
+ const href = url.startsWith("http") ? url : `https://${url}`;
+ const placeholder = `\u0000MDLINK${mdLinks.length}\u0000`;
+ mdLinks.push(`<a href="${href}" class="external-link" target="_blank" rel="noopener noreferrer">${text}</a>`);
+ return placeholder;
+ });
+
+ // Replace bare URLs, then timestamps, then rotations
+ html = html
+ .replace(urlRegex, (match) => {
+ // Strip common trailing punctuation that isn't part of the URL
+ const trailingPunct = /[.,;:!?)\]"'>]+$/;
+ const trimmed = match.match(trailingPunct) ? match.replace(trailingPunct, "") : match;
+ const suffix = match.slice(trimmed.length);
+ const href = trimmed.startsWith("http") ? trimmed : `https://${trimmed}`;
+ return `<a href="${href}" class="external-link" target="_blank" rel="noopener noreferrer">${trimmed}</a>${suffix}`;
+ })
.replace(timestampRegex, (match, ts) => {
const seconds = parseTimestamp(ts);
return `<a href="#" class="timestamp" data-time="${seconds}">${match}</a>`;
})
.replace(rotateRegex, (match, angle) => {
return `<a href="#" class="rotate" data-angle="${angle}">${match}</a>`;
- });
+ });
+
+ // Restore the markdown links now that no other regex can touch them
+ container.innerHTML = html.replace(/\u0000MDLINK(\d+)\u0000/g, (_, i) => mdLinks[Number(i)]);
// Handle clicks
container.addEventListener("click", e => {
if (e.target.classList.contains("timestamp")) {