Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions frontend/__tests__/utils/strings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.png)", "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
["<p>paragraph</p>", "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 = [
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/ts/components/modals/CustomTextModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,16 +324,26 @@ 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;
}

const reader = new FileReader();
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 = () => {
Expand Down Expand Up @@ -618,7 +628,7 @@ export function CustomTextModal(): JSXElement {
ref={fileInputRef}
type="file"
class="hidden"
accept=".txt"
accept=".txt,.md"
onChange={handleFileOpen}
/>
<Button
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/ts/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,51 @@ export function replaceUnderscoresWithSpaces(text: string): string {
return text.replace(/_/g, " ");
}

/**
* Strips all Markdown syntax from a string, returning plain text only.
* Handles: code blocks, inline code, images, links, headings, bold/italic,
* strikethrough, blockquotes, horizontal rules, list markers, task lists,
* tables, and HTML tags.
* @param text The Markdown string to strip.
* @returns The plain text with all Markdown syntax removed.
*/
export function stripMarkdown(text: string): string {
// 1. Remove fenced code blocks (with optional language specifier)
text = text.replace(/```[\s\S]*?```/g, "");
// 2. Remove inline code
text = text.replace(/`([^`]+)`/g, "$1");
// 3. Remove images (keep alt text)
text = text.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
// 4. Remove links (keep text)
text = text.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1");
// 5. Remove strikethrough markers
text = text.replace(/~~([^~]+)~~/g, "$1");
// 6. Remove heading markers
text = text.replace(/^#{1,6}\s+/gm, "");
// 7. Remove bold/italic markers (2+ consecutive, not single chars)
text = text.replace(/\*{2,}|_{2,}/g, "");
// 8. Remove blockquote markers
text = text.replace(/^>\s*/gm, "");
// 9. Remove horizontal rules
text = text.replace(/^[\-*_*]{3,}\s*$/gm, "");
// 10. Remove task list markers (bare or prefixed with -*/+)
text = text.replace(/^\s*[-*+]\s*\[[ xX]\]\s+/gm, "");
text = text.replace(/^\s*\[[ xX]\]\s+/gm, "");
// 11. Remove unordered list markers
text = text.replace(/^\s*[-*+]\s+/gm, "");
// 12. Remove ordered list markers
text = text.replace(/^\s*[\d]+\.\s+/gm, "");
// 13. Remove table pipe characters
text = text.replace(/\|/g, "");
// 14. Remove HTML tags
text = text.replace(/<[^>]+>/g, "");
// 15. Normalize whitespace
text = text.replace(/[\r\n]+/g, "\n");
text = text.replace(/ +/g, " ");
text = text.trim();
return text;
}

export function replaceSpacesWithUnderscores(text: string): string {
return text.replace(/ /g, "_");
}
Expand Down
Loading