323 lines
9.4 KiB
TypeScript
323 lines
9.4 KiB
TypeScript
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 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 code blocks across language/no-language/multiple variants", () => {
|
|
const withLanguage = `Here is some code:
|
|
|
|
\`\`\`javascript
|
|
const x = 1;
|
|
console.log(x);
|
|
\`\`\`
|
|
|
|
And more text.`;
|
|
const withLanguageResult = extractCodeBlocks(withLanguage);
|
|
expect(withLanguageResult.codeBlocks).toHaveLength(1);
|
|
expect(withLanguageResult.codeBlocks[0].language).toBe("javascript");
|
|
expect(withLanguageResult.codeBlocks[0].code).toBe("const x = 1;\nconsole.log(x);");
|
|
expect(withLanguageResult.textWithoutCode).toContain("Here is some code:");
|
|
expect(withLanguageResult.textWithoutCode).toContain("And more text.");
|
|
expect(withLanguageResult.textWithoutCode).not.toContain("```");
|
|
|
|
const withoutLanguage = `\`\`\`
|
|
plain code
|
|
\`\`\``;
|
|
const withoutLanguageResult = extractCodeBlocks(withoutLanguage);
|
|
expect(withoutLanguageResult.codeBlocks).toHaveLength(1);
|
|
expect(withoutLanguageResult.codeBlocks[0].language).toBeUndefined();
|
|
expect(withoutLanguageResult.codeBlocks[0].code).toBe("plain code");
|
|
|
|
const multiple = `\`\`\`python
|
|
print("hello")
|
|
\`\`\`
|
|
|
|
Some text
|
|
|
|
\`\`\`bash
|
|
echo "world"
|
|
\`\`\``;
|
|
const multipleResult = extractCodeBlocks(multiple);
|
|
expect(multipleResult.codeBlocks).toHaveLength(2);
|
|
expect(multipleResult.codeBlocks[0].language).toBe("python");
|
|
expect(multipleResult.codeBlocks[1].language).toBe("bash");
|
|
});
|
|
});
|
|
|
|
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.");
|
|
});
|
|
});
|
|
|
|
describe("stripMarkdown", () => {
|
|
it("strips inline markdown marker variants", () => {
|
|
const cases = [
|
|
["strips bold **", "This is **bold** text", "This is bold text"],
|
|
["strips bold __", "This is __bold__ text", "This is bold text"],
|
|
["strips italic *", "This is *italic* text", "This is italic text"],
|
|
["strips italic _", "This is _italic_ text", "This is italic text"],
|
|
["strips strikethrough", "This is ~~deleted~~ text", "This is deleted text"],
|
|
["removes hr ---", "Above\n---\nBelow", "Above\n\nBelow"],
|
|
["removes hr ***", "Above\n***\nBelow", "Above\n\nBelow"],
|
|
["strips inline code markers", "Use `const` keyword", "Use const keyword"],
|
|
] as const;
|
|
for (const [name, input, expected] of cases) {
|
|
expect(stripMarkdown(input), name).toBe(expected);
|
|
}
|
|
});
|
|
|
|
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("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);
|
|
|
|
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 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("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 supported markdown patterns", () => {
|
|
const cases = [
|
|
`| A | B |
|
|
|---|---|
|
|
| 1 | 2 |`,
|
|
"```js\ncode\n```",
|
|
"**bold**",
|
|
"~~deleted~~",
|
|
"# Title",
|
|
"> quote",
|
|
];
|
|
|
|
for (const text of cases) {
|
|
expect(hasMarkdownToConvert(text)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("returns false for plain text", () => {
|
|
expect(hasMarkdownToConvert("Just plain text.")).toBe(false);
|
|
});
|
|
});
|