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"); }); });