import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import csvToMarkdown from "../src/CsvToMarkdown.js";
const require = createRequire(import.meta.url);
const cjsCsvToMarkdown: typeof csvToMarkdown = require("../lib/CsvToMarkdown.cjs");
test.each([
["commonjs", 'const csv = require("csv-to-markdown-table");'],
["module", 'import csv from "csv-to-markdown-table";'],
])("package root works in %s", (mode, load) => {
const output = execFileSync(
process.execPath,
[
`--input-type=${mode}`,
"--eval",
`${load} process.stdout.write(csv("a,b", ",", true));`,
],
{
cwd: fileURLToPath(new URL("..", import.meta.url)),
encoding: "utf8",
timeout: 5000,
},
);
expect(output).toBe("| a | b | \n|---|---| \n");
});
describe("csvToMarkdown", () => {
test("should pass custom enclosure and escape settings to the parser", () => {
expect(
csvToMarkdown("'a,b','say !'hi, there!' '", ",", true, {
enclosure: "'",
escape: "!",
prettyPrint: false,
}),
).toBe("|a,b|say !'hi, there!' |\n|---|---|\n");
});
test("should allow CSV escaping to be disabled", () => {
expect(
csvToMarkdown('"a\\",b', ",", true, {
escape: "",
prettyPrint: false,
}),
).toBe("|a\\\\|b|\n|---|---|\n");
});
test.each([{ enclosure: "ab" }, { escape: "ab" }])(
"should retain csv-walker validation for %p",
(options) => {
expect(() => csvToMarkdown("a,b", ",", true, options)).toThrow(TypeError);
},
);
test("should preserve explicit overrides alongside omitted options", () => {
expect(
csvToMarkdown('"a\r\nb"', ",", true, {
newlineReplacement: null,
prettyPrint: false,
}),
).toBe("|a\r\nb|\n|---|\n");
});
test.each([
["Name,Age\nAda,37", true, "|Name|Age|\n|---|---|\n|Ada|37|\n"],
["Ada,37", false, "|||\n|---|---|\n|Ada|37|\n"],
["Name,Age", true, "|Name|Age|\n|---|---|\n"],
["a,b\nc\nd,e,f", true, "|a|b||\n|---|---|---|\n|c|||\n|d|e|f|\n"],
["", false, "||\n|---|\n||\n"],
["", true, "||\n|---|\n"],
])(
"should render compact tables for %p with headers %p",
(csv, headers, expected) => {
expect(csvToMarkdown(csv, ",", headers, { prettyPrint: false })).toBe(
expected,
);
},
);
test("should preserve cell content and transformations in compact tables", () => {
expect(
csvToMarkdown('" a|b\\c\nd "', ",", true, {
prettyPrint: false,
cellFilter: (value) => value.toUpperCase(),
newlineReplacement: "
",
}),
).toBe("| A\\|B\\\\C
D |\n|---|\n");
});
test("should retain existing formatting when prettyPrint is true", () => {
expect(csvToMarkdown("a,b\nc,d", ",", true, { prettyPrint: true })).toBe(
csvToMarkdown("a,b\nc,d", ",", true),
);
});
test("should filter parsed fields including headers and empty strings", () => {
const seen: string[] = [];
const result = csvToMarkdown('name,note\nAda,"a,b"\nBob,', ",", true, {
cellFilter: (value) => {
seen.push(value);
return value.toUpperCase();
},
});
expect(seen).toEqual(["name", "note", "Ada", "a,b", "Bob", ""]);
expect(result).toBe(
"| NAME | NOTE | \n|------|------| \n| ADA | A,B | \n| BOB | | \n",
);
});
test.each([false, true])(
"should pass parsed field indexes with headers %p",
(hasHeader) => {
const seen: [string, number, number][] = [];
csvToMarkdown(
'name,note\nAda,"line one\nline two"\nBob,\nSolo',
",",
hasHeader,
{
cellFilter: (value, rowIndex, columnIndex) => {
seen.push([value, rowIndex, columnIndex]);
return value;
},
},
);
expect(seen).toEqual([
["name", 0, 0],
["note", 0, 1],
["Ada", 1, 0],
["line one\nline two", 1, 1],
["Bob", 2, 0],
["", 2, 1],
["Solo", 3, 0],
]);
},
);
test("should format and size the filtered values", () => {
const result = csvToMarkdown("x", ",", true, {
cellFilter: () => "a\tb\nc|d\\e",
newlineReplacement: " / ",
});
expect(result).toBe("| a b / c\\|d\\\\e | \n|------------------| \n");
});
test("should allow the filter to return an empty string", () => {
expect(csvToMarkdown("x", ",", true, { cellFilter: () => "" })).toBe(
"| | \n|--| \n",
);
});
test("should merge options without changing the defaults or supplied options", () => {
const options = { newlineReplacement: "" };
csvToMarkdown('"a\nb"', ",", true, options);
expect(csvToMarkdown('"a\nb"', ",", true)).toBe(
"| a
b | \n|--------| \n",
);
expect(options).toEqual({ newlineReplacement: "" });
});
test("should return headers and blank row when no csv data is passed and all other values, using default header setting and default tab delimeter", () => {
const result = csvToMarkdown("");
expect(result).toBe("| | \n|--| \n| | \n");
});
test("should return a table with blank headers, using default header setting and default tab delimeter", () => {
const result = csvToMarkdown("a\tb\tc");
expect(result).toBe("| | | | \n|---|---|---| \n| a | b | c | \n");
});
test("should handle windows newlines gracefully", () => {
const result = csvToMarkdown("a\tb\r\nc\td\r\ne\tf\r\n", "\t", true);
expect(result).toBe("| a | b | \n|---|---| \n| c | d | \n| e | f | \n");
});
test("should return a table with no headers", () => {
const result = csvToMarkdown("a\tb\tc", "\t", false);
expect(result).toBe("| | | | \n|---|---|---| \n| a | b | c | \n");
});
test("should return a table with headers and no data", () => {
const result = csvToMarkdown("a\tb\tc", "\t", true);
expect(result).toBe("| a | b | c | \n|---|---|---| \n");
});
test("should return a table with blank headers with various separators", () => {
const cases = [
["a\tb\tc", "\t"],
["a,b,c", ","],
["a;b;c", ";"],
];
cases.forEach((entry) => {
const result = csvToMarkdown(entry[0], entry[1], false);
expect(result).toBe("| | | | \n|---|---|---| \n| a | b | c | \n");
});
});
test("should reject multi-character delimiters", () => {
expect(() => csvToMarkdown("a||b", "||")).toThrow(TypeError);
});
test("should parse separators wrapped in quotes", () => {
const cases = [
['a\t"b\tc"\td', "\t"],
['a,"b,c",d', ","],
['a;"b;c";d', ";"],
];
cases.forEach((entry) => {
const result = csvToMarkdown(entry[0], entry[1], false);
expect(result).toBe(
"| | | | \n|---|-----|---| \n| a | b" + entry[1] + "c | d | \n",
);
});
});
test("should return a table with headers and no data (duplicate test)", () => {
const result = csvToMarkdown("a\tb\tc", "\t", true);
expect(result).toBe("| a | b | c | \n|---|---|---| \n");
});
test("should convert tabs to 4 spaces to work on github", () => {
const result = csvToMarkdown("a\tb\tc", ";", false);
expect(result).toBe(
"| | \n|-------------| \n| a b c | \n",
);
});
test("should format correctly with semicolons and long values", () => {
const result = csvToMarkdown("a;b;c;long value\nd;e;f", ";", false);
expect(result).toBe(
"| | | | | \n|---|---|---|------------| \n| a | b | c | long value | \n| d | e | f | | \n",
);
});
test("should parse delimiters wrapped by quotes", () => {
const result = csvToMarkdown('"a, b, c, d",e', ",", false);
expect(result).toBe(
"| | | \n|------------|---| \n| a, b, c, d | e | \n",
);
});
test("should escape pipes and back slashes", () => {
const result = csvToMarkdown('"a|b|c|d",e\\f\\g', ",", false);
expect(result).toBe(
"| | | \n|------------|---------| \n| a\\|b\\|c\\|d | e\\\\f\\\\g | \n",
);
});
test("should handle single values ending in the delimiter", () => {
const result = csvToMarkdown('"assd;"', ";", false);
expect(result).toBe("| | \n|-------| \n| assd; | \n");
});
test("should handle items that begin with word boundaries", () => {
const result = csvToMarkdown(
'"foo";"bar";"baz"\n"1";"2";"[foo -;- bar baz]"',
";",
true,
);
expect(result).toBe(
"| foo | bar | baz | \n|-----|-----|-------------------| \n| 1 | 2 | [foo -;- bar baz] | \n",
);
});
test("should parse doubled enclosures", () => {
const result = csvToMarkdown('name,quote\nAda,"said ""hi"""', ",", true);
expect(result).toBe(
'| name | quote | \n|------|-----------| \n| Ada | said "hi" | \n',
);
});
test("should render newlines in quoted fields", () => {
const result = csvToMarkdown(
'name,quote\nAda,"line one\nline two"',
",",
true,
);
expect(result).toBe(
"| name | quote | \n|------|----------------------| \n| Ada | line one
line two | \n",
);
});
test.each([undefined, {}])(
"should preserve default newline replacement with options %p",
(options) => {
expect(csvToMarkdown('"a\nb"', ",", true, options)).toBe(
"| a
b | \n|--------| \n",
);
},
);
test.each(["\n", "\r", "\r\n"])(
"should replace quoted %p newlines with a custom string",
(newline) => {
expect(
csvToMarkdown(`"a${newline}b"`, ",", true, {
newlineReplacement: " / ",
}),
).toBe("| a / b | \n|-------| \n");
},
);
test.each(["\n", "\r", "\r\n"])(
"should preserve quoted %p newlines when replacement is null",
(newline) => {
expect(
csvToMarkdown(`"a${newline}b|"`, ",", true, {
newlineReplacement: null,
}),
).toBe(`| a${newline}b\\| | \n|${"-".repeat(6 + newline.length)}| \n`);
},
);
test.each(["", "\n", "$&"])(
"should use newline replacement %p literally",
(newlineReplacement) => {
const value = `a${newlineReplacement}b`;
expect(csvToMarkdown('"a\nb"', ",", true, { newlineReplacement })).toBe(
`| ${value} | \n|${"-".repeat(value.length + 2)}| \n`,
);
},
);
test("should handle delimiters that are regex special characters", () => {
const delimiters = [
"[",
"]",
"\\",
"/",
"^",
"$",
".",
"|",
"?",
"*",
"+",
"(",
")",
"{",
"}",
"-",
];
delimiters.forEach((delimiter) => {
const result = csvToMarkdown(
"a" + delimiter + "b" + delimiter + "c",
delimiter,
false,
);
expect(result).toBe("| | | | \n|---|---|---| \n| a | b | c | \n");
});
});
});
describe("CommonJS build", () => {
test("should work when required", () => {
const result = cjsCsvToMarkdown("a,b,c", ",");
expect(result).toBe("| | | | \n|---|---|---| \n| a | b | c | \n");
});
});