new functions and fixes
This commit is contained in:
+80
-3
@@ -43,6 +43,51 @@ function safeUrl(value) {
|
||||
}
|
||||
}
|
||||
|
||||
function safeAttachmentUrl(file, { download = false } = {}) {
|
||||
let raw = String(file?.url || "").trim();
|
||||
const route = attachmentRoute(raw);
|
||||
if (route && markdownFileRoutes.has(route)) raw = markdownFileRoutes.get(route);
|
||||
try {
|
||||
const url = new URL(raw, location.origin);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return "#";
|
||||
if (download && route) url.searchParams.set("download", "1");
|
||||
return escapeHtml(url.href);
|
||||
} catch {
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
|
||||
function safeAttachmentPlaybackUrl(file) {
|
||||
return safeAttachmentUrl(file);
|
||||
}
|
||||
|
||||
function safeAttachmentDownloadUrl(file) {
|
||||
return safeAttachmentUrl(file, { download: true });
|
||||
}
|
||||
|
||||
function youtubeVideo(value) {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw || raw.startsWith("//")) return null;
|
||||
try {
|
||||
const url = new URL(raw, location.origin);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
||||
const host = url.hostname.toLowerCase();
|
||||
let id = "";
|
||||
if (host === "youtu.be") {
|
||||
id = url.pathname.split("/").filter(Boolean)[0] || "";
|
||||
} else if (host === "youtube.com" || host.endsWith(".youtube.com") || host === "youtube-nocookie.com" || host.endsWith(".youtube-nocookie.com")) {
|
||||
if (url.pathname === "/watch") id = url.searchParams.get("v") || "";
|
||||
else {
|
||||
const match = url.pathname.match(/^\/(?:shorts|embed|live)\/([A-Za-z0-9_-]+)/);
|
||||
id = match?.[1] || "";
|
||||
}
|
||||
}
|
||||
return /^[A-Za-z0-9_-]{6,20}$/.test(id) ? { id, url: url.href } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setMarkdownFiles(files) {
|
||||
const normalized = (Array.isArray(files) ? files : [])
|
||||
.filter(file => file && file.filename && file.url)
|
||||
@@ -60,7 +105,7 @@ export function setMarkdownFiles(files) {
|
||||
export function unresolvedMarkdownFileAliases(value) {
|
||||
const missing = new Set();
|
||||
const source = String(value || "").replace(/`[^`]*`/g, "");
|
||||
for (const match of source.matchAll(/\[(?:file|image|img)=([^,\]\s]+)(?:,[^\]]*)?\]/gi)) {
|
||||
for (const match of source.matchAll(/\[(?:file|image|img|video)=([^,\]\s]+)(?:,[^\]]*)?\]/gi)) {
|
||||
if (!markdownFiles.has(match[1])) missing.add(match[1]);
|
||||
}
|
||||
return [...missing];
|
||||
@@ -76,13 +121,18 @@ function inline(value) {
|
||||
let html = escapeHtml(value);
|
||||
|
||||
html = html.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${code}</code>`));
|
||||
html = html.replace(/\[(file|image|img)=([^,\]\s]+)(?:,([^\]]*))?\]/gi, (match, kind, filename, label) => {
|
||||
html = html.replace(/\[(file|image|img|video)=([^,\]\s]+)(?:,([^\]]*))?\]/gi, (match, kind, filename, label) => {
|
||||
const normalizedKind = kind.toLowerCase();
|
||||
const file = markdownFiles.get(filename);
|
||||
if (!file) return match;
|
||||
if (normalizedKind === "file") {
|
||||
const text = String(label || filename).trim() || filename;
|
||||
return stash(`<a href="${safeUrl(file.url)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-file-alias="file" data-file-name="${filename}">${text}</a>`);
|
||||
return stash(`<a href="${safeAttachmentDownloadUrl(file)}" download="${escapeHtml(filename)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-file-alias="file" data-file-name="${escapeHtml(filename)}">${text}</a>`);
|
||||
}
|
||||
if (normalizedKind === "video") {
|
||||
if (!file.mimeType.startsWith("video/")) return match;
|
||||
const text = String(label || filename).trim() || filename;
|
||||
return stash(`<a href="${safeAttachmentPlaybackUrl(file)}" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer" data-file-alias="video" data-file-name="${escapeHtml(filename)}">${text}</a>`);
|
||||
}
|
||||
if (!file.mimeType.startsWith("image/")) return match;
|
||||
|
||||
@@ -162,6 +212,26 @@ function normalizeLanguage(value) {
|
||||
const attrs = (line, editable = false, prefix = "", suffix = "", lineOffset = 0) => ` class="preview-source-line${editable ? " preview-editable" : ""}" data-source-line="${line + lineOffset + 1}"${editable ? ` data-source-prefix="${escapeHtml(prefix)}" data-source-suffix="${escapeHtml(suffix)}"` : ""}`;
|
||||
const isPlainText = line => !/[`*_~^=\[\]<>|:#]/.test(line) && !/^\s*(?:[-+*>]|\d+\.)\s/.test(line);
|
||||
|
||||
function renderStandaloneMedia(line, sourceLine) {
|
||||
const videoAlias = String(line).trim().match(/^\[video=([^,\]\s]+)(?:,([^\]]*))?\]$/i);
|
||||
if (videoAlias) {
|
||||
const filename = videoAlias[1];
|
||||
const file = markdownFiles.get(filename);
|
||||
if (!file || !file.mimeType.startsWith("video/")) return null;
|
||||
const label = String(videoAlias[2] || filename).trim() || filename;
|
||||
const playbackUrl = safeAttachmentPlaybackUrl(file);
|
||||
const downloadUrl = safeAttachmentDownloadUrl(file);
|
||||
return `<div class="rustpad-media preview-source-line" data-source-line="${sourceLine}" contenteditable="false"><video class="rustpad-media__player" data-rustpad-player data-player-kind="video" controls playsinline preload="metadata" aria-label="${escapeHtml(label)}"><source src="${playbackUrl}" type="${escapeHtml(file.mimeType)}"></video><p class="rustpad-media__fallback" hidden>Playback is unavailable. <a href="${downloadUrl}" download="${escapeHtml(filename)}">Download ${escapeHtml(label)}</a>.</p></div>`;
|
||||
}
|
||||
|
||||
const trimmed = String(line).trim();
|
||||
const markdownLink = trimmed.match(/^\[([^\]]+)\]\(([^\s)]+)(?:\s+["'][^"']*["'])?\)$/);
|
||||
const candidate = markdownLink ? markdownLink[2] : trimmed;
|
||||
const youtube = youtubeVideo(candidate);
|
||||
if (!youtube) return null;
|
||||
const title = markdownLink?.[1]?.trim() || "YouTube video";
|
||||
return `<div class="rustpad-media preview-source-line" data-source-line="${sourceLine}" contenteditable="false"><div class="rustpad-media__player" data-rustpad-player data-player-kind="youtube" data-video-id="${escapeHtml(youtube.id)}" data-player-title="${escapeHtml(title)}"><p class="rustpad-media__fallback"><a href="${safeUrl(youtube.url)}" target="_blank" rel="noopener noreferrer">Open ${escapeHtml(title)}</a></p></div></div>`;
|
||||
}
|
||||
|
||||
function listLine(line) {
|
||||
const match = line.match(/^(\s*)([-*+]|(\d+)\.)\s+(?:\[([ xX])\]\s+)?(.+)$/);
|
||||
@@ -336,6 +406,13 @@ export function renderMarkdown(source, lineOffset = 0) {
|
||||
}
|
||||
if (inCode) { code.push(line); continue; }
|
||||
|
||||
const standaloneMedia = renderStandaloneMedia(line, index + lineOffset + 1);
|
||||
if (standaloneMedia) {
|
||||
closeList();
|
||||
html += standaloneMedia;
|
||||
continue;
|
||||
}
|
||||
|
||||
const delimiter = index + 1 < lines.length ? tableDelimiter(lines[index + 1]) : null;
|
||||
if (line.includes("|") && delimiter) {
|
||||
closeList();
|
||||
|
||||
Reference in New Issue
Block a user