From d3b6121aee6f84255481d8de8f0d04c269d49262 Mon Sep 17 00:00:00 2001 From: separatrix Date: Tue, 21 Jul 2026 11:43:22 +0300 Subject: [PATCH 1/3] feat: passing the resolveImageSrc parameter --- .../src/extensions/yfm/ImgSize/index.ts | 7 +++- .../plugins/ImgSizeNodeView/NodeView.tsx | 10 ++++- .../ImgSize/plugins/ImgSizeNodeView/index.ts | 37 ++++++++++--------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/packages/editor/src/extensions/yfm/ImgSize/index.ts b/packages/editor/src/extensions/yfm/ImgSize/index.ts index 5ede6fba0..394813bdd 100644 --- a/packages/editor/src/extensions/yfm/ImgSize/index.ts +++ b/packages/editor/src/extensions/yfm/ImgSize/index.ts @@ -13,6 +13,11 @@ export type ImgSizeOptions = ImgSizeSpecsOptions & { * @default false */ needToSetDimensionsForUploadedImages?: boolean; + /** + * Transforms the image src before rendering, e.g. to resolve a relative path. + * Affects only the displayed src; the node attrs and markup are left intact. + */ + resolveImageSrc?: (src: string) => string; } & Pick< ImagePasteOptions, 'imageUploadHandler' | 'parseInsertedUrlAsImage' | 'enableNewImageSizeCalculation' @@ -40,7 +45,7 @@ export const ImgSize: ExtensionAuto = (builder, opts) => { builder.addAction(addImageAction, ({schema}) => addImage(schema)); - builder.addPlugin(imgSizeNodeViewPlugin); + builder.addPlugin(imgSizeNodeViewPlugin({resolveImageSrc: opts.resolveImageSrc})); }; declare global { diff --git a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx index e510ba95f..f4ce93832 100644 --- a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx +++ b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx @@ -21,11 +21,16 @@ import './ImgNodeView.scss'; export const cnImgSizeNodeView = cn('img-size-node-view'); -export const ImageNodeView: React.FC = ({ +export type ImgSizeNodeViewOpts = { + resolveImageSrc?: (src: string) => string; +}; + +export const ImageNodeView: React.FC> = ({ node, view, getPos, updateAttributes, + extensionOptions, }) => { const imageContainerRef = useRef(null); const imageRef = useRef(null); @@ -34,7 +39,8 @@ export const ImageNodeView: React.FC = ({ const alt = node.attrs[ImgSizeAttr.Alt] || ''; const initialHeight = node.attrs[ImgSizeAttr.Height]; const initialWidth = node.attrs[ImgSizeAttr.Width]; - const src = node.attrs[ImgSizeAttr.Src] || ''; + const rawSrc = node.attrs[ImgSizeAttr.Src] || ''; + const src = extensionOptions?.resolveImageSrc?.(rawSrc) ?? rawSrc; const title = node.attrs[ImgSizeAttr.Title] || ''; const isNodeHovered = useNodeHovered(imageContainerRef); diff --git a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/index.ts b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/index.ts index 3beeb1984..8d74fbc59 100644 --- a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/index.ts +++ b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/index.ts @@ -4,23 +4,26 @@ import type {ExtensionDeps} from '../../../../../core'; import {reactNodeViewFactory} from '../../../../../react-utils'; import {imageNodeName, imageRendererKey} from '../../const'; -import {ImageNodeView, cnImgSizeNodeView} from './NodeView'; +import {ImageNodeView, type ImgSizeNodeViewOpts, cnImgSizeNodeView} from './NodeView'; -export const imgSizeNodeViewPlugin = (deps: ExtensionDeps) => - new Plugin({ - key: imageRendererKey, - state: { - init: () => undefined, - apply: (tr) => { - return tr.getMeta(imageRendererKey); +export const imgSizeNodeViewPlugin = + (opts: ImgSizeNodeViewOpts = {}) => + (deps: ExtensionDeps) => + new Plugin({ + key: imageRendererKey, + state: { + init: () => undefined, + apply: (tr) => { + return tr.getMeta(imageRendererKey); + }, }, - }, - props: { - nodeViews: { - [imageNodeName]: reactNodeViewFactory(ImageNodeView, { - isInline: true, - reactNodeWrapperCn: cnImgSizeNodeView('wrapper'), - })(deps), + props: { + nodeViews: { + [imageNodeName]: reactNodeViewFactory(ImageNodeView, { + isInline: true, + reactNodeWrapperCn: cnImgSizeNodeView('wrapper'), + extensionOptions: opts, + })(deps), + }, }, - }, - }); + }); From dd1aa91630024226dd2966cc5f1cc83ad8db24c0 Mon Sep 17 00:00:00 2001 From: separatrix Date: Tue, 21 Jul 2026 12:47:14 +0300 Subject: [PATCH 2/3] feat: added image error processing --- .../plugins/ImgSizeNodeView/NodeView.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx index f4ce93832..d10b0c5f7 100644 --- a/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx +++ b/packages/editor/src/extensions/yfm/ImgSize/plugins/ImgSizeNodeView/NodeView.tsx @@ -95,6 +95,22 @@ export const ImageNodeView: React.FC> = view.focus(); }, [getPos, node, view]); + const handleImgError = useCallback(() => { + const pos = getPos(); + + if (pos === undefined) { + return; + } + + const text = view.state.schema.text(`![${alt}](${rawSrc})`); + + view.dispatch( + view.state.tr + .replaceWith(pos, pos + node.nodeSize, text) + .setMeta('image-load-failed', rawSrc), + ); + }, [alt, rawSrc, getPos, node, view]); + const createHandleResize = (direction: ResizeDirection) => (event: React.MouseEvent) => { startResizing(event, direction); @@ -134,7 +150,7 @@ export const ImageNodeView: React.FC> = onDelete={handleDelete} unsetEdit={unsetEdit} /> - {alt} + {alt} ); From ea7b4f9207b69083e64f7fdd6bf7aa35ee30b43b Mon Sep 17 00:00:00 2001 From: separatrix Date: Wed, 22 Jul 2026 10:19:39 +0300 Subject: [PATCH 3/3] fix: added story --- .../img-resolve-src/ImgResolveSrc.stories.tsx | 13 +++++ .../img-resolve-src/ImgResolveSrc.tsx | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 demo/src/stories/examples/img-resolve-src/ImgResolveSrc.stories.tsx create mode 100644 demo/src/stories/examples/img-resolve-src/ImgResolveSrc.tsx diff --git a/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.stories.tsx b/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.stories.tsx new file mode 100644 index 000000000..97abb8ae1 --- /dev/null +++ b/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.stories.tsx @@ -0,0 +1,13 @@ +import type {StoryObj} from '@storybook/react'; + +import {ImgResolveSrcDemo as component} from './ImgResolveSrc'; + +export const Story: StoryObj = { + args: {}, +}; +Story.storyName = 'Resolve Image Src'; + +export default { + title: 'Examples / Resolve Image Src', + component, +}; diff --git a/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.tsx b/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.tsx new file mode 100644 index 000000000..9c479c76d --- /dev/null +++ b/demo/src/stories/examples/img-resolve-src/ImgResolveSrc.tsx @@ -0,0 +1,48 @@ +import {memo} from 'react'; + +import {MarkdownEditorView, useMarkdownEditor} from '@gravity-ui/markdown-editor'; + +import {PlaygroundLayout} from '../../../components/PlaygroundLayout'; + +const INITIAL_MARKUP = `![Avatar](./avatar.jpg =320x200)`; + +const resolveImageSrc = (src: string): string => { + const imageMap: Record = { + './avatar.jpg': + 'https://avatars.mds.yandex.net/get-shedevrum/14441318/img_1c3b6b42eee211efad66ea120268400c/orig', + }; + return imageMap[src] ?? src; +}; + +export const ImgResolveSrcDemo = memo(() => { + const editor = useMarkdownEditor({ + initial: { + mode: 'wysiwyg', + markup: INITIAL_MARKUP, + }, + wysiwygConfig: { + extensionOptions: { + imgSize: { + resolveImageSrc, + }, + }, + }, + }); + + return ( + ( + + )} + /> + ); +}); + +ImgResolveSrcDemo.displayName = 'ImgResolveSrcDemo';