mirror of
https://github.com/docmost/docmost.git
synced 2026-05-21 17:22:54 +08:00
feat(editor): add alt text support for images (#2097)
* feat(editor): add alt text support for images * feat: extend alt text support to videos and diagrams --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
This commit is contained in:
@@ -28,6 +28,7 @@ export interface DrawioOptions {
|
||||
export interface DrawioAttributes {
|
||||
src?: string;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
@@ -79,6 +80,13 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
"data-title": attributes.title,
|
||||
}),
|
||||
},
|
||||
alt: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("data-alt"),
|
||||
renderHTML: (attributes: DrawioAttributes) => ({
|
||||
"data-alt": attributes.alt,
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
@@ -155,7 +163,7 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
"img",
|
||||
{
|
||||
src: HTMLAttributes["data-src"],
|
||||
alt: HTMLAttributes["data-title"],
|
||||
alt: HTMLAttributes["data-alt"] || HTMLAttributes["data-title"],
|
||||
width: HTMLAttributes["data-width"],
|
||||
},
|
||||
],
|
||||
@@ -226,7 +234,7 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
|
||||
const el = document.createElement("img");
|
||||
el.src = normalizeFileUrl(node.attrs.src);
|
||||
el.alt = node.attrs.title || "";
|
||||
el.alt = node.attrs.alt || node.attrs.title || "";
|
||||
el.style.display = "block";
|
||||
el.style.maxWidth = "100%";
|
||||
el.style.borderRadius = "8px";
|
||||
@@ -264,6 +272,14 @@ export const Drawio = Node.create<DrawioOptions>({
|
||||
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||
}
|
||||
|
||||
if (
|
||||
updatedNode.attrs.alt !== currentNode.attrs.alt ||
|
||||
updatedNode.attrs.title !== currentNode.attrs.title
|
||||
) {
|
||||
el.alt =
|
||||
updatedNode.attrs.alt || updatedNode.attrs.title || "";
|
||||
}
|
||||
|
||||
const w = updatedNode.attrs.width;
|
||||
const h = updatedNode.attrs.height;
|
||||
if (w != null) {
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ExcalidrawOptions {
|
||||
export interface ExcalidrawAttributes {
|
||||
src?: string;
|
||||
title?: string;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
@@ -79,6 +80,13 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
"data-title": attributes.title,
|
||||
}),
|
||||
},
|
||||
alt: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("data-alt"),
|
||||
renderHTML: (attributes: ExcalidrawAttributes) => ({
|
||||
"data-alt": attributes.alt,
|
||||
}),
|
||||
},
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: (element) => {
|
||||
@@ -155,7 +163,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
"img",
|
||||
{
|
||||
src: HTMLAttributes["data-src"],
|
||||
alt: HTMLAttributes["data-title"],
|
||||
alt: HTMLAttributes["data-alt"] || HTMLAttributes["data-title"],
|
||||
width: HTMLAttributes["data-width"],
|
||||
},
|
||||
],
|
||||
@@ -226,7 +234,7 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
|
||||
const el = document.createElement("img");
|
||||
el.src = normalizeFileUrl(node.attrs.src);
|
||||
el.alt = node.attrs.title || "";
|
||||
el.alt = node.attrs.alt || node.attrs.title || "";
|
||||
el.style.display = "block";
|
||||
el.style.maxWidth = "100%";
|
||||
el.style.borderRadius = "8px";
|
||||
@@ -264,6 +272,14 @@ export const Excalidraw = Node.create<ExcalidrawOptions>({
|
||||
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||
}
|
||||
|
||||
if (
|
||||
updatedNode.attrs.alt !== currentNode.attrs.alt ||
|
||||
updatedNode.attrs.title !== currentNode.attrs.title
|
||||
) {
|
||||
el.alt =
|
||||
updatedNode.attrs.alt || updatedNode.attrs.title || "";
|
||||
}
|
||||
|
||||
const w = updatedNode.attrs.width;
|
||||
const h = updatedNode.attrs.height;
|
||||
if (w != null) {
|
||||
|
||||
@@ -5,6 +5,13 @@ import { getBasename } from './basename';
|
||||
// CJS/ESM interop: .default exists in Vite, not in NestJS
|
||||
const TurndownService = (_TurndownService as any).default || _TurndownService;
|
||||
|
||||
function sanitizeMdLinkText(value: string): string {
|
||||
return value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/([\[\]!])/g, '\\$1')
|
||||
.replace(/[\r\n]+/g, ' ');
|
||||
}
|
||||
|
||||
export function htmlToMarkdown(html: string): string {
|
||||
const turndownService = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
@@ -25,6 +32,7 @@ export function htmlToMarkdown(html: string): string {
|
||||
mathInline,
|
||||
mathBlock,
|
||||
iframeEmbed,
|
||||
image,
|
||||
video,
|
||||
]);
|
||||
return turndownService.turndown(html).replaceAll('<br>', ' ');
|
||||
@@ -181,6 +189,20 @@ function iframeEmbed(turndownService: _TurndownService) {
|
||||
});
|
||||
}
|
||||
|
||||
function image(turndownService: _TurndownService) {
|
||||
turndownService.addRule('image', {
|
||||
filter: 'img',
|
||||
replacement: function (_content: string, node: HTMLInputElement) {
|
||||
const src = node.getAttribute('src') || '';
|
||||
if (!src) return '';
|
||||
const alt = sanitizeMdLinkText(node.getAttribute('alt') || '');
|
||||
const title = node.getAttribute('title') || '';
|
||||
const titlePart = title ? ' "' + title.replace(/"/g, '\\"') + '"' : '';
|
||||
return '';
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function video(turndownService: _TurndownService) {
|
||||
turndownService.addRule('video', {
|
||||
filter: function (node: HTMLInputElement) {
|
||||
@@ -188,7 +210,10 @@ function video(turndownService: _TurndownService) {
|
||||
},
|
||||
replacement: function (_content: string, node: HTMLInputElement) {
|
||||
const src = node.getAttribute('src') || '';
|
||||
const name = getBasename(src) || src;
|
||||
const ariaLabel = node.getAttribute('aria-label');
|
||||
const name = sanitizeMdLinkText(
|
||||
ariaLabel || getBasename(src) || src,
|
||||
);
|
||||
return '[' + name + '](' + src + ')';
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface VideoOptions {
|
||||
|
||||
export interface VideoAttributes {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
align?: string;
|
||||
attachmentId?: string;
|
||||
size?: number;
|
||||
@@ -79,6 +80,13 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
||||
src: attributes.src,
|
||||
}),
|
||||
},
|
||||
alt: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("aria-label"),
|
||||
renderHTML: (attributes: VideoAttributes) => ({
|
||||
"aria-label": attributes.alt,
|
||||
}),
|
||||
},
|
||||
attachmentId: {
|
||||
default: undefined,
|
||||
parseHTML: (element) => element.getAttribute("data-attachment-id"),
|
||||
@@ -228,6 +236,9 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
||||
el.src = normalizeFileUrl(node.attrs.src);
|
||||
el.controls = true;
|
||||
el.preload = "metadata";
|
||||
if (node.attrs.alt) {
|
||||
el.setAttribute("aria-label", node.attrs.alt);
|
||||
}
|
||||
el.style.display = "block";
|
||||
el.style.maxWidth = "100%";
|
||||
el.style.borderRadius = "8px";
|
||||
@@ -272,6 +283,14 @@ export const TiptapVideo = Node.create<VideoOptions>({
|
||||
el.src = normalizeFileUrl(updatedNode.attrs.src);
|
||||
}
|
||||
|
||||
if (updatedNode.attrs.alt !== currentNode.attrs.alt) {
|
||||
if (updatedNode.attrs.alt) {
|
||||
el.setAttribute("aria-label", updatedNode.attrs.alt);
|
||||
} else {
|
||||
el.removeAttribute("aria-label");
|
||||
}
|
||||
}
|
||||
|
||||
const w = updatedNode.attrs.width;
|
||||
const h = updatedNode.attrs.height;
|
||||
if (w != null) {
|
||||
|
||||
Reference in New Issue
Block a user