summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2026-01-13 20:21:16 +0100
committerDawid Rycerz <dawid@rycerz.xyz>2026-01-13 20:21:16 +0100
commitf38a0cb8446201cd6c937d96da33b58b4427c78f (patch)
tree20225ce0df0f95e094a01650a3f7477c5032cd2c /src
parentb6e440699e9fca474869bf74ce09f2310f05c620 (diff)
Add alt texts rendering
Diffstat (limited to 'src')
-rw-r--r--src/loaders/pleroma.ts22
-rw-r--r--src/plugins/rehype-image-captions.ts78
-rw-r--r--src/utils/markdown.ts21
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);
}