diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/loaders/pleroma.ts | 22 | ||||
| -rw-r--r-- | src/plugins/rehype-image-captions.ts | 78 | ||||
| -rw-r--r-- | src/utils/markdown.ts | 21 |
3 files changed, 113 insertions, 8 deletions
diff --git a/src/loaders/pleroma.ts b/src/loaders/pleroma.ts index d633aa0..766e9ee 100644 --- a/src/loaders/pleroma.ts +++ b/src/loaders/pleroma.ts @@ -531,6 +531,18 @@ function buildImageGridHtml(attachments: PleromaMediaAttachment[]): string { if (count === 1 && imageAttachments[0]) { const attachment = imageAttachments[0]; const description = attachment.description || "Image"; + // Only wrap with figcaption if we have descriptive alt text + const hasDescriptiveAlt = + description && description.trim() && description.toLowerCase() !== "image"; + + if (hasDescriptiveAlt) { + return ` +<figure class="image-with-alt group relative mt-4 mb-4"> +<img src="${attachment.url}" alt="${description}" class="w-full cursor-zoom-in rounded-lg border border-gray-200 object-cover transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable /> +<figcaption class="pointer-events-none absolute bottom-0 left-0 right-0 rounded-b-lg bg-black/70 px-3 py-2 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100">${description}</figcaption> +</figure>`; + } + return ` <div class="mt-4 mb-4"> <img src="${attachment.url}" alt="${description}" class="w-full cursor-zoom-in rounded-lg border border-gray-200 object-cover transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable /> @@ -542,9 +554,19 @@ function buildImageGridHtml(attachments: PleromaMediaAttachment[]): string { const images = imageAttachments .map((attachment, index) => { const description = attachment.description || "Image"; + const hasDescriptiveAlt = + description && description.trim() && description.toLowerCase() !== "image"; // For odd count, the last image spans all columns const isLastImage = index === count - 1; const spanClass = isOdd && isLastImage ? "sm:col-span-2" : ""; + + if (hasDescriptiveAlt) { + return `<figure class="image-with-alt group relative h-fit ${spanClass}"> +<img src="${attachment.url}" alt="${description}" class="w-full max-h-96 object-cover cursor-zoom-in rounded-lg border border-gray-200 transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600" loading="lazy" data-zoomable /> +<figcaption class="pointer-events-none absolute bottom-0 left-0 right-0 rounded-b-lg bg-black/70 px-3 py-2 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100">${description}</figcaption> +</figure>`; + } + return `<img src="${attachment.url}" alt="${description}" class="w-full max-h-96 object-cover cursor-zoom-in rounded-lg border border-gray-200 transition-colors hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600 ${spanClass}" loading="lazy" data-zoomable />`; }) .join("\n"); diff --git a/src/plugins/rehype-image-captions.ts b/src/plugins/rehype-image-captions.ts new file mode 100644 index 0000000..61bdd84 --- /dev/null +++ b/src/plugins/rehype-image-captions.ts @@ -0,0 +1,78 @@ +import type { Element, Root } from "hast"; +import { h } from "hastscript"; +import type { Plugin } from "unified"; +import { visit } from "unist-util-visit"; + +/** + * Rehype plugin that wraps images with figure/figcaption to display alt text on hover. + * Skips images without alt text or with generic alt text like "Image". + */ +export const rehypeImageCaptions: Plugin<[], Root> = () => { + return (tree: Root) => { + visit(tree, "element", (node: Element, index, parent) => { + // Only process img elements that have a parent and an index + if (node.tagName !== "img" || !parent || index === undefined) { + return; + } + + // Get the alt text + const alt = node.properties?.alt; + if (!alt || typeof alt !== "string") { + return; + } + + // Skip generic or empty alt text + const trimmedAlt = alt.trim(); + if (!trimmedAlt || trimmedAlt.toLowerCase() === "image") { + return; + } + + // Preserve all existing properties including data-zoomable for medium-zoom + const imgProperties = { ...node.properties }; + + // Create the figcaption element with tailwind classes for hover overlay + const figcaption = h( + "figcaption", + { + class: [ + "pointer-events-none", + "absolute", + "bottom-0", + "left-0", + "right-0", + "rounded-b-lg", + "bg-black/70", + "px-3", + "py-2", + "text-sm", + "text-white", + "opacity-0", + "transition-opacity", + "group-hover:opacity-100", + ].join(" "), + }, + [{ type: "text", value: trimmedAlt }], + ); + + // Create the figure wrapper with group class for hover trigger + const figure = h( + "figure", + { + class: "image-with-alt group relative", + }, + [ + { + type: "element", + tagName: "img", + properties: imgProperties, + children: [], + }, + figcaption, + ], + ); + + // Replace the img with the figure in the parent + parent.children[index] = figure; + }); + }; +}; diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 946d8d8..eaac6e7 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -1,11 +1,16 @@ -import { marked } from "marked"; - -marked.setOptions({ - breaks: true, - gfm: true, -}); +import rehypeStringify from "rehype-stringify"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; +import { rehypeImageCaptions } from "@/plugins/rehype-image-captions"; export function markdownToHtml(markdown: string): string { - const html = marked.parse(markdown); - return typeof html === "string" ? html : ""; + const result = unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeImageCaptions) + .use(rehypeStringify, { allowDangerousHtml: true }) + .processSync(markdown); + + return String(result); } |
