Compare commits

...

3 Commits

14 changed files with 469 additions and 287 deletions

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.1.6.1-beta-1",
"version": "2.1.8.2-beta-1",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -10,9 +10,10 @@ const o0 = Decoration.line({ attributes: {class: "ex-opacity-0"} });
export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
class {
view: EditorView;
decorations: DecorationSet;
decorations: DecorationSet;
reExcalidrawData = /^%%(?:\r\n|\r|\n)# Excalidraw Data$/gm;
reTextElements = /^%%(?:\r\n|\r|\n)# Text Elements$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)# Drawing$/gm;
reDrawing = /^%%(?:\r\n|\r|\n)##? Drawing$/gm;
linecount = 0;
isExcalidraw = false;
@@ -32,11 +33,15 @@ export const HideTextBetweenCommentsExtension = ViewPlugin.fromClass(
const text = doc.toString();
let start = text.search(this.reTextElements);
let start = text.search(this.reExcalidrawData);
if(start == -1) {
start = text.search(this.reTextElements);
}
if(start == -1) {
start = text.search(this.reDrawing);
if(start == -1) return Decoration.none;
}
if(start == -1) return Decoration.none;
const startLine = doc.lineAt(start).number;
const endLine = doc.lines;
let builder = new RangeSetBuilder<Decoration>()

View File

@@ -231,7 +231,7 @@ export class EmbeddedFile {
return false;
}
}
return this.mtime != this.file.stat.mtime;
return this.mtime !== this.file.stat.mtime;
}
public setImage(
@@ -817,7 +817,7 @@ export class EmbeddedFilesLoader {
? fileCache.frontmatter[FRONTMATTER_KEYS["md-css"].name] ?? ""
: "";
let frontmatterCSSisAfile = false;
if (style && style != "") {
if (style && style !== "") {
const f = plugin.app.metadataCache.getFirstLinkpathDest(style, file.path);
if (f) {
style = await plugin.app.vault.read(f);

View File

@@ -35,12 +35,9 @@ import {
REG_LINKINDEX_INVALIDCHARS,
THEME_FILTER,
mermaidToExcalidraw,
MD_TEXTELEMENTS,
MD_DRAWING,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
arrayToMap,
//debug,
embedFontsInSVG,
errorlog,
@@ -53,12 +50,13 @@ import {
isVersionNewerThanOther,
scaleLoadedImage,
wrapTextAtCharLength,
arrayToMap,
} from "src/utils/Utils";
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/ObsidianUtils";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
import { tex2dataURL } from "src/LaTeX";
import { GenericInputPrompt, NewFileActions, Prompt } from "src/dialogs/Prompt";
import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt";
import { t } from "src/lang/helpers";
import { ScriptEngine } from "src/Scripts";
import { ConnectionPoint, DeviceType } from "src/types";
@@ -80,7 +78,7 @@ import { TInput } from "colormaster/types";
import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser"
import { ROUNDNESS } from "src/constants/constants";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
import { emulateKeysForLinkClick, KeyEvent, PaneTarget } from "src/utils/ModifierkeyHelper";
import { emulateKeysForLinkClick, PaneTarget } from "src/utils/ModifierkeyHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import PolyBool from "polybooljs";
import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings";
@@ -720,7 +718,7 @@ export class ExcalidrawAutomate {
const generateMD = ():string => {
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
let outString = `${MD_TEXTELEMENTS}\n`;
let outString = `# Excalidraw Data\n## Text Elements\n`;
textElements.forEach(te=> {
outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
});
@@ -731,7 +729,7 @@ export class ExcalidrawAutomate {
})
outString += Object.keys(this.imagesDict).length > 0
? "\n# Embedded files\n"
? `\n## Embedded Files\n`
: "";
Object.keys(this.imagesDict).forEach((key: FileId)=> {
@@ -2776,9 +2774,9 @@ async function getTemplate(
textMode,
);
let trimLocation = data.search(new RegExp(`^${MD_TEXTELEMENTS}$`,"m"));
let trimLocation = data.search(/^##? Text Elements$/m);
if (trimLocation == -1) {
trimLocation = data.search(`${MD_DRAWING}\n`);
trimLocation = data.search(/##? Drawing\n/);
}
let scene = excalidrawData.scene;

View File

@@ -18,9 +18,8 @@ import {
ERROR_IFRAME_CONVERSION_CANCELED,
JSON_parse,
FRONTMATTER_KEYS,
MD_TEXTELEMENTS,
MD_DRAWING,
MD_ELEMENTLINKS,
refreshTextDimensions,
getContainerElement,
} from "./constants/constants";
import { _measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
@@ -31,7 +30,7 @@ import {
decompress,
//getBakPath,
getBinaryFileFromDataURL,
getContainerElement,
_getContainerElement,
getExportTheme,
getLinkParts,
hasExportTheme,
@@ -39,11 +38,13 @@ import {
LinkParts,
updateFrontmatterInString,
wrapTextAtCharLength,
arrayToMap,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils";
import {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawTextElement,
FileId,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types";
@@ -51,6 +52,7 @@ import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { debug } from "./utils/DebugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -117,12 +119,12 @@ export const REGEX_LINK = {
};
//added \n at and of DRAWING_REG: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/357
const DRAWING_REG = /\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
const DRAWING_REG = /\n##? Drawing\n[^`]*(```json\n)([\s\S]*?)```\n/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_REG_FALLBACK = /\n##? Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
export const DRAWING_COMPRESSED_REG =
/(\n# Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
/(\n##? Drawing\n[^`]*(?:```compressed\-json\n))([\s\S]*?)(```\n)/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
const DRAWING_COMPRESSED_REG_FALLBACK =
/(\n# Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
/(\n##? Drawing\n(?:```compressed\-json\n)?)(.*)((```)?(%%)?)/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const isCompressedMD = (data: string): boolean => {
@@ -204,10 +206,10 @@ export function getMarkdownDrawingSection(
compressed: boolean,
) {
return compressed
? `# Drawing\n\x60\x60\x60compressed-json\n${compress(
? `## Drawing\n\x60\x60\x60compressed-json\n${compress(
jsonString,
)}\n\x60\x60\x60\n%%`
: `# Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
: `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
}
/**
@@ -241,33 +243,97 @@ const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_OK = new RegExp(`^#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_WITHSECTION_NOTOK = new RegExp(`#\n%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_NOSECTION_OK = new RegExp(`^(%%\n)?# Excalidraw Data(?:\n|$)`, "m");
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?##? Text Elements(?:\n|$)`, "m");
//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the lest section before # Text Elements
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//to collide. This is particularly problematic when the user is editing the last section before # Text Elements
const RE_EXCALIDRAWDATA_FALLBACK_1 = new RegExp(`(.*)%%\n# Excalidraw Data(?:\n|$)`, "m");
const RE_EXCALIDRAWDATA_FALLBACK_2 = new RegExp(`(.*)# Excalidraw Data(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n##? Text Elements(?:\n|$)`, "m");
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)##? Text Elements(?:\n|$)`, "m");
const RE_DRAWING = new RegExp(`(%%\n)?${MD_DRAWING}\n`);
const RE_DRAWING = new RegExp(`(%%\n)?##? Drawing\n`);
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
//The base case scenario is at the top, continued with fallbacks in order of likelihood and file structure
//change history for sake of backward compatibility
/* Expected markdown structure:
bla bla bla
#
%%
# Excalidraw Data
*/
let trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
}
/* Expected markdown structure (this happens when the user deletes the last empty line of the last back-of-the-card note):
bla bla bla#
%%
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_WITHSECTION_NOTOK);
if(trimLocation > 0) {
shouldFixTrailingHashtag = true;
}
}
/* Expected markdown structure
a)
bla bla bla
%%
# Excalidraw Data
b)
bla bla bla
# Excalidraw Data
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
}
/* Expected markdown structure:
bla bla bla%%
# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_1);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla# Excalidraw Data
*/
if(trimLocation === -1) {
const res = data.match(RE_EXCALIDRAWDATA_FALLBACK_2);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla
#
%%
# Text Elements
*/
let trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2;
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
if(trimLocation > 0) {
trimLocation += 2; //accounts for the "#\n" which I want to leave there untouched
}
}
/* Expected markdown structure:
bla bla bla#
%%
@@ -342,7 +408,7 @@ export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,st
export class ExcalidrawData {
public textElements: Map<
string,
{ raw: string; parsed: string; wrapAt: number | null }
{ raw: string; parsed: string}
> = null;
public elementLinks: Map<string, string> = null;
public scene: any = null;
@@ -543,10 +609,10 @@ export class ExcalidrawData {
this.selectedElementIds = {};
this.textElements = new Map<
string,
{ raw: string; parsed: string; wrapAt: number }
{ raw: string; parsed: string}
>();
this.elementLinks = new Map<string, string>();
if (this.file != file) {
if (this.file !== file) {
//this is a reload - files, equations and mermaids will take care of reloading when needed
this.files.clear();
this.equations.clear();
@@ -641,7 +707,28 @@ export class ExcalidrawData {
//link was updated due to filename changes
//The .excalidraw JSON is modified to reflect the MD in case of difference
//Read the text elements into the textElements Map
let position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
let position = data.search(RE_EXCALIDRAWDATA_NOSECTION_OK);
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
// ....# Excalidraw Data
// ....
// --------------
// instead of
// --------------
// # back of note section
// ....
// # Excalidraw Data
position = data.search(RE_EXCALIDRAWDATA_FALLBACK_2);
}
if(position === -1) {
// # back of note section
// ....
// # Text Elements
position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
}
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
@@ -661,10 +748,12 @@ export class ExcalidrawData {
return true; //Text Elements header does not exist
}
data = data.slice(position);
const normalMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"));
const normalMatch = data.match(/^((%%\n)?# Excalidraw Data\n## Text Elements(?:\n|$))/m)
??data.match(/^((%%\n)?##? Text Elements(?:\n|$))/m);
const textElementsMatch = normalMatch
? normalMatch[0]
: data.match(new RegExp(`(.*${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0];
: data.match(/(.*##? Text Elements(?:\n|$))/m)[0];
data = data.slice(textElementsMatch.length);
this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
@@ -673,9 +762,13 @@ export class ExcalidrawData {
//load element links
const elementLinkMap = new Map<string,string>();
const elementLinksData = data.substring(
data.indexOf(`${MD_ELEMENTLINKS}\n`) + `${MD_ELEMENTLINKS}\n`.length,
);
const indexOfNewElementLinks = data.indexOf("## Element Links\n");
const lengthOfNewElementLinks = 17; //`## Element Links\n`.length
const indexOfOldElementLinks = data.indexOf("# Element Links\n");
const lengthOfOldElementLinks = 16; //`# Element Links\n`.length
const elementLinksData = indexOfNewElementLinks>-1
? data.substring(indexOfNewElementLinks + lengthOfNewElementLinks)
: data.substring(indexOfOldElementLinks + lengthOfOldElementLinks);
//Load Embedded files
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
@@ -685,7 +778,7 @@ export class ExcalidrawData {
//iterating through all the text elements in .md
//Text elements always contain the raw value
const BLOCKREF_LEN: number = " ^12345678\n\n".length;
const BLOCKREF_LEN: number = 12; // " ^12345678\n\n".length;
const RE_TEXT_ELEMENT_LINK = /^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm;
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
while (!(parts = res.next()).done) {
@@ -703,9 +796,8 @@ export class ExcalidrawData {
}
this.elementLinks.set(id, text);
} else {
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
const elementLink = elementLinkRes.next();
if(!elementLink.done) {
text = text.replace(RE_TEXT_ELEMENT_LINK,"");
@@ -720,7 +812,6 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text,
parsed: parseRes.parsed,
wrapAt,
});
if (parseRes.link) {
textEl.link = parseRes.link;
@@ -746,11 +837,15 @@ export class ExcalidrawData {
}
}
const indexOfEmbeddedFiles = data.indexOf("# Embedded files\n");
if(indexOfEmbeddedFiles>-1) {
data = data.substring(
indexOfEmbeddedFiles + "# Embedded files\n".length,
);
const indexOfNewEmbeddedFiles = data.indexOf("## Embedded Files\n");
const embeddedFilesNewLength = 18; //"## Embedded Files\n".length
const indexOfOldEmbeddedFiles = data.indexOf("# Embedded files\n");
const embeddedFilesOldLength = 17; //"# Embedded files\n".length
if(indexOfNewEmbeddedFiles>-1 || indexOfOldEmbeddedFiles>-1) {
data = indexOfNewEmbeddedFiles>-1
? data.substring(indexOfNewEmbeddedFiles + embeddedFilesNewLength)
: data.substring(indexOfOldEmbeddedFiles + embeddedFilesOldLength);
//Load Embedded files
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
res = data.matchAll(REG_FILEID_FILEPATH);
@@ -815,7 +910,7 @@ export class ExcalidrawData {
this.file = file;
this.textElements = new Map<
string,
{ raw: string; parsed: string; wrapAt: number }
{ raw: string; parsed: string}
>();
this.elementLinks = new Map<string, string>();
this.setShowLinkBrackets();
@@ -846,34 +941,6 @@ export class ExcalidrawData {
await this.updateSceneTextElements(forceupdate);
}
//update a single text element in the scene if the newText is different
public updateTextElement(
sceneTextElement: any,
newText: string,
newOriginalText: string,
forceUpdate: boolean = false,
containerType?: string,
) {
if (forceUpdate || newText != sceneTextElement.text) {
const measure = _measureText(
newText,
sceneTextElement.fontSize,
sceneTextElement.fontFamily,
sceneTextElement.lineHeight??getDefaultLineHeight(sceneTextElement.fontFamily),
);
sceneTextElement.text = newText;
sceneTextElement.originalText = newOriginalText;
if (!sceneTextElement.containerId || containerType==="arrow") {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/376
//I leave the setting of text width to excalidraw, when text is in a container
//because text width is fixed to the container width
sceneTextElement.width = measure.w;
}
sceneTextElement.height = measure.h;
sceneTextElement.baseline = measure.baseline;
}
}
/**
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
@@ -884,24 +951,25 @@ export class ExcalidrawData {
private async updateSceneTextElements(forceupdate: boolean = false) {
//update text in scene based on textElements Map
//first get scene text elements
const texts = this.scene.elements?.filter((el: any) => el.type === "text");
const elementsMap = arrayToMap(this.scene.elements);
const texts = this.scene.elements?.filter((el: any) => el.type === "text") as Mutable<ExcalidrawTextElement>[];
for (const te of texts) {
const container = getContainerElement(te,this.scene);
const container = getContainerElement(te, elementsMap);
const originalText =
(await this.getText(te.id)) ?? te.originalText ?? te.text;
const wrapAt = this.textElements.get(te.id)?.wrapAt;
const {text, x, y, width, height} = refreshTextDimensions(
te,
container,
elementsMap,
originalText,
)
try { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1062
this.updateTextElement(
te,
wrapAt ? wrapText(
originalText,
getFontString({fontSize: te.fontSize, fontFamily: te.fontFamily}),
getBoundTextMaxWidth(container as any)
) : originalText,
originalText,
forceupdate,
container?.type,
); //(await this.getText(te.id))??te.text serves the case when the whole #Text Elements section is deleted by accident
te.originalText = originalText;
te.text = text;
te.x = x;
te.y = y;
te.width = width;
te.height = height;
} catch(e) {
debug(`ExcalidrawData.updateSceneTextElements, textElement: ${te?.id}`, te, this.updateSceneTextElements);
}
@@ -920,7 +988,6 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text.raw,
parsed: (await this.parse(text.raw)).parsed,
wrapAt: text.wrapAt,
});
}
//console.log("parsed",this.textElements.get(id).parsed);
@@ -997,21 +1064,18 @@ export class ExcalidrawData {
this.textElements.set(id, {
raw: text.raw,
parsed: text.parsed,
wrapAt: text.wrapAt,
});
this.textElements.delete(te.id); //delete the old ID from the Map
}
if (!this.textElements.has(id)) {
const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
const wrapAt = estimateMaxLineLen(te.text, te.originalText);
this.textElements.set(id, { raw, parsed: null, wrapAt });
this.parseasync(id, raw, wrapAt);
this.textElements.set(id, { raw, parsed: null});
this.parseasync(id, raw);
}
} else if (!this.textElements.has(te.id)) {
const raw = te.rawText && te.rawText !== "" ? te.rawText : te.text; //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
const wrapAt = estimateMaxLineLen(te.text, te.originalText);
this.textElements.set(id, { raw, parsed: null, wrapAt });
this.parseasync(id, raw, wrapAt);
this.textElements.set(id, { raw, parsed: null});
this.parseasync(id, raw);
}
}
@@ -1059,22 +1123,19 @@ export class ExcalidrawData {
? el[0].rawText
: (el[0].originalText ?? el[0].text);
if (text !== (el[0].originalText ?? el[0].text)) {
const wrapAt = estimateMaxLineLen(el[0].text, el[0].originalText);
this.textElements.set(key, {
raw,
parsed: (await this.parse(raw)).parsed,
wrapAt,
});
}
}
}
}
private async parseasync(key: string, raw: string, wrapAt: number) {
private async parseasync(key: string, raw: string) {
this.textElements.set(key, {
raw,
parsed: (await this.parse(raw)).parsed,
wrapAt,
});
}
@@ -1265,7 +1326,7 @@ export class ExcalidrawData {
disableCompression: boolean = false;
generateMD(deletedElements: ExcalidrawElement[] = []): string {
let outString = this.textElementCommentedOut ? "%%\n" : "";
outString += `${MD_TEXTELEMENTS}\n`;
outString += `# Excalidraw Data\n## Text Elements\n`;
const textElementLinks = new Map<string, string>();
for (const key of this.textElements.keys()) {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
@@ -1281,7 +1342,7 @@ export class ExcalidrawData {
}
if (this.elementLinks.size > 0 || textElementLinks.size > 0) {
outString += `${MD_ELEMENTLINKS}\n`;
outString += `## Element Links\n`;
for (const key of this.elementLinks.keys()) {
outString += `${key}: ${this.elementLinks.get(key)}\n`;
}
@@ -1294,7 +1355,7 @@ export class ExcalidrawData {
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
outString +=
this.equations.size > 0 || this.files.size > 0
? "# Embedded files\n"
? "## Embedded Files\n"
: "";
if (this.equations.size > 0) {
for (const key of this.equations.keys()) {
@@ -1541,12 +1602,12 @@ export class ExcalidrawData {
* @param id
* @returns
*/
public getParsedText(id: string): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
public getParsedText(id: string): string {
const t = this.textElements.get(id);
if (!t) {
return [null, null, null];
return null;
}
return [wrap(t.parsed, t.wrapAt), t.parsed, null];
return t.parsed;
}
/**
@@ -1563,24 +1624,23 @@ export class ExcalidrawData {
* @param rawText
* @param rawOriginalText
* @param updateSceneCallback
* @returns [parseResultWrapped: string, parseResultOriginal: string, link: string]
* @returns [parseResultOriginal: string, link: string]
*/
public setTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
updateSceneCallback: Function,
): [parseResultWrapped: string, parseResultOriginal: string, link: string] {
const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
): [parseResultOriginal: string, link: string] {
//const maxLineLen = estimateMaxLineLen(rawText, rawOriginalText);
const [parseResult, link] = this.quickParse(rawOriginalText); //will return the parsed result if raw text does not include transclusion
if (parseResult) {
//No transclusion
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parseResult,
wrapAt: maxLineLen,
});
return [wrap(parseResult, maxLineLen), parseResult, link];
return [parseResult, link];
}
//transclusion needs to be resolved asynchornously
this.parse(rawOriginalText).then((parseRes) => {
@@ -1588,35 +1648,28 @@ export class ExcalidrawData {
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parsedText,
wrapAt: maxLineLen,
});
if (parsedText) {
updateSceneCallback(wrap(parsedText, maxLineLen), parsedText);
updateSceneCallback(parsedText);
}
});
return [null, null, null];
return [null, null];
}
public async addTextElement(
elementID: string,
rawText: string,
rawOriginalText: string,
): Promise<[string, string, string]> {
let wrapAt: number = estimateMaxLineLen(rawText, rawOriginalText);
if (this.textElements.has(elementID)) {
wrapAt = this.textElements.get(elementID).wrapAt;
}
): Promise<{parseResult: string, link:string}> {
const parseResult = await this.parse(rawOriginalText);
this.textElements.set(elementID, {
raw: rawOriginalText,
parsed: parseResult.parsed,
wrapAt,
});
return [
wrap(parseResult.parsed, wrapAt),
parseResult.parsed,
parseResult.link,
];
return {
parseResult: parseResult.parsed,
link: parseResult.link,
};
}
public deleteTextElement(id: string) {
@@ -1630,7 +1683,8 @@ export class ExcalidrawData {
: this.plugin.settings.defaultMode;
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name] !== "undefined")
) {
mode = fileCache.frontmatter[FRONTMATTER_KEYS["default-mode"].name];
}
@@ -1650,7 +1704,8 @@ export class ExcalidrawData {
let opacity = this.plugin.settings.linkOpacity;
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name] !== "undefined")
) {
opacity = fileCache.frontmatter[FRONTMATTER_KEYS["linkbutton-opacity"].name];
}
@@ -1661,7 +1716,8 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["onload-script"].name];
}
@@ -1673,13 +1729,13 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] != null
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name] !== "undefined")
) {
this.linkPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name];
this.linkPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["link-prefix"].name]??"";
} else {
this.linkPrefix = this.plugin.settings.linkPrefix;
}
return linkPrefix != this.linkPrefix;
return linkPrefix !== this.linkPrefix;
}
private setUrlPrefix(): boolean {
@@ -1687,20 +1743,21 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] != null
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name] !== "undefined")
) {
this.urlPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name];
this.urlPrefix = fileCache.frontmatter[FRONTMATTER_KEYS["url-prefix"].name]??"";
} else {
this.urlPrefix = this.plugin.settings.urlPrefix;
}
return urlPrefix != this.urlPrefix;
return urlPrefix !== this.urlPrefix;
}
private setAutoexportPreferences() {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name] !== "undefined")
) {
switch ((fileCache.frontmatter[FRONTMATTER_KEYS["autoexport"].name]).toLowerCase()) {
case "none": this.autoexportPreference = AutoexportPreference.none; break;
@@ -1719,7 +1776,8 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name] !== "undefined")
) {
this.embeddableTheme = fileCache.frontmatter[FRONTMATTER_KEYS["iframe-theme"].name].toLowerCase();
if (!EMBEDDABLE_THEME_FRONTMATTER_VALUES.includes(this.embeddableTheme)) {
@@ -1728,7 +1786,7 @@ export class ExcalidrawData {
} else {
this.embeddableTheme = this.plugin.settings.iframeMatchExcalidrawTheme ? "auto" : "default";
}
return embeddableTheme != this.embeddableTheme;
return embeddableTheme !== this.embeddableTheme;
}
private setShowLinkBrackets(): boolean {
@@ -1736,14 +1794,15 @@ export class ExcalidrawData {
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== "undefined")
) {
this.showLinkBrackets =
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] != false;
fileCache.frontmatter[FRONTMATTER_KEYS["link-brackets"].name] !== false;
} else {
this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
}
return showLinkBrackets != this.showLinkBrackets;
return showLinkBrackets !== this.showLinkBrackets;
}
/**
@@ -1976,7 +2035,7 @@ export const getTransclusion = async (
{ isCancelled: () => false },
file,
)
).blocks.filter((block: any) => block.node.type != "comment");
).blocks.filter((block: any) => block.node.type !== "comment");
if (!blocks) {
return { contents: linkParts.original.trim(), lineNum: 0 };
}

View File

@@ -1,7 +1,7 @@
import { RestoredDataState } from "@zsviczian/excalidraw/types/excalidraw/data/restore";
import { ImportedDataState } from "@zsviczian/excalidraw/types/excalidraw/data/types";
import { BoundingBox } from "@zsviczian/excalidraw/types/excalidraw/element/bounds";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ElementsMap, ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFrameElement, ExcalidrawTextContainer, ExcalidrawTextElement, FontFamilyValues, FontString, NonDeleted, NonDeletedExcalidrawElement, Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { AppState, BinaryFiles, ExportOpts, Point, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
@@ -87,6 +87,24 @@ declare namespace ExcalidrawLib {
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): BoundingBox;
function getContainerElement(
element: ExcalidrawTextElement | null,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null;
function refreshTextDimensions(
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
text: string,
): {
text: string,
x: number,
y: number,
width: number,
height: number,
};
function getMaximumGroups(
elements: ExcalidrawElement[],
elementsMap: ElementsMap,

View File

@@ -51,6 +51,8 @@ import {
fileid,
sceneCoordsToViewportCoords,
MD_EX_SECTIONS,
refreshTextDimensions,
getContainerElement,
} from "./constants/constants";
import ExcalidrawPlugin from "./main";
import {
@@ -99,7 +101,8 @@ import {
fragWithHTML,
isMaskFile,
shouldEmbedScene,
getContainerElement,
_getContainerElement,
arrayToMap,
} from "./utils/Utils";
import { cleanBlockRef, cleanSectionHeading, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf } from "./utils/ObsidianUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
@@ -965,7 +968,7 @@ export default class ExcalidrawView extends TextFileView {
if (!linkText || partsArray.length === 0) {
//the container link takes precedence over the text link
if(selectedTextElement?.containerId) {
const container = getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
}
@@ -1393,7 +1396,7 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const { offsetLeft, offsetTop } = target;
if (offsetLeft !== self.offsetLeft || offsetTop != self.offsetTop) {
if (offsetLeft !== self.offsetLeft || offsetTop !== self.offsetTop) {
if (self.excalidrawAPI) {
self.refreshCanvasOffset();
}
@@ -2796,26 +2799,32 @@ export default class ExcalidrawView extends TextFileView {
if (!api) {
return false;
}
const elementsMap = arrayToMap(api.getSceneElements());
const textElements = newElements.filter((el) => el.type == "text");
for (let i = 0; i < textElements.length; i++) {
const [parseResultWrapped, parseResult, link] =
const textElement = textElements[i] as Mutable<ExcalidrawTextElement>;
const {parseResult, link} =
await this.excalidrawData.addTextElement(
textElements[i].id,
textElement.id,
//@ts-ignore
textElements[i].text,
textElement.text,
//@ts-ignore
textElements[i].rawText, //TODO: implement originalText support in ExcalidrawAutomate
textElement.rawText, //TODO: implement originalText support in ExcalidrawAutomate
);
if (link) {
//@ts-ignore
textElements[i].link = link;
textElement.link = link;
}
if (this.textMode == TextMode.parsed) {
this.excalidrawData.updateTextElement(
textElements[i],
parseResultWrapped,
parseResult,
const {text, x, y, width, height} = refreshTextDimensions(
textElement,null,elementsMap,parseResult
);
textElement.text = text;
textElement.originalText = parseResult;
textElement.x = x;
textElement.y = y;
textElement.width = width;
textElement.height = height;
}
}
@@ -3857,6 +3866,8 @@ export default class ExcalidrawView extends TextFileView {
return true;
}
//returns the raw text of the element which is the original text without parsing
//in compatibility mode, returns the original text, and for backward compatibility the text if originalText is not available
private onBeforeTextEdit (textElement: ExcalidrawTextElement) {
clearTimeout(this.isEditingTextResetTimer);
this.isEditingTextResetTimer = null;
@@ -3871,15 +3882,16 @@ export default class ExcalidrawView extends TextFileView {
return raw;
}
private onBeforeTextSubmit (
textElement: ExcalidrawTextElement,
text: string,
originalText: string,
nextText: string,
nextOriginalText: string,
isDeleted: boolean,
): [string, string, string] {
): {updatedNextOriginalText: string, nextLink: string} {
const api = this.excalidrawAPI;
if (!api) {
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
}
// 1. Set the isEditingText flag to true to prevent autoresize on mobile
@@ -3898,7 +3910,7 @@ export default class ExcalidrawView extends TextFileView {
if (isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.setDirty(7);
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
}
// 3. Check if the user accidently pasted Excalidraw data from the clipboard
@@ -3906,7 +3918,7 @@ export default class ExcalidrawView extends TextFileView {
// textElements cache and update the text element in the scene with a warning.
const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`;
const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT");
if(text.startsWith(FORBIDDEN_TEXT)) {
if(nextOriginalText.startsWith(FORBIDDEN_TEXT)) {
setTimeout(()=>{
const elements = this.excalidrawAPI.getSceneElements();
const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
@@ -3914,23 +3926,23 @@ export default class ExcalidrawView extends TextFileView {
const clone = cloneElement(el[0]);
clone.rawText = WARNING;
elements[elements.indexOf(el[0])] = clone;
this.excalidrawData.setTextElement(clone.id,WARNING,WARNING,()=>{});
this.excalidrawData.setTextElement(clone.id,WARNING,()=>{});
this.updateScene({elements});
api.history.clear();
}
});
return [WARNING,WARNING,null];
return {updatedNextOriginalText:WARNING, nextLink:null};
}
const containerId = textElement.containerId;
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
// 4. Check if the text matches the transclusion pattern and if so,
// check if the link in the transclusion can be resolved to a file in the vault
// if the link can be resolved, check if the file is a markdown file but not an
// Excalidraw file. If so, create a timeout to remove the text element from the
// scene and invoke the UniversalInsertFileModal with the file.
const match = originalText.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
// check if the link in the transclusion can be resolved to a file in the vault.
// If the link is an image or a PDF file, replace the text element with the image or the PDF.
// If the link is an embedded markdown file, then display a message, but otherwise transclude the text step 5.
// 1 2
const REG_TRANSCLUSION = /^!\[\[([^|\]]*)?.*?]]$|^!\[[^\]]*?]\((.*?)\)$/g;
const match = nextOriginalText.trim().matchAll(REG_TRANSCLUSION).next(); //reset the iterator
if(match?.value?.[0]) {
const link = match.value[1] ?? match.value[2];
const file = this.app.metadataCache.getFirstLinkpathDest(link, this.file.path);
@@ -3958,7 +3970,7 @@ export default class ExcalidrawView extends TextFileView {
this.setDirty(9);
}
});
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
} else {
new Notice(t("USE_INSERT_FILE_MODAL"),5000);
}
@@ -3968,8 +3980,7 @@ export default class ExcalidrawView extends TextFileView {
// 5. Check if the user made changes to the text, or
// the text is missing from ExcalidrawData textElements cache (recently copy/pasted)
if (
text !== textElement.text ||
originalText !== textElement.originalText ||
nextOriginalText !== textElement.originalText ||
!this.excalidrawData.getRawText(textElement.id)
) {
//the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
@@ -3978,24 +3989,25 @@ export default class ExcalidrawView extends TextFileView {
// setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions
// in this case I need to update the scene asynchronously when parsing is complete
const callback = async (wrappedParsedText:string, parsedText:string) => {
const callback = async (parsedText:string) => {
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
if(this.textMode === TextMode.raw) return;
const elements = this.excalidrawAPI.getSceneElements();
const elementsMap = arrayToMap(elements);
const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id);
if(el.length === 1) {
const container = getContainerElement(el[0],elementsMap);
const clone = cloneElement(el[0]);
const containerType = el[0].containerId
? api.getSceneElements().filter((e:ExcalidrawElement)=>e.id===el[0].containerId)?.[0]?.type
: undefined;
this.excalidrawData.updateTextElement(
clone,
wrappedParsedText,
parsedText,
true,
containerType
);
const {text, x, y, width, height} = refreshTextDimensions(el[0], container, elementsMap, parsedText);
clone.x = x;
clone.y = y;
clone.width = width;
clone.height = height;
clone.originalText = parsedText;
clone.text = text;
elements[elements.indexOf(el[0])] = clone;
this.updateScene({elements});
if(clone.containerId) this.updateContainerSize(clone.containerId);
@@ -4004,11 +4016,10 @@ export default class ExcalidrawView extends TextFileView {
api.history.clear();
};
const [parseResultWrapped, parseResultOriginal, link] =
const [parseResultOriginal, link] =
this.excalidrawData.setTextElement(
textElement.id,
text,
originalText,
nextOriginalText,
callback,
);
@@ -4017,34 +4028,35 @@ export default class ExcalidrawView extends TextFileView {
// because the parsed text will have a different size than the raw text had
// - depending on the textMode, return the text with markdown markup or the parsed text
// if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element
if (parseResultWrapped) {
if (parseResultOriginal) {
//there were no transclusions in the raw text, quick parse was successful
if (containerId) {
this.updateContainerSize(containerId, true);
}
if (this.textMode === TextMode.raw) {
return [text, originalText, link];
return {updatedNextOriginalText: nextOriginalText, nextLink: link};
} //text is displayed in raw, no need to clear the history, undo will not create problems
if (text === parseResultWrapped) {
if (nextOriginalText === parseResultOriginal) {
if (link) {
//don't forget the case: link-prefix:"" && link-brackets:true
return [parseResultWrapped, parseResultOriginal, link];
return {updatedNextOriginalText: parseResultOriginal, nextLink: link};
}
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
} //There were no links to parse, raw text and parsed text are equivalent
api.history.clear();
return [parseResultWrapped, parseResultOriginal, link];
return {updatedNextOriginalText: parseResultOriginal, nextLink:link};
}
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
}
// even if the text did not change, container sizes might need to be updated
if (containerId) {
this.updateContainerSize(containerId, true);
}
if (this.textMode === TextMode.parsed) {
return this.excalidrawData.getParsedText(textElement.id);
const parseResultOriginal = this.excalidrawData.getParsedText(textElement.id);
return {updatedNextOriginalText: parseResultOriginal, nextLink: textElement.link};
}
return [null, null, null];
return {updatedNextOriginalText: null, nextLink: null};
}
private async onLinkOpen(element: ExcalidrawElement, e: any): Promise<void> {
@@ -4290,14 +4302,13 @@ export default class ExcalidrawView extends TextFileView {
const selectedTextElements = this.getViewSelectedElements().filter(el=>el.type === "text");
if(selectedTextElements.length===1) {
const selectedTextElement = selectedTextElements[0] as ExcalidrawTextElement;
this.excalidrawData.getParsedText(selectedTextElement.id);
const containerElement = (this.getViewElements() as ExcalidrawElement[]).find(el=>el.id === selectedTextElement.containerId);
//if the text element in the container no longer has a link associated with it...
if(
containerElement &&
selectedTextElement.link &&
this.excalidrawData.getParsedText(selectedTextElement.id)[1] === selectedTextElement.rawText
this.excalidrawData.getParsedText(selectedTextElement.id) === selectedTextElement.rawText
) {
contextMenuActions.push([
renderContextMenuAction(
@@ -4866,10 +4877,10 @@ export default class ExcalidrawView extends TextFileView {
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => this.onBeforeTextEdit(textElement),
onBeforeTextSubmit: (
textElement: ExcalidrawTextElement,
text: string,
originalText: string,
nextText: string,
nextOriginalText: string,
isDeleted: boolean,
): [string, string, string] => this.onBeforeTextSubmit(textElement, text, originalText, isDeleted),
): {updatedNextOriginalText: string, nextLink: string} => this.onBeforeTextSubmit(textElement, nextText, nextOriginalText, isDeleted),
onLinkOpen: (element: ExcalidrawElement, e: any) => this.onLinkOpen(element, e),
onLinkHover: (element: NonDeletedExcalidrawElement, event: React.PointerEvent<HTMLCanvasElement>) => this.onLinkHover(element, event),
onContextMenu,

View File

@@ -808,10 +808,10 @@ const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => {
if (!plugin.hover.linkText) {
return;
}
if (m.length != 1) {
if (m.length !== 1) {
return;
}
if (m[0].addedNodes.length != 1) {
if (m[0].addedNodes.length !== 1) {
return;
}
if (

View File

@@ -9,13 +9,12 @@ export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null;
export const setExcalidrawPlugin = (plugin: ExcalidrawPlugin) => {
EXCALIDRAW_PLUGIN = plugin;
};
export const MD_TEXTELEMENTS = "# Text Elements";
export const MD_JSON_START = "```json\n";
export const MD_JSON_END = "```";
export const MD_DRAWING = "# Drawing";
export const MD_ELEMENTLINKS = "# Element Links";
export const MD_EMBEDFILES = "# Embedded files";
export const MD_EX_SECTIONS = [MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
const MD_EXCALIDRAW = "# Excalidraw Data";
const MD_TEXTELEMENTS = "## Text Elements";
const MD_DRAWING = "## Drawing";
const MD_ELEMENTLINKS = "## Element Links";
const MD_EMBEDFILES = "## Embedded Files";
export const MD_EX_SECTIONS = [MD_EXCALIDRAW, MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
@@ -94,6 +93,8 @@ export const {
mutateElement,
restore,
mermaidToExcalidraw,
getContainerElement,
refreshTextDimensions,
} = excalidrawLib;
export function JSON_parse(x: string): any {

View File

@@ -9,6 +9,7 @@ export default {
// main.ts
CONVERT_URL_TO_FILE: "Save image from URL to local file",
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
ZIP_CURRENT_FILE: "Compress current Excalidraw file",
PUBLISH_SVG_CHECK: "Obsidian Publish: Find SVG and PNG exports that are out of date",
EMBEDDABLE_PROPERTIES: "Embeddable Properties",
EMBEDDABLE_RELATIVE_ZOOM: "Scale selected embeddable elements to 100% relative to the current canvas zoom",
@@ -35,6 +36,7 @@ export default {
TRANSCLUDE: "Embed a drawing",
TRANSCLUDE_MOST_RECENT: "Embed the most recently edited drawing",
TOGGLE_LEFTHANDED_MODE: "Toggle left-handed mode",
FLIP_IMAGE: "Open the back-of-the-note of the selected excalidraw image",
NEW_IN_NEW_PANE: "Create new drawing - IN AN ADJACENT WINDOW",
NEW_IN_NEW_TAB: "Create new drawing - IN A NEW TAB",
NEW_IN_ACTIVE_PANE: "Create new drawing - IN THE CURRENT ACTIVE WINDOW",

View File

@@ -20,7 +20,6 @@ import {
Editor,
MarkdownFileInfo,
loadMermaid,
requireApiVersion,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -42,7 +41,6 @@ import {
EXPORT_IMG_ICON,
LOCALE,
IMAGE_TYPES,
MD_TEXTELEMENTS,
setExcalidrawPlugin,
DEVICE
} from "./constants/constants";
@@ -105,7 +103,7 @@ import {
decompress,
getImageSize,
} from "./utils/Utils";
import { editorInsertText, extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { editorInsertText, extractSVGPNGFileName, foldExcalidrawSection, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ScriptEngine } from "./Scripts";
import {
@@ -188,6 +186,8 @@ export default class ExcalidrawPlugin extends Plugin {
private textMeasureDiv:HTMLDivElement = null;
public editorHandler: EditorHandler;
public activeLeafChangeEventHandler: (leaf: WorkspaceLeaf) => Promise<void>;
//if set, the next time this file is opened it will be opened as markdown
public forceToOpenInMarkdownFilepath: string = null;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -401,14 +401,14 @@ export default class ExcalidrawPlugin extends Plugin {
debug(`ExcalidrawPlugin.switchToExcalidarwAfterLoad app.workspace.onLayoutReady`);
let leaf: WorkspaceLeaf;
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
if (
leaf.view instanceof MarkdownView &&
self.isExcalidrawFile(leaf.view.file) &&
fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)
) {
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
self.setExcalidrawView(leaf);
if ( leaf.view instanceof MarkdownView && self.isExcalidrawFile(leaf.view.file)) {
if (fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)) {
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
VIEW_TYPE_EXCALIDRAW;
self.setExcalidrawView(leaf);
} else {
foldExcalidrawSection(leaf.view);
}
}
}
});
@@ -713,7 +713,7 @@ export default class ExcalidrawPlugin extends Plugin {
*/
private experimentalFileTypeDisplay() {
const insertFiletype = (el: HTMLElement) => {
if (el.childElementCount != 1) {
if (el.childElementCount !== 1) {
return;
}
const filename = el.getAttribute("data-path");
@@ -878,9 +878,9 @@ export default class ExcalidrawPlugin extends Plugin {
(async () => {
const data = await this.app.vault.read(activeFile);
const parts = data.split("\n# Drawing\n```compressed-json\n");
const parts = data.split("\n## Drawing\n```compressed-json\n");
if(parts.length!==2) return;
const header = parts[0] + "\n# Drawing\n```json\n";
const header = parts[0] + "\n## Drawing\n```json\n";
const compressed = parts[1].split("\n```\n%%");
if(compressed.length!==2) return;
const decompressed = decompress(compressed[0]);
@@ -1055,7 +1055,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (checking) {
return (
Boolean(this.app.workspace.getActiveViewOfType(MarkdownView)) &&
this.lastActiveExcalidrawFilePath != null
this.lastActiveExcalidrawFilePath !== null
);
}
const file = this.app.vault.getAbstractFileByPath(
@@ -1585,6 +1585,31 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: "flip-image",
name: t("FLIP_IMAGE"),
checkCallback: (checking:boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if(!view) return false;
if(!view.excalidrawAPI) return false;
const els = view.getViewSelectedElements().filter(el=>el.type==="image");
if(els.length !== 1) {
return false;
}
const el = els[0] as ExcalidrawImageElement;
let ef = view.excalidrawData.getFile(el.fileId);
if(!ef) {
return false;
}
if(!this.isExcalidrawFile(ef.file)) {
return false;
}
if(checking) return true;
this.forceToOpenInMarkdownFilepath = ef.file?.path;
this.openDrawing(ef.file, DEVICE.isMobile ? "new-tab":"popout-window", true);
}
})
this.addCommand({
id: "reset-image-to-100",
name: t("RESET_IMG_TO_100"),
@@ -2281,12 +2306,13 @@ export default class ExcalidrawPlugin extends Plugin {
private registerMonkeyPatches() {
const key = "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues";
this.register(
around(Workspace.prototype, {
getActiveViewOfType(old) {
return dedupe(key, old, function(...args) {
const result = old && old.apply(this, args);
const maybeEAView = app?.workspace?.activeLeaf?.view;
const maybeEAView = self.app?.workspace?.activeLeaf?.view;
if(!maybeEAView || !(maybeEAView instanceof ExcalidrawView)) return result;
const error = new Error();
const stackTrace = error.stack;
@@ -2386,28 +2412,38 @@ export default class ExcalidrawPlugin extends Plugin {
setViewState(next) {
return function (state: ViewState, ...rest: any[]) {
const markdownViewLoaded =
self._loaded && // Don't force excalidraw mode during shutdown
state.type === "markdown" && // If we have a markdown file
state.state?.file;
if (
// Don't force excalidraw mode during shutdown
self._loaded &&
// If we have a markdown file
state.type === "markdown" &&
state.state?.file &&
// And the current mode of the file is not set to markdown
self.excalidrawFileModes[this.id || state.state.file] !==
"markdown"
markdownViewLoaded &&
self.excalidrawFileModes[this.id || state.state.file] !== "markdown"
) {
if (fileShouldDefaultAsExcalidraw(state.state.file,this.app)) {
const file = state.state.file;
if ((self.forceToOpenInMarkdownFilepath !== file) && fileShouldDefaultAsExcalidraw(file,this.app)) {
// If we have it, force the view type to excalidraw
const newState = {
...state,
type: VIEW_TYPE_EXCALIDRAW,
};
self.excalidrawFileModes[state.state.file] =
self.excalidrawFileModes[file] =
VIEW_TYPE_EXCALIDRAW;
return next.apply(this, [newState, ...rest]);
}
self.forceToOpenInMarkdownFilepath = null;
}
if(markdownViewLoaded) {
const leaf = this;
setTimeout(async ()=> {
if(!leaf || !leaf.view || !(leaf.view instanceof MarkdownView) ||
!leaf.view.file || !self.isExcalidrawFile(leaf.view.file)
) return;
foldExcalidrawSection(leaf.view)
},500);
}
return next.apply(this, [state, ...rest]);
@@ -3106,10 +3142,10 @@ export default class ExcalidrawPlugin extends Plugin {
}
let leaf: WorkspaceLeaf;
if(location === "popout-window") {
leaf = app.workspace.openPopoutLeaf();
leaf = this.app.workspace.openPopoutLeaf();
}
if(location === "new-tab") {
leaf = app.workspace.getLeaf('tab');
leaf = this.app.workspace.getLeaf('tab');
}
if(!leaf) {
leaf = this.app.workspace.getLeaf(false);
@@ -3190,7 +3226,7 @@ export default class ExcalidrawPlugin extends Plugin {
const textElements = excalidrawData.elements?.filter(
(el: any) => el.type == "text",
);
let outString = `${MD_TEXTELEMENTS}\n`;
let outString = `# Excalidraw Data\n## Text Elements\n`;
let id: string;
for (const te of textElements) {
id = te.id;
@@ -3269,6 +3305,12 @@ export default class ExcalidrawPlugin extends Plugin {
} as ViewState,
eState ? eState : { focus: true },
);
const mdView = leaf.view;
if(mdView instanceof MarkdownView) {
foldExcalidrawSection(mdView);
}
}
public async setExcalidrawView(leaf: WorkspaceLeaf) {

14
src/types.d.ts vendored
View File

@@ -58,6 +58,20 @@ declare module "obsidian" {
},
basePath: string;
}
interface FoldPosition {
from: number;
to: number;
}
interface FoldInfo {
folds: FoldPosition[];
lines: number;
}
interface MarkdownSubView {
applyFoldInfo(foldInfo: FoldInfo): void;
getFoldInfo(): FoldInfo | null;
}
/*interface Editor {
insertText(data: string): void;
}*/

View File

@@ -2,13 +2,14 @@ import {
App,
Editor,
FrontMatterCache,
MarkdownView,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
import ExcalidrawPlugin from "../main";
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
import yaml from "js-yaml";
import yaml, { Mark } from "js-yaml";
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
let parent = element.parentElement;
@@ -350,4 +351,20 @@ export const editorInsertText = (editor: Editor, text: string)=> {
const line = editor.getLine(cursor.line);
const updatedLine = line.slice(0, cursor.ch) + text + line.slice(cursor.ch);
editor.setLine(cursor.line, updatedLine);
}
export const foldExcalidrawSection = (view: MarkdownView) => {
if(!view || !(view instanceof MarkdownView)) return;
const existingFolds = view.currentMode.getFoldInfo();
const lineCount = view.editor.lineCount();
let i = -1;
while(i++<lineCount && view.editor.getLine(i) !== "# Excalidraw Data");
if(i>-1 && i<lineCount) {
const foldPositions = [
...(existingFolds?.folds ?? []),
...[{from: i, to: lineCount-1}],
];
view.currentMode.applyFoldInfo({folds: foldPositions, lines:lineCount});
}
}

View File

@@ -21,9 +21,10 @@ import {
EXCALIDRAW_PLUGIN,
getCommonBoundingBox,
DEVICE,
getContainerElement,
} from "../constants/constants";
import ExcalidrawPlugin from "../main";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExportSettings } from "../ExcalidrawView";
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils";
import { generateEmbeddableLink } from "./CustomEmbeddableUtils";
@@ -293,7 +294,7 @@ export const getSVG = async (
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
? scene.appState?.theme !== "light"
: false,
...scene.appState,
},
@@ -345,7 +346,7 @@ export const getPNG = async (
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme
? scene.appState?.theme != "light"
? scene.appState?.theme !== "light"
: false,
...scene.appState,
},
@@ -392,13 +393,13 @@ export const embedFontsInSVG = (
): SVGSVGElement => {
//replace font references with base64 fonts)
const includesVirgil = !localOnly &&
svg.querySelector("text[font-family^='Virgil']") != null;
svg.querySelector("text[font-family^='Virgil']") !== null;
const includesCascadia = !localOnly &&
svg.querySelector("text[font-family^='Cascadia']") != null;
svg.querySelector("text[font-family^='Cascadia']") !== null;
const includesAssistant = !localOnly &&
svg.querySelector("text[font-family^='Assistant']") != null;
svg.querySelector("text[font-family^='Assistant']") !== null;
const includesLocalFont =
svg.querySelector("text[font-family^='LocalFont']") != null;
svg.querySelector("text[font-family^='LocalFont']") !== null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil || includesLocalFont || includesAssistant)) {
let style = defs.querySelector("style");
@@ -467,7 +468,7 @@ export const scaleLoadedImage = (
}
if(f.shouldScale) {
const elementAspectRatio = w_old / h_old;
if (imageAspectRatio != elementAspectRatio) {
if (imageAspectRatio !== elementAspectRatio) {
dirty = true;
const h_new = Math.sqrt((w_old * h_old * h_image) / w_image);
const w_new = Math.sqrt((w_old * h_old * w_image) / h_image);
@@ -558,7 +559,8 @@ export const isMaskFile = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name] !== "undefined")
) {
return Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["mask"].name]);
}
@@ -574,7 +576,8 @@ export const hasExportTheme = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
) {
return true;
}
@@ -591,7 +594,8 @@ export const getExportTheme = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["export-dark"].name]
? "dark"
@@ -609,7 +613,8 @@ export const shouldEmbedScene = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name] !== "undefined")
) {
return fileCache.frontmatter[FRONTMATTER_KEYS["export-embed-scene"].name];
}
@@ -625,7 +630,8 @@ export const hasExportBackground = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
) {
return true;
}
@@ -641,7 +647,8 @@ export const getWithBackground = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name] !== "undefined")
) {
return !fileCache.frontmatter[FRONTMATTER_KEYS["export-transparent"].name];
}
@@ -657,7 +664,10 @@ export const getExportPadding = (
const fileCache = plugin.app.metadataCache.getFileCache(file);
if(!fileCache?.frontmatter) return plugin.settings.exportPaddingSVG;
if (fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] != null) {
if (
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name] !== "undefined")
) {
const val = parseInt(
fileCache.frontmatter[FRONTMATTER_KEYS["export-padding"].name],
);
@@ -667,7 +677,10 @@ export const getExportPadding = (
}
//deprecated. Retained for backward compatibility
if (fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] != null) {
if (
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name] !== "undefined")
) {
const val = parseInt(
fileCache.frontmatter[FRONTMATTER_KEYS["export-svgpadding"].name],
);
@@ -685,7 +698,8 @@ export const getPNGScale = (plugin: ExcalidrawPlugin, file: TFile): number => {
const fileCache = plugin.app.metadataCache.getFileCache(file);
if (
fileCache?.frontmatter &&
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] != null
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== null &&
(typeof fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name] !== "undefined")
) {
const val = parseFloat(
fileCache.frontmatter[FRONTMATTER_KEYS["export-pngscale"].name],
@@ -764,21 +778,38 @@ export const awaitNextAnimationFrame = async () => new Promise(requestAnimationF
//export const debug = function(){};
export const getContainerElement = (
export const _getContainerElement = (
element:
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
| null,
scene: any,
) => {
if (!element) {
if (!element || !scene?.elements || element.type !== "text") {
return null;
}
if (element.containerId) {
return scene.elements.find((el:ExcalidrawElement)=>el.id === element.containerId) ?? null;
return getContainerElement(element as ExcalidrawTextElement, arrayToMap(scene.elements))
//return scene.elements.find((el:ExcalidrawElement)=>el.id === element.containerId) ?? null;
}
return null;
};
/**
* Transforms array of objects containing `id` attribute,
* or array of ids (strings), into a Map, keyd by `id`.
*/
export const arrayToMap = <T extends { id: string } | string>(
items: readonly T[] | Map<string, T>,
) => {
if (items instanceof Map) {
return items;
}
return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element);
return acc;
}, new Map());
};
export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,string][]):string => {
if(!data || !keyValuePairs) return data;
for(const kvp of keyValuePairs) {
@@ -863,20 +894,4 @@ export const addIframe = (containerEl: HTMLElement, link:string, startAt?: numbe
sandbox: "allow-forms allow-presentation allow-same-origin allow-scripts allow-modals",
},
});
}
/**
* Transforms array of objects containing `id` attribute,
* or array of ids (strings), into a Map, keyd by `id`.
*/
export const arrayToMap = <T extends { id: string } | string>(
items: readonly T[] | Map<string, T>,
) => {
if (items instanceof Map) {
return items;
}
return items.reduce((acc: Map<string, T>, element) => {
acc.set(typeof element === "string" ? element : element.id, element);
return acc;
}, new Map());
};
}