Skip to content

Commit

Permalink
feat(editor): Support using custom SVG loader plugins when pasting fr…
Browse files Browse the repository at this point in the history
…om SVG
  • Loading branch information
personalizedrefrigerator committed Feb 1, 2025
1 parent 3024501 commit c01de4f
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 14 deletions.
17 changes: 14 additions & 3 deletions packages/js-draw/src/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Viewport from './Viewport';
import EventDispatcher from './EventDispatcher';
import { Point2, Vec2, Vec3, Color4, Mat33, Rect2 } from '@js-draw/math';
import Display, { RenderingMode } from './rendering/Display';
import SVGLoader from './SVGLoader/SVGLoader';
import SVGLoader, { SVGLoaderPlugin } from './SVGLoader/SVGLoader';
import Pointer from './Pointer';
import { EditorLocalization } from './localization';
import getLocalizationTable from './localizations/getLocalizationTable';
Expand Down Expand Up @@ -175,6 +175,11 @@ export interface EditorSettings {
/** Called to write data to the clipboard. Keys in `data` are MIME types. Values are the data associated with that type. */
write(data: Map<string, Blob | Promise<Blob> | string>): void | Promise<void>;
} | null;

svg: {
/** Plugins that create custom components while loading with {@link Editor.loadFromSVG}. */
loaderPlugins?: SVGLoaderPlugin[];
} | null;
}

/**
Expand Down Expand Up @@ -369,8 +374,11 @@ export class Editor {
image: {
showImagePicker: settings.image?.showImagePicker ?? undefined,
},
svg: {
loaderPlugins: settings.svg?.loaderPlugins ?? [],
},
clipboardApi: settings.clipboardApi ?? null,
};
} satisfies EditorSettings;

// Validate settings
if (this.settings.minZoom > this.settings.maxZoom) {
Expand Down Expand Up @@ -1815,7 +1823,10 @@ export class Editor {
* ```
*/
public async loadFromSVG(svgData: string, sanitize: boolean = false) {
const loader = SVGLoader.fromString(svgData, sanitize);
const loader = SVGLoader.fromString(svgData, {
sanitize,
plugins: this.getCurrentSettings().svg?.loaderPlugins,
});
await this.loadFrom(loader);
}

Expand Down
52 changes: 42 additions & 10 deletions packages/js-draw/src/tools/PasteHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { InputEvtType, PasteEvent } from '../inputEvents';
import TextComponent from '../components/TextComponent';
import createEditor from '../testing/createEditor';
import PasteHandler from './PasteHandler';
import { EditorSettings } from '../Editor';
import Stroke from '../components/Stroke';
import { Color4 } from '@js-draw/math';
import { SVGLoaderPlugin } from '../SVGLoader/SVGLoader';
import { EditorImage } from '../lib';

const createTestEditor = () => {
const editor = createEditor();
const createTestEditor = (settigs?: Partial<EditorSettings>) => {
const editor = createEditor(settigs);
const pasteTool = editor.toolController.getMatchingTools(PasteHandler)[0];
return {
editor,
Expand All @@ -17,6 +22,13 @@ const createTestEditor = () => {
};
};

const textFromImage = (image: EditorImage) => {
return image
.getAllElements()
.filter((elem) => elem instanceof TextComponent)
.map((elem) => elem.getText());
};

describe('PasteHandler', () => {
test('should interpret non-SVG text/plain data as a text component', async () => {
const { editor, testPaste } = createTestEditor();
Expand All @@ -27,10 +39,7 @@ describe('PasteHandler', () => {
mime: 'text/plain',
});

const allText = editor.image
.getAllElements()
.filter((elem) => elem instanceof TextComponent)
.map((elem) => elem.getText());
const allText = textFromImage(editor.image);
expect(allText).toEqual(['Test text/plain']);
});

Expand All @@ -43,10 +52,33 @@ describe('PasteHandler', () => {
mime: 'text/plain',
});

const allText = editor.image
.getAllElements()
.filter((elem) => elem instanceof TextComponent)
.map((elem) => elem.getText());
const allText = textFromImage(editor.image);
expect(allText).toEqual(['Test']);
});

test('should allow processing pasted data with SVG loader plugins', async () => {
const plugin: SVGLoaderPlugin = {
async visit(node, loader) {
loader.addComponent(Stroke.fromFilled('m0,0 l10,10 z', Color4.red));
return true;
},
};

const { editor, testPaste } = createTestEditor({
svg: {
loaderPlugins: [plugin],
},
});

await testPaste({
kind: InputEvtType.PasteEvent,
data: '<svg><text>Test</text></svg>',
mime: 'text/plain',
});

const allText = textFromImage(editor.image);
// Should have added all components as strokes instead of text.
expect(allText).toEqual([]);
expect(editor.image.getAllElements().filter((comp) => comp instanceof Stroke)).toHaveLength(1);
});
});
5 changes: 4 additions & 1 deletion packages/js-draw/src/tools/PasteHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ export default class PasteHandler extends BaseTool {
private async doSVGPaste(data: string) {
this.editor.showLoadingWarning(0);
try {
const loader = SVGLoader.fromString(data, true);
const loader = SVGLoader.fromString(data, {
sanitize: true,
plugins: this.editor.getCurrentSettings().svg?.loaderPlugins ?? [],
});

const components: AbstractComponent[] = [];
await loader.start(
Expand Down

0 comments on commit c01de4f

Please sign in to comment.