chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,92 @@
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists } from '@blocksuite/global/utils';
import type {
BlockSnapshot,
DocSnapshot,
FromBlockSnapshotPayload,
FromBlockSnapshotResult,
FromDocSnapshotPayload,
FromDocSnapshotResult,
FromSliceSnapshotPayload,
FromSliceSnapshotResult,
SliceSnapshot,
ToBlockSnapshotPayload,
ToDocSnapshotPayload,
ToSliceSnapshotPayload,
} from '@blocksuite/store';
import { BaseAdapter } from '@blocksuite/store';
import { decodeClipboardBlobs, encodeClipboardBlobs } from './utils.js';
export type FileSnapshot = {
name: string;
type: string;
content: string;
};
export class ClipboardAdapter extends BaseAdapter<string> {
static MIME = 'BLOCKSUITE/SNAPSHOT';
override fromBlockSnapshot(
_payload: FromBlockSnapshotPayload
): Promise<FromBlockSnapshotResult<string>> {
throw new BlockSuiteError(
ErrorCode.TransformerNotImplementedError,
'ClipboardAdapter.fromBlockSnapshot is not implemented'
);
}
override fromDocSnapshot(
_payload: FromDocSnapshotPayload
): Promise<FromDocSnapshotResult<string>> {
throw new BlockSuiteError(
ErrorCode.TransformerNotImplementedError,
'ClipboardAdapter.fromDocSnapshot is not implemented'
);
}
override async fromSliceSnapshot(
payload: FromSliceSnapshotPayload
): Promise<FromSliceSnapshotResult<string>> {
const snapshot = payload.snapshot;
const assets = payload.assets;
assertExists(assets);
const map = assets.getAssets();
const blobs: Record<string, FileSnapshot> = await encodeClipboardBlobs(map);
return {
file: JSON.stringify({
snapshot,
blobs,
}),
assetsIds: [],
};
}
override toBlockSnapshot(
_payload: ToBlockSnapshotPayload<string>
): Promise<BlockSnapshot> {
throw new BlockSuiteError(
ErrorCode.TransformerNotImplementedError,
'ClipboardAdapter.toBlockSnapshot is not implemented'
);
}
override toDocSnapshot(
_payload: ToDocSnapshotPayload<string>
): Promise<DocSnapshot> {
throw new BlockSuiteError(
ErrorCode.TransformerNotImplementedError,
'ClipboardAdapter.toDocSnapshot is not implemented'
);
}
override toSliceSnapshot(
payload: ToSliceSnapshotPayload<string>
): Promise<SliceSnapshot> {
const json = JSON.parse(payload.file);
const { blobs, snapshot } = json;
const map = payload.assets?.getAssets();
decodeClipboardBlobs(blobs, map);
return Promise.resolve(snapshot);
}
}
@@ -0,0 +1,218 @@
import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { BlockSnapshot, Doc } from '@blocksuite/store';
import {
AttachmentAdapter,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
} from '../../_common/adapters/index.js';
import {
defaultImageProxyMiddleware,
replaceIdMiddleware,
titleMiddleware,
} from '../../_common/transformers/middlewares.js';
import { ClipboardAdapter } from './adapter.js';
import { copyMiddleware, pasteMiddleware } from './middlewares/index.js';
export class PageClipboard {
private _copySelected = (onCopy?: () => void) => {
return this._std.command
.chain()
.with({ onCopy })
.getSelectedModels()
.draftSelectedModels()
.copySelectedModels();
};
protected _disposables = new DisposableGroup();
protected _init = () => {
this._std.clipboard.registerAdapter(
ClipboardAdapter.MIME,
ClipboardAdapter,
100
);
this._std.clipboard.registerAdapter(
'text/_notion-text-production',
NotionTextAdapter,
95
);
this._std.clipboard.registerAdapter('text/html', HtmlAdapter, 90);
[
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].forEach(type =>
this._std.clipboard.registerAdapter(type, ImageAdapter, 80)
);
this._std.clipboard.registerAdapter('text/plain', MixTextAdapter, 70);
this._std.clipboard.registerAdapter('*/*', AttachmentAdapter, 60);
const copy = copyMiddleware(this._std);
const paste = pasteMiddleware(this._std);
this._std.clipboard.use(copy);
this._std.clipboard.use(paste);
this._std.clipboard.use(replaceIdMiddleware);
this._std.clipboard.use(titleMiddleware);
this._std.clipboard.use(defaultImageProxyMiddleware);
this._disposables.add({
dispose: () => {
this._std.clipboard.unregisterAdapter(ClipboardAdapter.MIME);
this._std.clipboard.unregisterAdapter('text/plain');
[
'image/apng',
'image/avif',
'image/gif',
'image/jpeg',
'image/png',
'image/svg+xml',
'image/webp',
].forEach(type => this._std.clipboard.unregisterAdapter(type));
this._std.clipboard.unregisterAdapter('text/html');
this._std.clipboard.unregisterAdapter('*/*');
this._std.clipboard.unuse(copy);
this._std.clipboard.unuse(paste);
this._std.clipboard.unuse(replaceIdMiddleware);
this._std.clipboard.unuse(titleMiddleware);
this._std.clipboard.unuse(defaultImageProxyMiddleware);
},
});
};
host: BlockComponent;
onBlockSnapshotPaste = async (
snapshot: BlockSnapshot,
doc: Doc,
parent?: string,
index?: number
) => {
const block = await this._std.clipboard.pasteBlockSnapshot(
snapshot,
doc,
parent,
index
);
return block?.id ?? null;
};
onPageCopy: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._copySelected().run();
};
onPageCut: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._copySelected(() => {
this._std.command
.chain()
.try(cmd => [
cmd.getTextSelection().deleteText(),
cmd.getSelectedModels().deleteSelectedModels(),
])
.run();
}).run();
};
onPagePaste: UIEventHandler = ctx => {
const e = ctx.get('clipboardState').raw;
e.preventDefault();
this._std.doc.captureSync();
this._std.command
.chain()
.try(cmd => [
cmd.getTextSelection(),
cmd
.getSelectedModels()
.clearAndSelectFirstModel()
.retainFirstModel()
.deleteSelectedModels(),
])
.try(cmd => [
cmd.getTextSelection().inline<'currentSelectionPath'>((ctx, next) => {
const textSelection = ctx.currentTextSelection;
if (!textSelection) {
return;
}
next({ currentSelectionPath: textSelection.from.blockId });
}),
cmd.getBlockSelections().inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
if (!currentBlockSelections) {
return;
}
const blockSelection = currentBlockSelections.at(-1);
if (!blockSelection) {
return;
}
next({ currentSelectionPath: blockSelection.blockId });
}),
cmd.getImageSelections().inline<'currentSelectionPath'>((ctx, next) => {
const currentImageSelections = ctx.currentImageSelections;
if (!currentImageSelections) {
return;
}
const imageSelection = currentImageSelections.at(-1);
if (!imageSelection) {
return;
}
next({ currentSelectionPath: imageSelection.blockId });
}),
])
.getBlockIndex()
.inline((ctx, next) => {
if (!ctx.parentBlock) {
return;
}
this._std.clipboard
.paste(
e,
this._std.doc,
ctx.parentBlock.model.id,
ctx.blockIndex ? ctx.blockIndex + 1 : 1
)
.catch(console.error);
return next();
})
.run();
};
private get _std() {
return this.host.std;
}
constructor(host: BlockComponent) {
this.host = host;
}
hostConnected() {
if (this._disposables.disposed) {
this._disposables = new DisposableGroup();
}
if (navigator.clipboard) {
this.host.handleEvent('copy', this.onPageCopy);
this.host.handleEvent('paste', this.onPagePaste);
this.host.handleEvent('cut', this.onPageCut);
this._init();
}
}
hostDisconnected() {
this._disposables.dispose();
}
}
export { copyMiddleware, pasteMiddleware };
@@ -0,0 +1,54 @@
import type { EditorHost, TextRangePoint } from '@blocksuite/block-std';
import type {
BlockSnapshot,
DraftModel,
JobMiddleware,
JobSlots,
} from '@blocksuite/store';
import { matchFlavours } from '../../../_common/utils/index.js';
const handlePoint = (
point: TextRangePoint,
snapshot: BlockSnapshot,
model: DraftModel
) => {
const { index, length } = point;
if (matchFlavours(model, ['affine:page'])) {
if (length === 0) return;
(snapshot.props.title as Record<string, unknown>).delta =
model.title.sliceToDelta(index, length + index);
return;
}
if (!snapshot.props.text || length === 0) {
return;
}
(snapshot.props.text as Record<string, unknown>).delta =
model.text?.sliceToDelta(index, length + index);
};
const sliceText = (slots: JobSlots, std: EditorHost['std']) => {
slots.afterExport.on(payload => {
if (payload.type === 'block') {
const snapshot = payload.snapshot;
const model = payload.model;
const text = std.selection.find('text');
if (text && text.from.blockId === model.id) {
handlePoint(text.from, snapshot, model);
return;
}
if (text && text.to && text.to.blockId === model.id) {
handlePoint(text.to, snapshot, model);
return;
}
}
});
};
export const copyMiddleware = (std: EditorHost['std']): JobMiddleware => {
return ({ slots }) => {
sliceText(slots, std);
};
};
@@ -0,0 +1,2 @@
export * from './copy.js';
export * from './paste.js';
@@ -0,0 +1,529 @@
import { REFERENCE_NODE } from '@blocksuite/affine-components/rich-text';
import type { ParagraphBlockModel } from '@blocksuite/affine-model';
import {
ParseDocUrlProvider,
type ParseDocUrlService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { referenceToNode } from '@blocksuite/affine-shared/utils';
import {
BLOCK_ID_ATTR,
type BlockComponent,
type EditorHost,
type TextRangePoint,
type TextSelection,
} from '@blocksuite/block-std';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists } from '@blocksuite/global/utils';
import {
type BlockModel,
type BlockSnapshot,
type DeltaOperation,
DocCollection,
fromJSON,
type JobMiddleware,
type SliceSnapshot,
type Text,
} from '@blocksuite/store';
import { matchFlavours } from '../../../_common/utils/index.js';
import { extractSearchParams } from '../../../_common/utils/url.js';
function findLastMatchingNode(
root: BlockSnapshot[],
fn: (node: BlockSnapshot) => boolean
): BlockSnapshot | null {
let lastMatchingNode: BlockSnapshot | null = null;
function traverse(node: BlockSnapshot) {
if (fn(node)) {
lastMatchingNode = node;
}
if (node.children) {
for (const child of node.children) {
traverse(child);
}
}
}
root.forEach(traverse);
return lastMatchingNode;
}
// find last child that has text as prop
const findLast = (snapshot: SliceSnapshot): BlockSnapshot | null => {
return findLastMatchingNode(snapshot.content, node => !!node.props.text);
};
class PointState {
private _blockFromPath = (path: string) => {
const block = this.std.view.getBlock(path);
assertExists(block);
return block;
};
readonly block: BlockComponent;
readonly model: BlockModel;
readonly text: Text;
constructor(
readonly std: EditorHost['std'],
readonly point: TextRangePoint
) {
this.block = this._blockFromPath(point.blockId);
this.model = this.block.model;
const text = this.model.text;
if (!text) {
console.error(this.point);
throw new BlockSuiteError(
ErrorCode.TransformerError,
'Text point without text model'
);
}
this.text = text;
}
}
class PasteTr {
private _getDeltas = () => {
const firstTextSnapshot = this._textFromSnapshot(this.firstSnapshot!);
const lastTextSnapshot = this._textFromSnapshot(this.lastSnapshot!);
const fromDelta = this.pointState.text.sliceToDelta(
0,
this.pointState.point.index
);
const toDelta = this.pointState.text.sliceToDelta(
this.pointState.point.index + this.pointState.point.length,
this.pointState.text.length
);
const firstDelta = firstTextSnapshot.delta;
const lastDelta = lastTextSnapshot.delta;
return {
firstTextSnapshot,
lastTextSnapshot,
fromDelta,
toDelta,
firstDelta,
lastDelta,
};
};
private _mergeCode = () => {
const deltas: DeltaOperation[] = [{ retain: this.pointState.point.index }];
this.snapshot.content.forEach((blockSnapshot, i) => {
if (blockSnapshot.props.text) {
const text = this._textFromSnapshot(blockSnapshot);
if (i > 0) {
deltas.push({ insert: '\n' });
}
deltas.push(...text.delta);
}
});
this.pointState.text.applyDelta(deltas);
this.snapshot.content = [];
};
private _mergeMultiple = () => {
this._updateFlavour();
const { lastTextSnapshot, toDelta, firstDelta, lastDelta } =
this._getDeltas();
this.pointState.text.applyDelta([
{ retain: this.pointState.point.index },
this.pointState.text.length - this.pointState.point.index > 0
? { delete: this.pointState.text.length - this.pointState.point.index }
: {},
...firstDelta,
]);
const removedFirstSnapshot = this.snapshot.content.shift();
removedFirstSnapshot?.children.forEach(block => {
this.snapshot.content.unshift(block);
});
this.pasteStartModelChildrenCount =
removedFirstSnapshot?.children.length ?? 0;
this._updateSnapshot();
lastTextSnapshot.delta = [...lastDelta, ...toDelta];
};
private _mergeSingle = () => {
this._updateFlavour();
const { firstDelta } = this._getDeltas();
const { index, length } = this.pointState.point;
// Pastes a link
if (length && firstDelta.length === 1 && firstDelta[0].attributes?.link) {
this.pointState.text.format(index, length, firstDelta[0].attributes);
} else {
const ops: DeltaOperation[] = [{ retain: index }];
if (length) ops.push({ delete: length });
ops.push(...firstDelta);
this.pointState.text.applyDelta(ops);
}
this.snapshot.content.splice(0, 1);
this._updateSnapshot();
};
private _textFromSnapshot = (snapshot: BlockSnapshot) => {
return (snapshot.props.text ?? { delta: [] }) as Record<
'delta',
DeltaOperation[]
>;
};
private _updateSnapshot = () => {
if (this.snapshot.content.length === 0) {
this.firstSnapshot = this.lastSnapshot = undefined;
return;
}
this.firstSnapshot = this.snapshot.content[0];
this.lastSnapshot = findLast(this.snapshot) ?? this.firstSnapshot;
};
private firstSnapshot?: BlockSnapshot;
private readonly firstSnapshotIsPlainText: boolean;
private lastIndex: number;
private lastSnapshot?: BlockSnapshot;
private needCleanup = false;
private pasteStartModelChildrenCount = 0;
private readonly pointState: PointState;
canMerge = () => {
if (this.snapshot.content.length === 0) {
return false;
}
if (!this.firstSnapshot!.props.text) {
return false;
}
const firstTextSnapshot = this._textFromSnapshot(this.firstSnapshot!);
const lastTextSnapshot = this._textFromSnapshot(this.lastSnapshot!);
return (
firstTextSnapshot &&
lastTextSnapshot &&
(this.pointState.text.length > 0 || this.firstSnapshotIsPlainText)
);
};
convertToLinkedDoc = () => {
const parseDocUrlService = this.std.getOptional(ParseDocUrlProvider);
if (!parseDocUrlService) {
return;
}
const linkToDocId = new Map<string, string | null>();
for (const blockSnapshot of this.snapshot.content) {
if (blockSnapshot.props.text) {
const [delta, transformed] = this._transformLinkDelta(
this._textFromSnapshot(blockSnapshot).delta,
linkToDocId,
parseDocUrlService
);
const model = this.std.doc.getBlock(blockSnapshot.id)?.model;
if (transformed && model) {
this.std.doc.captureSync();
this.std.doc.transact(() => {
const text = model.text as Text;
text.clear();
text.applyDelta(delta);
});
}
}
}
const fromPointStateText = this.pointState.model.text;
if (!fromPointStateText) {
return;
}
const [delta, transformed] = this._transformLinkDelta(
fromPointStateText.toDelta(),
linkToDocId,
parseDocUrlService
);
if (!transformed) {
return;
}
this.std.doc.captureSync();
this.std.doc.transact(() => {
fromPointStateText.clear();
fromPointStateText.applyDelta(delta);
});
};
focusPasted = () => {
const host = this.std.host;
const cursorBlock =
this.pointState.model.flavour === 'affine:code' || !this.lastSnapshot
? this.std.doc.getBlock(this.pointState.model.id)
: this.std.doc.getBlock(this.lastSnapshot.id);
if (!cursorBlock) {
return;
}
const { model: cursorModel } = cursorBlock;
host.updateComplete
.then(() => {
const target = this.std.host.querySelector<BlockComponent>(
`[${BLOCK_ID_ATTR}="${cursorModel.id}"]`
);
if (!target) {
return;
}
if (!cursorModel.text) {
if (matchFlavours(cursorModel, ['affine:image'])) {
const selection = this.std.selection.create('image', {
blockId: target.blockId,
});
this.std.selection.setGroup('note', [selection]);
return;
}
const selection = this.std.selection.create('block', {
blockId: target.blockId,
});
this.std.selection.setGroup('note', [selection]);
return;
}
const selection = this.std.selection.create('text', {
from: {
blockId: target.blockId,
index: cursorModel.text ? this.lastIndex : 0,
length: 0,
},
to: null,
});
this.std.selection.setGroup('note', [selection]);
})
.catch(console.error);
};
pasted = () => {
if (!(this.needCleanup || this.pointState.text.length === 0)) {
return;
}
if (this.lastSnapshot) {
const lastModel = this.std.doc.getBlock(this.lastSnapshot.id)?.model;
if (!lastModel) {
return;
}
this.std.doc.moveBlocks(this.pointState.model.children, lastModel);
}
this.std.doc.moveBlocks(
this.std.doc
.getNexts(this.pointState.model.id)
.slice(0, this.pasteStartModelChildrenCount),
this.pointState.model
);
if (!this.firstSnapshotIsPlainText && this.pointState.text.length == 0) {
this.std.doc.deleteBlock(this.pointState.model);
}
};
constructor(
readonly std: EditorHost['std'],
readonly text: TextSelection,
readonly snapshot: SliceSnapshot
) {
const { from } = text;
this.pointState = new PointState(std, from);
this.firstSnapshot = snapshot.content[0];
this.lastSnapshot = findLast(snapshot) ?? this.firstSnapshot;
if (
this.firstSnapshot !== this.lastSnapshot &&
this.lastSnapshot.props.text &&
!matchFlavours(this.pointState.model, ['affine:code'])
) {
const text = fromJSON(this.lastSnapshot.props.text) as Text;
const doc = new DocCollection.Y.Doc();
const temp = doc.getMap('temp');
temp.set('text', text.yText);
this.lastIndex = text.length;
} else {
this.lastIndex =
this.pointState.point.index +
this.snapshot.content
.map(snapshot =>
this._textFromSnapshot(snapshot)
.delta.map(op => {
if (op.insert) {
return op.insert.length;
} else if (op.delete) {
return -op.delete;
} else {
return 0;
}
})
.reduce((a, b) => a + b, 0)
)
.reduce((a, b) => a + b + 1, -1);
}
this.firstSnapshotIsPlainText =
this.firstSnapshot.flavour === 'affine:paragraph' &&
this.firstSnapshot.props.type === 'text';
}
private _transformLinkDelta(
delta: DeltaOperation[],
linkToDocId: Map<string, string | null>,
parseDocUrlService: ParseDocUrlService
): [DeltaOperation[], boolean] {
let transformed = false;
const needToConvert = new Map<DeltaOperation, string>();
for (const op of delta) {
if (op.attributes?.link) {
let docId = linkToDocId.get(op.attributes.link);
if (!docId) {
const searchResult = parseDocUrlService.parseDocUrl(
op.attributes.link
);
if (searchResult) {
const doc = this.std.collection.getDoc(searchResult.docId);
if (doc) {
docId = doc.id;
linkToDocId.set(op.attributes.link, doc.id);
}
}
}
if (docId) {
needToConvert.set(op, docId);
}
}
}
const newDelta = delta.map(op => {
if (!needToConvert.has(op)) {
return { ...op };
}
const link = op.attributes?.link;
if (!link) {
return { ...op };
}
const pageId = needToConvert.get(op);
if (!pageId) {
// External link
this.std.getOptional(TelemetryProvider)?.track('Link', {
page: 'doc editor',
category: 'pasted link',
other: 'external link',
type: 'link',
});
return { ...op };
}
const reference: AffineTextAttributes['reference'] = {
pageId,
type: 'LinkedPage',
};
// Title alias
if (op.insert && op.insert !== REFERENCE_NODE && op.insert !== link) {
reference.title = op.insert;
}
const extractedParams = extractSearchParams(link);
const isLinkedBlock = extractedParams
? referenceToNode({ pageId, ...extractedParams })
: false;
Object.assign(reference, extractedParams);
// Internal link
this.std.getOptional(TelemetryProvider)?.track('LinkedDocCreated', {
page: 'doc editor',
category: 'pasted link',
other: 'existing doc',
type: isLinkedBlock ? 'block' : 'doc',
});
transformed = true;
return {
...op,
attributes: { reference },
insert: REFERENCE_NODE,
};
});
return [newDelta, transformed];
}
private _updateFlavour() {
this.firstSnapshot!.flavour = this.pointState.model.flavour;
if (this.firstSnapshot!.props.type) {
this.firstSnapshot!.props.type = (
this.pointState.model as ParagraphBlockModel
).type;
}
}
merge() {
if (this.pointState.model.flavour === 'affine:code') {
this._mergeCode();
return;
}
if (this.firstSnapshot === this.lastSnapshot) {
this._mergeSingle();
return;
}
this.needCleanup = true;
this._mergeMultiple();
}
}
function flatNote(snapshot: SliceSnapshot) {
if (snapshot.content[0]?.flavour === 'affine:note') {
snapshot.content = snapshot.content[0].children;
}
}
export const pasteMiddleware = (std: EditorHost['std']): JobMiddleware => {
return ({ slots }) => {
let tr: PasteTr | undefined;
slots.beforeImport.on(payload => {
if (payload.type === 'slice') {
const { snapshot } = payload;
flatNote(snapshot);
const text = std.selection.find('text');
if (!text) {
return;
}
tr = new PasteTr(std, text, payload.snapshot);
if (tr.canMerge()) {
tr.merge();
}
}
});
slots.afterImport.on(payload => {
if (tr && payload.type === 'slice') {
tr.pasted();
tr.focusPasted();
tr.convertToLinkedDoc();
}
});
};
};
@@ -0,0 +1,124 @@
import { toast } from '@blocksuite/affine-components/toast';
import { assertExists } from '@blocksuite/global/utils';
import type { FileSnapshot } from './adapter.js';
const chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
// Use a lookup table to find the index.
const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i;
}
export const encode = (arraybuffer: ArrayBuffer): string => {
const bytes = new Uint8Array(arraybuffer);
const len = bytes.length;
let i,
base64 = '';
for (i = 0; i < len; i += 3) {
base64 += chars[bytes[i] >> 2];
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
base64 += chars[bytes[i + 2] & 63];
}
if (len % 3 === 2) {
base64 = base64.substring(0, base64.length - 1) + '=';
} else if (len % 3 === 1) {
base64 = base64.substring(0, base64.length - 2) + '==';
}
return base64;
};
export const decode = (base64: string): ArrayBuffer => {
const len = base64.length;
let bufferLength = base64.length * 0.75,
i,
p = 0,
encoded1,
encoded2,
encoded3,
encoded4;
if (base64[base64.length - 1] === '=') {
bufferLength--;
if (base64[base64.length - 2] === '=') {
bufferLength--;
}
}
const arraybuffer = new ArrayBuffer(bufferLength),
bytes = new Uint8Array(arraybuffer);
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)];
encoded2 = lookup[base64.charCodeAt(i + 1)];
encoded3 = lookup[base64.charCodeAt(i + 2)];
encoded4 = lookup[base64.charCodeAt(i + 3)];
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
}
return arraybuffer;
};
export async function encodeClipboardBlobs(map: Map<string, Blob>) {
const blobs: Record<string, FileSnapshot> = {};
let sumSize = 0;
await Promise.all(
Array.from(map.entries()).map(async ([id, blob]) => {
if (blob.size > 4 * 1024 * 1024) {
const host = document.querySelector('editor-host');
if (!host) {
return;
}
toast(
host,
(blob as File).name ?? 'File' + ' is too large to be copied'
);
return;
}
sumSize += blob.size;
if (sumSize > 6 * 1024 * 1024) {
const host = document.querySelector('editor-host');
if (!host) {
return;
}
toast(
host,
(blob as File).name ??
'File' + ' cannot be copied due to the clipboard size limit'
);
return;
}
const content = encode(await blob.arrayBuffer());
const file: FileSnapshot = {
name: (blob as File).name,
type: blob.type,
content,
};
blobs[id] = file;
})
);
return blobs;
}
export function decodeClipboardBlobs(
blobs: Record<string, FileSnapshot>,
map: Map<string, Blob> | undefined
) {
Object.entries<FileSnapshot>(blobs).forEach(([sourceId, file]) => {
const blob = new Blob([decode(file.content)]);
const f = new File([blob], file.name, {
type: file.type,
});
assertExists(map);
map.set(sourceId, f);
});
}