diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts index 01bcd6f441b1..3f41799a6ee4 100644 --- a/frontend/__tests__/utils/strings.spec.ts +++ b/frontend/__tests__/utils/strings.spec.ts @@ -591,6 +591,78 @@ describe("string utils", () => { }); }); + describe("stripMarkdown", () => { + it.each([ + // Headings + ["# Heading 1", "Heading 1", "heading 1"], + ["## Heading 2", "Heading 2", "heading 2"], + ["###### Heading 6", "Heading 6", "heading 6"], + // Bold (only 2+ consecutive markers are formatting) + ["**bold** text", "bold text", "double asterisk bold"], + ["__bold__ text", "bold text", "double underscore bold"], + // Bold + italic (3+ consecutive) + ["***bold italic***", "bold italic", "triple asterisk bold italic"], + // Single * or _ are NOT stripped (literal punctuation) + ["a *b* c", "a *b* c", "single asterisk is literal"], + ["a _b_ c", "a _b_ c", "single underscore is literal"], + // Links (keep text, remove URL) + ["[click here](https://example.com)", "click here", "link"], + // Images (keep alt text) + ["", "alt text", "image"], + // Inline code + ["use `console.log` here", "use console.log here", "inline code"], + // Code blocks (remove entirely) + [ + "some text\n```\ncode block\n```\nmore text", + "some text\nmore text", + "code block", + ], + // Blockquotes + ["> quoted text", "quoted text", "blockquote"], + // Horizontal rules + ["before\n---\nafter", "before\nafter", "horizontal rule (dash)"], + ["before\n***\nafter", "before\nafter", "horizontal rule (asterisk)"], + // List markers + [ + "- item 1\n- item 2\n- item 3", + "item 1\nitem 2\nitem 3", + "unordered list", + ], + ["1. first\n2. second\n3. third", "first\nsecond\nthird", "ordered list"], + // HTML tags + ["
paragraph
", "paragraph", "HTML tags"], + // Mixed markdown + [ + "# Hello\n\nThis is **bold** and italic.\n\n[link](url)\n\n```\ncode\n```\n\n> quote\n\n---\n\n- list item", + "Hello\nThis is bold and italic.\nlink\nquote\nlist item", + "mixed markdown", + ], + // Strikethrough + ["~~deleted~~ text", "deleted text", "strikethrough"], + // Task lists + ["- [ ] unchecked\n- [x] checked", "unchecked\nchecked", "task list"], + // Tables (pipe characters removed, separator line remains) + [ + "| Header | Header |\n| ------ | ------ |\n| Cell | Cell |", + "Header Header \n ------ ------ \n Cell Cell", + "table", + ], + // Plain text (no markdown) + ["just plain text", "just plain text", "plain text"], + // Empty string + ["", "", "empty string"], + // Plain text (no markdown) + ["just plain text", "just plain text", "plain text"], + // Empty string + ["", "", "empty string"], + ])( + "should strip markdown: %s -> %s (%s)", + (input: string, expected: string, _description: string) => { + expect(Strings.stripMarkdown(input)).toBe(expected); + }, + ); + }); + describe("countChars", () => { describe("it should count characters correctly", () => { const testCases = [ diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index 602cda856cab..943f840c2dcd 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -324,8 +324,17 @@ export function CustomTextModal(): JSXElement { const file = fileInputRef?.files?.[0]; if (!file) return; - if (file.type !== "text/plain") { - showErrorNotification("File is not a text file", { durationMs: 5000 }); + const fileExtension = file.name.split(".").pop()?.toLowerCase() ?? ""; + + const isTextFile = file.type === "text/plain" || fileExtension === "txt"; + + const isMarkdownFile = + file.type === "text/markdown" || + file.type === "text/x-markdown" || + fileExtension === "md"; + + if (!isMarkdownFile && !isTextFile) { + showErrorNotification("Unsupported file type", { durationMs: 5000 }); return; } @@ -333,7 +342,8 @@ export function CustomTextModal(): JSXElement { reader.readAsText(file, "UTF-8"); reader.onload = (e) => { const content = e.target?.result as string; - form.setFieldValue("text", content); + const text = isMarkdownFile ? Strings.stripMarkdown(content) : content; + form.setFieldValue("text", text); fileInputRef.value = ""; }; reader.onerror = () => { @@ -618,7 +628,7 @@ export function CustomTextModal(): JSXElement { ref={fileInputRef} type="file" class="hidden" - accept=".txt" + accept=".txt,.md" onChange={handleFileOpen} />