Spaces:
Running
Running
| import { describe, expect, it } from "vitest"; | |
| import { | |
| extractMarkdownTables, | |
| extractCodeBlocks, | |
| extractLinks, | |
| stripMarkdown, | |
| processLineMessage, | |
| convertTableToFlexBubble, | |
| convertCodeBlockToFlexBubble, | |
| hasMarkdownToConvert, | |
| } from "./markdown-to-line.js"; | |
| describe("extractMarkdownTables", () => { | |
| it("extracts a simple 2-column table", () => { | |
| const text = `Here is a table: | |
| | Name | Value | | |
| |------|-------| | |
| | foo | 123 | | |
| | bar | 456 | | |
| And some more text.`; | |
| const { tables, textWithoutTables } = extractMarkdownTables(text); | |
| expect(tables).toHaveLength(1); | |
| expect(tables[0].headers).toEqual(["Name", "Value"]); | |
| expect(tables[0].rows).toEqual([ | |
| ["foo", "123"], | |
| ["bar", "456"], | |
| ]); | |
| expect(textWithoutTables).toContain("Here is a table:"); | |
| expect(textWithoutTables).toContain("And some more text."); | |
| expect(textWithoutTables).not.toContain("|"); | |
| }); | |
| it("extracts a multi-column table", () => { | |
| const text = `| Col A | Col B | Col C | | |
| |-------|-------|-------| | |
| | 1 | 2 | 3 | | |
| | a | b | c |`; | |
| const { tables } = extractMarkdownTables(text); | |
| expect(tables).toHaveLength(1); | |
| expect(tables[0].headers).toEqual(["Col A", "Col B", "Col C"]); | |
| expect(tables[0].rows).toHaveLength(2); | |
| }); | |
| it("extracts multiple tables", () => { | |
| const text = `Table 1: | |
| | A | B | | |
| |---|---| | |
| | 1 | 2 | | |
| Table 2: | |
| | X | Y | | |
| |---|---| | |
| | 3 | 4 |`; | |
| const { tables } = extractMarkdownTables(text); | |
| expect(tables).toHaveLength(2); | |
| expect(tables[0].headers).toEqual(["A", "B"]); | |
| expect(tables[1].headers).toEqual(["X", "Y"]); | |
| }); | |
| it("handles tables with alignment markers", () => { | |
| const text = `| Left | Center | Right | | |
| |:-----|:------:|------:| | |
| | a | b | c |`; | |
| const { tables } = extractMarkdownTables(text); | |
| expect(tables).toHaveLength(1); | |
| expect(tables[0].headers).toEqual(["Left", "Center", "Right"]); | |
| expect(tables[0].rows).toEqual([["a", "b", "c"]]); | |
| }); | |
| it("returns empty when no tables present", () => { | |
| const text = "Just some plain text without tables."; | |
| const { tables, textWithoutTables } = extractMarkdownTables(text); | |
| expect(tables).toHaveLength(0); | |
| expect(textWithoutTables).toBe(text); | |
| }); | |
| }); | |
| describe("extractCodeBlocks", () => { | |
| it("extracts a code block with language", () => { | |
| const text = `Here is some code: | |
| \`\`\`javascript | |
| const x = 1; | |
| console.log(x); | |
| \`\`\` | |
| And more text.`; | |
| const { codeBlocks, textWithoutCode } = extractCodeBlocks(text); | |
| expect(codeBlocks).toHaveLength(1); | |
| expect(codeBlocks[0].language).toBe("javascript"); | |
| expect(codeBlocks[0].code).toBe("const x = 1;\nconsole.log(x);"); | |
| expect(textWithoutCode).toContain("Here is some code:"); | |
| expect(textWithoutCode).toContain("And more text."); | |
| expect(textWithoutCode).not.toContain("```"); | |
| }); | |
| it("extracts a code block without language", () => { | |
| const text = `\`\`\` | |
| plain code | |
| \`\`\``; | |
| const { codeBlocks } = extractCodeBlocks(text); | |
| expect(codeBlocks).toHaveLength(1); | |
| expect(codeBlocks[0].language).toBeUndefined(); | |
| expect(codeBlocks[0].code).toBe("plain code"); | |
| }); | |
| it("extracts multiple code blocks", () => { | |
| const text = `\`\`\`python | |
| print("hello") | |
| \`\`\` | |
| Some text | |
| \`\`\`bash | |
| echo "world" | |
| \`\`\``; | |
| const { codeBlocks } = extractCodeBlocks(text); | |
| expect(codeBlocks).toHaveLength(2); | |
| expect(codeBlocks[0].language).toBe("python"); | |
| expect(codeBlocks[1].language).toBe("bash"); | |
| }); | |
| it("returns empty when no code blocks present", () => { | |
| const text = "No code here, just text."; | |
| const { codeBlocks, textWithoutCode } = extractCodeBlocks(text); | |
| expect(codeBlocks).toHaveLength(0); | |
| expect(textWithoutCode).toBe(text); | |
| }); | |
| }); | |
| describe("extractLinks", () => { | |
| it("extracts markdown links", () => { | |
| const text = "Check out [Google](https://google.com) and [GitHub](https://github.com)."; | |
| const { links, textWithLinks } = extractLinks(text); | |
| expect(links).toHaveLength(2); | |
| expect(links[0]).toEqual({ text: "Google", url: "https://google.com" }); | |
| expect(links[1]).toEqual({ text: "GitHub", url: "https://github.com" }); | |
| expect(textWithLinks).toBe("Check out Google and GitHub."); | |
| }); | |
| it("handles text without links", () => { | |
| const text = "No links here."; | |
| const { links, textWithLinks } = extractLinks(text); | |
| expect(links).toHaveLength(0); | |
| expect(textWithLinks).toBe(text); | |
| }); | |
| }); | |
| describe("stripMarkdown", () => { | |
| it("strips bold markers", () => { | |
| expect(stripMarkdown("This is **bold** text")).toBe("This is bold text"); | |
| expect(stripMarkdown("This is __bold__ text")).toBe("This is bold text"); | |
| }); | |
| it("strips italic markers", () => { | |
| expect(stripMarkdown("This is *italic* text")).toBe("This is italic text"); | |
| expect(stripMarkdown("This is _italic_ text")).toBe("This is italic text"); | |
| }); | |
| it("strips strikethrough markers", () => { | |
| expect(stripMarkdown("This is ~~deleted~~ text")).toBe("This is deleted text"); | |
| }); | |
| it("strips headers", () => { | |
| expect(stripMarkdown("# Heading 1")).toBe("Heading 1"); | |
| expect(stripMarkdown("## Heading 2")).toBe("Heading 2"); | |
| expect(stripMarkdown("### Heading 3")).toBe("Heading 3"); | |
| }); | |
| it("strips blockquotes", () => { | |
| expect(stripMarkdown("> This is a quote")).toBe("This is a quote"); | |
| expect(stripMarkdown(">This is also a quote")).toBe("This is also a quote"); | |
| }); | |
| it("removes horizontal rules", () => { | |
| expect(stripMarkdown("Above\n---\nBelow")).toBe("Above\n\nBelow"); | |
| expect(stripMarkdown("Above\n***\nBelow")).toBe("Above\n\nBelow"); | |
| }); | |
| it("strips inline code markers", () => { | |
| expect(stripMarkdown("Use `const` keyword")).toBe("Use const keyword"); | |
| }); | |
| it("handles complex markdown", () => { | |
| const input = `# Title | |
| This is **bold** and *italic* text. | |
| > A quote | |
| Some ~~deleted~~ content.`; | |
| const result = stripMarkdown(input); | |
| expect(result).toContain("Title"); | |
| expect(result).toContain("This is bold and italic text."); | |
| expect(result).toContain("A quote"); | |
| expect(result).toContain("Some deleted content."); | |
| expect(result).not.toContain("#"); | |
| expect(result).not.toContain("**"); | |
| expect(result).not.toContain("~~"); | |
| expect(result).not.toContain(">"); | |
| }); | |
| }); | |
| describe("convertTableToFlexBubble", () => { | |
| it("creates a receipt-style card for 2-column tables", () => { | |
| const table = { | |
| headers: ["Item", "Price"], | |
| rows: [ | |
| ["Apple", "$1"], | |
| ["Banana", "$2"], | |
| ], | |
| }; | |
| const bubble = convertTableToFlexBubble(table); | |
| expect(bubble.type).toBe("bubble"); | |
| expect(bubble.body).toBeDefined(); | |
| }); | |
| it("creates a multi-column layout for 3+ column tables", () => { | |
| const table = { | |
| headers: ["A", "B", "C"], | |
| rows: [["1", "2", "3"]], | |
| }; | |
| const bubble = convertTableToFlexBubble(table); | |
| expect(bubble.type).toBe("bubble"); | |
| expect(bubble.body).toBeDefined(); | |
| }); | |
| it("replaces empty cells with placeholders", () => { | |
| const table = { | |
| headers: ["A", "B"], | |
| rows: [["", ""]], | |
| }; | |
| const bubble = convertTableToFlexBubble(table); | |
| const body = bubble.body as { | |
| contents: Array<{ contents?: Array<{ contents?: Array<{ text: string }> }> }>; | |
| }; | |
| const rowsBox = body.contents[2] as { contents: Array<{ contents: Array<{ text: string }> }> }; | |
| expect(rowsBox.contents[0].contents[0].text).toBe("-"); | |
| expect(rowsBox.contents[0].contents[1].text).toBe("-"); | |
| }); | |
| it("strips bold markers and applies weight for fully bold cells", () => { | |
| const table = { | |
| headers: ["**Name**", "Status"], | |
| rows: [["**Alpha**", "OK"]], | |
| }; | |
| const bubble = convertTableToFlexBubble(table); | |
| const body = bubble.body as { | |
| contents: Array<{ contents?: Array<{ text: string; weight?: string }> }>; | |
| }; | |
| const headerRow = body.contents[0] as { contents: Array<{ text: string; weight?: string }> }; | |
| const dataRow = body.contents[2] as { contents: Array<{ text: string; weight?: string }> }; | |
| expect(headerRow.contents[0].text).toBe("Name"); | |
| expect(headerRow.contents[0].weight).toBe("bold"); | |
| expect(dataRow.contents[0].text).toBe("Alpha"); | |
| expect(dataRow.contents[0].weight).toBe("bold"); | |
| }); | |
| }); | |
| describe("convertCodeBlockToFlexBubble", () => { | |
| it("creates a code card with language label", () => { | |
| const block = { language: "typescript", code: "const x = 1;" }; | |
| const bubble = convertCodeBlockToFlexBubble(block); | |
| expect(bubble.type).toBe("bubble"); | |
| expect(bubble.body).toBeDefined(); | |
| const body = bubble.body as { contents: Array<{ text: string }> }; | |
| expect(body.contents[0].text).toBe("Code (typescript)"); | |
| }); | |
| it("creates a code card without language", () => { | |
| const block = { code: "plain code" }; | |
| const bubble = convertCodeBlockToFlexBubble(block); | |
| const body = bubble.body as { contents: Array<{ text: string }> }; | |
| expect(body.contents[0].text).toBe("Code"); | |
| }); | |
| it("truncates very long code", () => { | |
| const longCode = "x".repeat(3000); | |
| const block = { code: longCode }; | |
| const bubble = convertCodeBlockToFlexBubble(block); | |
| const body = bubble.body as { contents: Array<{ contents: Array<{ text: string }> }> }; | |
| const codeText = body.contents[1].contents[0].text; | |
| expect(codeText.length).toBeLessThan(longCode.length); | |
| expect(codeText).toContain("..."); | |
| }); | |
| }); | |
| describe("processLineMessage", () => { | |
| it("processes text with tables", () => { | |
| const text = `Here's the data: | |
| | Key | Value | | |
| |-----|-------| | |
| | a | 1 | | |
| Done.`; | |
| const result = processLineMessage(text); | |
| expect(result.flexMessages).toHaveLength(1); | |
| expect(result.flexMessages[0].type).toBe("flex"); | |
| expect(result.text).toContain("Here's the data:"); | |
| expect(result.text).toContain("Done."); | |
| expect(result.text).not.toContain("|"); | |
| }); | |
| it("processes text with code blocks", () => { | |
| const text = `Check this code: | |
| \`\`\`js | |
| console.log("hi"); | |
| \`\`\` | |
| That's it.`; | |
| const result = processLineMessage(text); | |
| expect(result.flexMessages).toHaveLength(1); | |
| expect(result.text).toContain("Check this code:"); | |
| expect(result.text).toContain("That's it."); | |
| expect(result.text).not.toContain("```"); | |
| }); | |
| it("processes text with markdown formatting", () => { | |
| const text = "This is **bold** and *italic* text."; | |
| const result = processLineMessage(text); | |
| expect(result.text).toBe("This is bold and italic text."); | |
| expect(result.flexMessages).toHaveLength(0); | |
| }); | |
| it("handles mixed content", () => { | |
| const text = `# Summary | |
| Here's **important** info: | |
| | Item | Count | | |
| |------|-------| | |
| | A | 5 | | |
| \`\`\`python | |
| print("done") | |
| \`\`\` | |
| > Note: Check the link [here](https://example.com).`; | |
| const result = processLineMessage(text); | |
| // Should have 2 flex messages (table + code) | |
| expect(result.flexMessages).toHaveLength(2); | |
| // Text should be cleaned | |
| expect(result.text).toContain("Summary"); | |
| expect(result.text).toContain("important"); | |
| expect(result.text).toContain("Note: Check the link here."); | |
| expect(result.text).not.toContain("#"); | |
| expect(result.text).not.toContain("**"); | |
| expect(result.text).not.toContain("|"); | |
| expect(result.text).not.toContain("```"); | |
| expect(result.text).not.toContain("[here]"); | |
| }); | |
| it("handles plain text unchanged", () => { | |
| const text = "Just plain text with no markdown."; | |
| const result = processLineMessage(text); | |
| expect(result.text).toBe(text); | |
| expect(result.flexMessages).toHaveLength(0); | |
| }); | |
| }); | |
| describe("hasMarkdownToConvert", () => { | |
| it("detects tables", () => { | |
| const text = `| A | B | | |
| |---|---| | |
| | 1 | 2 |`; | |
| expect(hasMarkdownToConvert(text)).toBe(true); | |
| }); | |
| it("detects code blocks", () => { | |
| const text = "```js\ncode\n```"; | |
| expect(hasMarkdownToConvert(text)).toBe(true); | |
| }); | |
| it("detects bold", () => { | |
| expect(hasMarkdownToConvert("**bold**")).toBe(true); | |
| }); | |
| it("detects strikethrough", () => { | |
| expect(hasMarkdownToConvert("~~deleted~~")).toBe(true); | |
| }); | |
| it("detects headers", () => { | |
| expect(hasMarkdownToConvert("# Title")).toBe(true); | |
| }); | |
| it("detects blockquotes", () => { | |
| expect(hasMarkdownToConvert("> quote")).toBe(true); | |
| }); | |
| it("returns false for plain text", () => { | |
| expect(hasMarkdownToConvert("Just plain text.")).toBe(false); | |
| }); | |
| }); | |