diff --git a/apps/obsidian/src/components/NodeTypeModal.tsx b/apps/obsidian/src/components/NodeTypeModal.tsx index bcb8aac07..6e91d65d1 100644 --- a/apps/obsidian/src/components/NodeTypeModal.tsx +++ b/apps/obsidian/src/components/NodeTypeModal.tsx @@ -1,6 +1,7 @@ import { App, Editor, SuggestModal, TFile, Notice } from "obsidian"; -import { DiscourseNode } from "../types"; -import { getDiscourseNodeFormatExpression } from "../utils/getDiscourseNodeFormatExpression"; +import { DiscourseNode } from "~/types"; +import { getDiscourseNodeFormatExpression } from "~/utils/getDiscourseNodeFormatExpression"; +import { checkInvalidChars } from "~/utils/validateNodeType"; export class NodeTypeModal extends SuggestModal { constructor( @@ -25,6 +26,7 @@ export class NodeTypeModal extends SuggestModal { renderSuggestion(nodeType: DiscourseNode, el: HTMLElement) { el.createEl("div", { text: nodeType.name }); } + async createDiscourseNode( title: string, nodeType: DiscourseNode, @@ -67,7 +69,12 @@ export class NodeTypeModal extends SuggestModal { nodeFormat[1]?.replace(/\\/g, "") + selectedText + nodeFormat[2]?.replace(/\\/g, ""); - if (!nodeFormat) return; + + const isFilenameValid = checkInvalidChars(formattedNodeName); + if (!isFilenameValid.isValid) { + new Notice(`${isFilenameValid.error}`, 5000); + return; + } const newFile = await this.createDiscourseNode(formattedNodeName, nodeType); if (newFile) { diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index f6cf97d9a..c81c967c1 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -146,7 +146,7 @@ const NodeTypeSettings = () => { /> handleNodeTypeChange(index, "format", e.target.value) diff --git a/apps/obsidian/src/utils/validateNodeType.ts b/apps/obsidian/src/utils/validateNodeType.ts index 816cb9874..d7fbc84d8 100644 --- a/apps/obsidian/src/utils/validateNodeType.ts +++ b/apps/obsidian/src/utils/validateNodeType.ts @@ -1,12 +1,14 @@ import { DiscourseNode } from "~/types"; +type ValidationResult = { + isValid: boolean; + error?: string; +}; + export function validateNodeFormat( format: string, nodeTypes: DiscourseNode[], -): { - isValid: boolean; - error?: string; -} { +): ValidationResult { if (!format) { return { isValid: false, @@ -28,17 +30,35 @@ export function validateNodeFormat( }; } - const { isValid, error } = validateFormatUniqueness(nodeTypes); - if (!isValid) { - return { isValid: false, error }; + const invalidCharsResult = checkInvalidChars(format); + if (!invalidCharsResult.isValid) { + return invalidCharsResult; + } + + const uniquenessResult = validateFormatUniqueness(nodeTypes); + if (!uniquenessResult.isValid) { + return uniquenessResult; } return { isValid: true }; } +export const checkInvalidChars = (format: string): ValidationResult => { + const INVALID_FILENAME_CHARS_REGEX = /[#^\[\]|]/; + const invalidCharMatch = format.match(INVALID_FILENAME_CHARS_REGEX); + if (invalidCharMatch) { + return { + isValid: false, + error: `Node contains invalid character: ${invalidCharMatch[0]}. Characters #, ^, [, ], | cannot be used in filenames.`, + }; + } + + return { isValid: true }; +}; + const validateFormatUniqueness = ( nodeTypes: DiscourseNode[], -): { isValid: boolean; error?: string } => { +): ValidationResult => { const isDuplicate = new Set(nodeTypes.map((nodeType) => nodeType.format)).size !== nodeTypes.length; @@ -53,7 +73,7 @@ const validateFormatUniqueness = ( export const validateNodeName = ( name: string, nodeTypes: DiscourseNode[], -): { isValid: boolean; error?: string } => { +): ValidationResult => { if (!name || name.trim() === "") { return { isValid: false, error: "Name is required" }; }