mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-01 12:27:41 +08:00
Merge branch 'develop' into feature/MFA
This commit is contained in:
commit
f42ecb2e83
@ -343,9 +343,8 @@ type EventMappings = {
|
|||||||
noteContextRemoved: {
|
noteContextRemoved: {
|
||||||
ntxIds: string[];
|
ntxIds: string[];
|
||||||
};
|
};
|
||||||
exportSvg: {
|
exportSvg: { ntxId: string | null | undefined; };
|
||||||
ntxId: string | null | undefined;
|
exportPng: { ntxId: string | null | undefined; };
|
||||||
};
|
|
||||||
geoMapCreateChildNote: {
|
geoMapCreateChildNote: {
|
||||||
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
ntxId: string | null | undefined; // TODO: deduplicate ntxId
|
||||||
};
|
};
|
||||||
|
@ -91,6 +91,7 @@ import type { AppContext } from "./../components/app_context.js";
|
|||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
|
import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
|
||||||
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
|
import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
|
||||||
|
import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@ -214,6 +215,7 @@ export default class DesktopLayout {
|
|||||||
.child(new GeoMapButtons())
|
.child(new GeoMapButtons())
|
||||||
.child(new CopyImageReferenceButton())
|
.child(new CopyImageReferenceButton())
|
||||||
.child(new SvgExportButton())
|
.child(new SvgExportButton())
|
||||||
|
.child(new PngExportButton())
|
||||||
.child(new BacklinksWidget())
|
.child(new BacklinksWidget())
|
||||||
.child(new ContextualHelpButton())
|
.child(new ContextualHelpButton())
|
||||||
.child(new HideFloatingButtonsButton())
|
.child(new HideFloatingButtonsButton())
|
||||||
|
@ -609,9 +609,20 @@ function createImageSrcUrl(note: { noteId: string; title: string }) {
|
|||||||
*/
|
*/
|
||||||
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
||||||
const filename = `${nameWithoutExtension}.svg`;
|
const filename = `${nameWithoutExtension}.svg`;
|
||||||
|
const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`;
|
||||||
|
triggerDownload(filename, dataUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the given data URL on the client device, with a custom file name.
|
||||||
|
*
|
||||||
|
* @param fileName the name to give the downloaded file.
|
||||||
|
* @param dataUrl the data URI to download.
|
||||||
|
*/
|
||||||
|
function triggerDownload(fileName: string, dataUrl: string) {
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
element.setAttribute("href", `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
|
element.setAttribute("href", dataUrl);
|
||||||
element.setAttribute("download", filename);
|
element.setAttribute("download", fileName);
|
||||||
|
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
@ -621,6 +632,56 @@ function downloadSvg(nameWithoutExtension: string, svgContent: string) {
|
|||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string representation of an SVG, renders the SVG to PNG and triggers a download of the file on the client device.
|
||||||
|
*
|
||||||
|
* Note that the SVG must specify its width and height as attributes in order for it to be rendered.
|
||||||
|
*
|
||||||
|
* @param nameWithoutExtension the name of the file. The .png suffix is automatically added to it.
|
||||||
|
* @param svgContent the content of the SVG file download.
|
||||||
|
* @returns `true` if the operation succeeded (width/height present), or `false` if the download was not triggered.
|
||||||
|
*/
|
||||||
|
function downloadSvgAsPng(nameWithoutExtension: string, svgContent: string) {
|
||||||
|
const mime = "image/svg+xml";
|
||||||
|
|
||||||
|
// First, we need to determine the width and the height from the input SVG.
|
||||||
|
const svgDocument = (new DOMParser()).parseFromString(svgContent, mime);
|
||||||
|
const width = svgDocument.documentElement?.getAttribute("width");
|
||||||
|
const height = svgDocument.documentElement?.getAttribute("height");
|
||||||
|
|
||||||
|
if (!width || !height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the image to a blob.
|
||||||
|
const svgBlob = new Blob([ svgContent ], {
|
||||||
|
type: mime
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create an image element and load the SVG.
|
||||||
|
const imageEl = new Image();
|
||||||
|
imageEl.width = parseFloat(width);
|
||||||
|
imageEl.height = parseFloat(height);
|
||||||
|
imageEl.src = URL.createObjectURL(svgBlob);
|
||||||
|
imageEl.onload = () => {
|
||||||
|
// Draw the image with a canvas.
|
||||||
|
const canvasEl = document.createElement("canvas");
|
||||||
|
canvasEl.width = imageEl.width;
|
||||||
|
canvasEl.height = imageEl.height;
|
||||||
|
document.body.appendChild(canvasEl);
|
||||||
|
|
||||||
|
const ctx = canvasEl.getContext("2d");
|
||||||
|
ctx?.drawImage(imageEl, 0, 0);
|
||||||
|
URL.revokeObjectURL(imageEl.src);
|
||||||
|
|
||||||
|
const imgUri = canvasEl.toDataURL("image/png")
|
||||||
|
triggerDownload(`${nameWithoutExtension}.png`, imgUri);
|
||||||
|
document.body.removeChild(canvasEl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compares two semantic version strings.
|
* Compares two semantic version strings.
|
||||||
* Returns:
|
* Returns:
|
||||||
@ -719,6 +780,7 @@ export default {
|
|||||||
copyHtmlToClipboard,
|
copyHtmlToClipboard,
|
||||||
createImageSrcUrl,
|
createImageSrcUrl,
|
||||||
downloadSvg,
|
downloadSvg,
|
||||||
|
downloadSvgAsPng,
|
||||||
compareVersions,
|
compareVersions,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
isLaunchBarConfig
|
isLaunchBarConfig
|
||||||
|
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
24
src/public/app/widgets/floating_buttons/png_export_button.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { t } from "../../services/i18n.js";
|
||||||
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<button type="button"
|
||||||
|
class="export-svg-button"
|
||||||
|
title="${t("png_export_button.button_title")}">
|
||||||
|
<span class="bx bxs-file-png"></span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default class PngExportButton extends NoteContextAwareWidget {
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled() && ["mermaid"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
super.doRender();
|
||||||
|
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$widget.on("click", () => this.triggerEvent("exportPng", { ntxId: this.ntxId }));
|
||||||
|
this.contentSized();
|
||||||
|
}
|
||||||
|
}
|
@ -40,6 +40,10 @@ const TPL = `\
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note-detail-split .note-detail-split-editor .note-detail-code {
|
||||||
|
contain: size !important;
|
||||||
|
}
|
||||||
|
|
||||||
.note-detail-split .note-detail-error-container {
|
.note-detail-split .note-detail-error-container {
|
||||||
font-family: var(--monospace-font-family);
|
font-family: var(--monospace-font-family);
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
@ -184,7 +188,7 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vertical vs horizontal layout
|
// Vertical vs horizontal layout
|
||||||
const layoutOrientation = options.get("splitEditorOrientation") ?? "horizontal";
|
const layoutOrientation = (!utils.isMobile() ? options.get("splitEditorOrientation") ?? "horizontal" : "vertical");
|
||||||
if (this.layoutOrientation === layoutOrientation && this.isReadOnly === isReadOnly) {
|
if (this.layoutOrientation === layoutOrientation && this.isReadOnly === isReadOnly) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -217,4 +217,12 @@ export default abstract class AbstractSvgSplitTypeWidget extends AbstractSplitTy
|
|||||||
utils.downloadSvg(this.note.title, this.svg);
|
utils.downloadSvg(this.note.title, this.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async exportPngEvent({ ntxId }: EventData<"exportPng">) {
|
||||||
|
if (!this.isNoteContext(ntxId) || this.note?.type !== "mermaid" || !this.svg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.downloadSvgAsPng(this.note.title, this.svg);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1724,5 +1724,8 @@
|
|||||||
"toggle_read_only_button": {
|
"toggle_read_only_button": {
|
||||||
"unlock-editing": "Unlock editing",
|
"unlock-editing": "Unlock editing",
|
||||||
"lock-editing": "Lock editing"
|
"lock-editing": "Lock editing"
|
||||||
|
},
|
||||||
|
"png_export_button": {
|
||||||
|
"button_title": "Export diagram as PNG"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
src/services/export/single.spec.ts
Normal file
17
src/services/export/single.spec.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import BNote from "../../becca/entities/bnote.js";
|
||||||
|
import { mapByNoteType } from "./single.js";
|
||||||
|
|
||||||
|
describe("Note type mappings", () => {
|
||||||
|
it("supports mermaid note", () => {
|
||||||
|
const note = new BNote({
|
||||||
|
type: "mermaid",
|
||||||
|
title: "New note"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mapByNoteType(note, "", "html")).toMatchObject({
|
||||||
|
extension: "mermaid",
|
||||||
|
mime: "text/vnd.mermaid"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -8,6 +8,7 @@ import becca from "../../becca/becca.js";
|
|||||||
import type TaskContext from "../task_context.js";
|
import type TaskContext from "../task_context.js";
|
||||||
import type BBranch from "../../becca/entities/bbranch.js";
|
import type BBranch from "../../becca/entities/bbranch.js";
|
||||||
import type { Response } from "express";
|
import type { Response } from "express";
|
||||||
|
import type BNote from "../../becca/entities/bnote.js";
|
||||||
|
|
||||||
function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
|
function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "html" | "markdown", res: Response) {
|
||||||
const note = branch.getNote();
|
const note = branch.getNote();
|
||||||
@ -20,9 +21,21 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
|
|||||||
return [400, `Unrecognized format '${format}'`];
|
return [400, `Unrecognized format '${format}'`];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { payload, extension, mime } = mapByNoteType(note, note.getContent(), format);
|
||||||
|
const fileName = `${note.title}.${extension}`;
|
||||||
|
|
||||||
|
res.setHeader("Content-Disposition", getContentDisposition(fileName));
|
||||||
|
res.setHeader("Content-Type", `${mime}; charset=UTF-8`);
|
||||||
|
|
||||||
|
res.send(payload);
|
||||||
|
|
||||||
|
taskContext.increaseProgressCount();
|
||||||
|
taskContext.taskSucceeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapByNoteType(note: BNote, content: string | Buffer<ArrayBufferLike>, format: "html" | "markdown") {
|
||||||
let payload, extension, mime;
|
let payload, extension, mime;
|
||||||
|
|
||||||
let content = note.getContent();
|
|
||||||
if (typeof content !== "string") {
|
if (typeof content !== "string") {
|
||||||
throw new Error("Unsupported content type for export.");
|
throw new Error("Unsupported content type for export.");
|
||||||
}
|
}
|
||||||
@ -52,21 +65,17 @@ function exportSingleNote(taskContext: TaskContext, branch: BBranch, format: "ht
|
|||||||
payload = content;
|
payload = content;
|
||||||
extension = "excalidraw";
|
extension = "excalidraw";
|
||||||
mime = "application/json";
|
mime = "application/json";
|
||||||
|
} else if (note.type === "mermaid") {
|
||||||
|
payload = content;
|
||||||
|
extension = "mermaid";
|
||||||
|
mime = "text/vnd.mermaid";
|
||||||
} else if (note.type === "relationMap" || note.type === "search") {
|
} else if (note.type === "relationMap" || note.type === "search") {
|
||||||
payload = content;
|
payload = content;
|
||||||
extension = "json";
|
extension = "json";
|
||||||
mime = "application/json";
|
mime = "application/json";
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = `${note.title}.${extension}`;
|
return { payload, extension, mime };
|
||||||
|
|
||||||
res.setHeader("Content-Disposition", getContentDisposition(fileName));
|
|
||||||
res.setHeader("Content-Type", `${mime}; charset=UTF-8`);
|
|
||||||
|
|
||||||
res.send(payload);
|
|
||||||
|
|
||||||
taskContext.increaseProgressCount();
|
|
||||||
taskContext.taskSucceeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function inlineAttachments(content: string) {
|
function inlineAttachments(content: string) {
|
||||||
|
@ -26,6 +26,15 @@ describe("#getMime", () => {
|
|||||||
["test.excalidraw"], "application/json"
|
["test.excalidraw"], "application/json"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
[
|
||||||
|
"File extension ('.mermaid') that is defined in EXTENSION_TO_MIME",
|
||||||
|
["test.mermaid"], "text/vnd.mermaid"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"File extension ('.mermaid') that is defined in EXTENSION_TO_MIME",
|
||||||
|
["test.mmd"], "text/vnd.mermaid"
|
||||||
|
],
|
||||||
|
|
||||||
[
|
[
|
||||||
"File extension with inconsistent capitalization that is defined in EXTENSION_TO_MIME",
|
"File extension with inconsistent capitalization that is defined in EXTENSION_TO_MIME",
|
||||||
["test.gRoOvY"], "text/x-groovy"
|
["test.gRoOvY"], "text/x-groovy"
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import mimeTypes from "mime-types";
|
import mimeTypes from "mime-types";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type { TaskData } from "../task_context_interface.js";
|
import type { TaskData } from "../task_context_interface.js";
|
||||||
|
import type { NoteType } from "../../becca/entities/rows.js";
|
||||||
|
|
||||||
const CODE_MIME_TYPES = new Set([
|
const CODE_MIME_TYPES = new Set([
|
||||||
"application/json",
|
"application/json",
|
||||||
@ -68,7 +69,9 @@ const EXTENSION_TO_MIME = new Map<string, string>([
|
|||||||
[".scala", "text/x-scala"],
|
[".scala", "text/x-scala"],
|
||||||
[".swift", "text/x-swift"],
|
[".swift", "text/x-swift"],
|
||||||
[".ts", "text/x-typescript"],
|
[".ts", "text/x-typescript"],
|
||||||
[".excalidraw", "application/json"]
|
[".excalidraw", "application/json"],
|
||||||
|
[".mermaid", "text/vnd.mermaid"],
|
||||||
|
[".mmd", "text/vnd.mermaid"]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** @returns false if MIME is not detected */
|
/** @returns false if MIME is not detected */
|
||||||
@ -85,7 +88,7 @@ function getMime(fileName: string) {
|
|||||||
return mimeFromExt || mimeTypes.lookup(fileNameLc);
|
return mimeFromExt || mimeTypes.lookup(fileNameLc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getType(options: TaskData, mime: string) {
|
function getType(options: TaskData, mime: string): NoteType {
|
||||||
const mimeLc = mime?.toLowerCase();
|
const mimeLc = mime?.toLowerCase();
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
@ -98,6 +101,9 @@ function getType(options: TaskData, mime: string) {
|
|||||||
case mime.startsWith("image/"):
|
case mime.startsWith("image/"):
|
||||||
return "image";
|
return "image";
|
||||||
|
|
||||||
|
case mime === "text/vnd.mermaid":
|
||||||
|
return "mermaid";
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "file";
|
return "file";
|
||||||
}
|
}
|
||||||
|
5
src/services/import/samples/New note.mermaid
Normal file
5
src/services/import/samples/New note.mermaid
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
A-->C;
|
||||||
|
B-->D;
|
||||||
|
C-->D;
|
5
src/services/import/samples/New note.mmd
Normal file
5
src/services/import/samples/New note.mmd
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
A-->C;
|
||||||
|
B-->D;
|
||||||
|
C-->D;
|
@ -96,4 +96,22 @@ describe("processNoteContent", () => {
|
|||||||
expect(importedNote.type).toBe("canvas");
|
expect(importedNote.type).toBe("canvas");
|
||||||
expect(importedNote.title).toBe("New note");
|
expect(importedNote.title).toBe("New note");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("imports .mermaid as mermaid note", async () => {
|
||||||
|
const { importedNote } = await testImport("New note.mermaid", "application/json");
|
||||||
|
expect(importedNote).toMatchObject({
|
||||||
|
mime: "text/vnd.mermaid",
|
||||||
|
type: "mermaid",
|
||||||
|
title: "New note"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("imports .mmd as mermaid note", async () => {
|
||||||
|
const { importedNote } = await testImport("New note.mmd", "application/json");
|
||||||
|
expect(importedNote).toMatchObject({
|
||||||
|
mime: "text/vnd.mermaid",
|
||||||
|
type: "mermaid",
|
||||||
|
title: "New note"
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,10 @@ function importSingleFile(taskContext: TaskContext, file: File, parentNote: BNot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mime === "text/vnd.mermaid") {
|
||||||
|
return importCustomType(taskContext, file, parentNote, "mermaid", mime);
|
||||||
|
}
|
||||||
|
|
||||||
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === "code") {
|
if (taskContext?.data?.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === "code") {
|
||||||
return importCodeNote(taskContext, file, parentNote);
|
return importCodeNote(taskContext, file, parentNote);
|
||||||
}
|
}
|
||||||
@ -93,6 +97,24 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
|
|||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function importCustomType(taskContext: TaskContext, file: File, parentNote: BNote, type: NoteType, mime: string) {
|
||||||
|
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
||||||
|
const content = processStringOrBuffer(file.buffer);
|
||||||
|
|
||||||
|
const { note } = noteService.createNewNote({
|
||||||
|
parentNoteId: parentNote.noteId,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
type,
|
||||||
|
mime: mime,
|
||||||
|
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
|
||||||
|
});
|
||||||
|
|
||||||
|
taskContext.increaseProgressCount();
|
||||||
|
|
||||||
|
return note;
|
||||||
|
}
|
||||||
|
|
||||||
function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
|
function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||||
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
||||||
const plainTextContent = processStringOrBuffer(file.buffer);
|
const plainTextContent = processStringOrBuffer(file.buffer);
|
||||||
|
@ -8,7 +8,7 @@ const noteTypes = [
|
|||||||
{ type: "relationMap", defaultMime: "application/json" },
|
{ type: "relationMap", defaultMime: "application/json" },
|
||||||
{ type: "book", defaultMime: "" },
|
{ type: "book", defaultMime: "" },
|
||||||
{ type: "noteMap", defaultMime: "" },
|
{ type: "noteMap", defaultMime: "" },
|
||||||
{ type: "mermaid", defaultMime: "text/plain" },
|
{ type: "mermaid", defaultMime: "text/vnd.mermaid" },
|
||||||
{ type: "canvas", defaultMime: "application/json" },
|
{ type: "canvas", defaultMime: "application/json" },
|
||||||
{ type: "webView", defaultMime: "" },
|
{ type: "webView", defaultMime: "" },
|
||||||
{ type: "launcher", defaultMime: "" },
|
{ type: "launcher", defaultMime: "" },
|
||||||
|
@ -181,6 +181,8 @@ export function removeTextFileExtension(filePath: string) {
|
|||||||
case ".html":
|
case ".html":
|
||||||
case ".htm":
|
case ".htm":
|
||||||
case ".excalidraw":
|
case ".excalidraw":
|
||||||
|
case ".mermaid":
|
||||||
|
case ".mmd":
|
||||||
return filePath.substring(0, filePath.length - extension.length);
|
return filePath.substring(0, filePath.length - extension.length);
|
||||||
default:
|
default:
|
||||||
return filePath;
|
return filePath;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user