Skip to content

haltcase/tablemark

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

tablemark Β· npm version license TypeScript GitHub Actions Workflow Status

Generate markdown tables from JSON data.

Render arrays of objects as markdown tables, with configurable fancy output.

  • Rename table headers and transform cell content
  • Align columns to the left, center, or right (all columns or per column)
  • Customize text casing for column headers (using change-case)
  • Auto-detect and handle ANSI styles and Unicode characters
  • Wrap or truncate long cell contents or strip line breaks

Installation

pnpm add tablemark

# or
npm install tablemark

# or
yarn add tablemark

# or
bun add tablemark

Usage

import { tablemark } from "tablemark";
tablemark([
	{ name: "Bob", age: 21, isCool: false },
	{ name: "Sarah", age: 22, isCool: true },
	{ name: "Lee", age: 23, isCool: true }
]);

// | Name  | Age   | Is cool |
// | :---- | :---- | :------ |
// | Bob   | 21    | false   |
// | Sarah | 22    | true    |
// | Lee   | 23    | true    |

... displays as:

Name Age Is cool
Bob 21 false
Sarah 22 true
Lee 23 true

API

tablemark (input: InputData, options?: TablemarkOptions): string

Arguments

  • InputData input: the data to table-ify as an array or iterable of objects

    • Note that nested objects are not supported. Use options.toCellText to customize how nested objects and other non-string values are output.
  • TablemarkOptions options:

    key type default description
    align "left" | "center" | "right" "left" Horizontal alignment to use for all columns.
    columns Array<string | ColumnDescriptor> - Array of column descriptors.
    countAnsiEscapeCodes boolean false Whether to count ANSI escape codes when calculating string width.
    headerCase "preserve" | ... "sentenceCase" Casing to use for headers derived from input object keys (read more).
    lineBreakStrategy "preserve" | "strip" | "truncate" "preserve" What to do when cell content contains line breaks.
    lineEnding string "\n" String used at end-of-line.
    maxWidth number Infinity Wrap cell text at this length.
    overflowStrategy "wrap" | "truncateStart" | "truncateEnd" "wrap" How to handle overflowing text in cells.
    overflowHeaderStrategy "wrap" | "truncateStart" | "truncateEnd" "wrap" How to handle overflowing text in header cells.
    padHeaderSeparator boolean true Whether to pad gutters of the header separator (alignment) row.
    toCellText ({ key, value }) => string - Provide a custom cell value transform function.
    toHeaderTitle ({ key, title }) => string - Provide a custom header title transform function.
    unknownKeyStrategy "ignore" | "throw" "ignore" How to handle unknown keys found in objects.
    textHandlingStrategy "auto" | "advanced" | basic "auto" Control support for ANSI styles or Unicode characters (read more).
    wrapWithGutters boolean false Add sides (| <content> |) to wrapped rows.

Returns

string: the resulting markdown formatted table

If input is an empty array, an empty string is returned.

Throws

TypeError: when input is not iterable (e.g., an array)
TypeError: when an unknown column alignment option is provided
RangeError: when config.unknownKeyStrategy === "throw" and an unknown key in an object is encountered

Note

The keys of the first encountered object are used for the table's headers. By default, any other keys from successive objects will be ignored, excluding those columns from the table. You can customize this behavior to make this raise an error by using config.unknownKeyStrategy.

options.align

Set the horizontal alignment for all columns. Accepts "left", "center", or "right".

tablemark(
	[
		{ name: "Bob", age: 21 },
		{ name: "Sarah", age: 22 }
	],
	{ align: "center" }
);

// | Name  | Age |
// | :---: | :-: |
// | Bob   | 21  |
// | Sarah | 22  |

options.columns

Describe the columns of the table. Each column can be a simple string to rename the column or an object with properties to further customize the column's behavior. The following properties are available and will override behavior specified elsewhere in options:

  • name: Name of the column used as the title in the header row.
  • align: Horizontal alignment of the column content.
  • maxWidth: Maximum content width of this column.
  • overflowHeaderStrategy: How to handle overflowing text in header cells. Defaults to "wrap".
  • overflowStrategy: How to handle overflowing text in this column. Defaults to the root overflowStrategy.
  • textHandlingStrategy: How to handle text in this column. Defaults to the root textHandlingStrategy.
  • width: Fixed display width for the column, overriding both the root- and column-level maxWidth setting.
tablemark(
	[
		{ name: "Bob", age: 21, isCool: false },
		{ name: "Sarah", age: 22, isCool: true },
		{ name: "Lee", age: 23, isCool: true }
	],
	{
		columns: [
			"first name",
			{ name: "how old", align: "center" },
			"are they cool"
		]
	}
);

// | first name | how old | are they cool |
// | :--------- | :-----: | :------------ |
// | Bob        |   21    | false         |
// | Sarah      |   22    | true          |
// | Lee        |   23    | true          |

... displays as:

first name how old are they cool
Bob 21 false
Sarah 22 true
Lee 23 true

options.countAnsiEscapeCodes

Control whether to count ANSI escape codes when calculating string width. The default is false, meaning ANSI codes are ignored. Setting this to true is useful when the output is not intended for a terminal, such as when generating a markdown table for an example in a README file.

const data = [
	{ text: "\u001B[31mRed\u001B[0m", note: "Normal text" },
	{ text: "\u001B[32mGreen\u001B[0m", note: "More text" }
];

tablemark(data, { countAnsiEscapeCodes: false });

// | Text  | Note        |
// | :---- | :---------- |
// | οΏ½[31mRedοΏ½[0m   | Normal text |
// | οΏ½[32mGreenοΏ½[0m | More text   |

tablemark(data, { countAnsiEscapeCodes: true });

// | Text           | Note        |
// | :------------- | :---------- |
// | οΏ½[31mRedοΏ½[0m   | Normal text |
// | οΏ½[32mGreenοΏ½[0m | More text   |

options.headerCase

Control the casing of headers derived from input object keys. The default is "sentenceCase". The options are:

  • "preserve": Keep the original case
  • "camelCase": Example: twoWords
  • "capitalCase": Example: Two Words
  • "constantCase": Example: TWO_WORDS
  • "dotCase": Example: two.words
  • "kebabCase": Example: two-words
  • "noCase": Example: two words
  • "pascalCase": Example: TwoWords
  • "pascalSnakeCase": Example: Two_Words
  • "pathCase": Example: two/words
  • "sentenceCase": Example: Two words
  • "snakeCase": Example: two_words
  • "trainCase": Example: Two-Words
tablemark([{ first_name: "Bob", last_name: "Smith" }], {
	headerCase: "constantCase"
});

// | FIRST_NAME | LAST_NAME |
// | :--------- | :-------- |
// | Bob        | Smith     |

options.lineBreakStrategy

Specify how to handle line breaks in cell content. The options are:

  • "preserve" (default): Keep line breaks
  • "strip": Replace line breaks with spaces
  • "truncate": Trim content at the first line break
tablemark([{ note: "Line 1\nLine 2" }], { lineBreakStrategy: "strip" });

// | Note          |
// | :------------ |
// | Line 1 Line 2 |

options.lineEnding

Set the string used at the end of each line. The default is \n (linefeed).

tablemark([{ name: "Bob" }], { lineEnding: "\r\n" });

options.maxWidth

Set options.maxWidth to wrap any content at that length onto a new adjacent line:

tablemark(
	[
		{ star: false, name: "Benjamin" },
		{ star: true, name: "Jet Li" }
	],
	{ maxWidth: 5 }
);

// | Star  | Name  |
// | :---- | :---- |
// | false | Benja |
//           min
// | true  | Jet   |
//           Li

Note

To output valid GitHub Flavored Markdown a cell must not contain newlines. Consider replacing those with <br /> (e.g., using options.toCellText).

options.overflowHeaderStrategy

Control how overflowing text in header cells is handled. The options are the same as overflowStrategy. The default is "wrap".

options.overflowStrategy

Control how overflowing text in cells is handled. The options are:

  • "wrap": Wrap text to a new line
  • "truncateStart": Trim overflowing content at the start and replace with …
  • "truncateEnd" (default): Trim overflowing content at the end and replace with …
tablemark([{ desc: "This is a long description" }], {
	maxWidth: 17,
	overflowStrategy: "truncateStart"
});

// | Desc              |
// | :---------------- |
// | …long description |

options.padHeaderSeparator

Exclude padding around the header's dividing lines (which some formatters prefer).

tablemark(
	[
		{ name: "Bob", age: 21, isCool: false },
		{ name: "Sarah", age: 22, isCool: true },
		{ name: "Lee", age: 23, isCool: true }
	],
	{
		columns: [{ align: "left" }, { align: "center" }, { align: "right" }]
	}
);

// | first name | how old | are they cool |
// |:-----------|:-------:|--------------:|
// | Bob        |   21    | false         |
// | Sarah      |   22    | true          |
// | Lee        |   23    | true          |

... displays as:

first name how old are they cool
Bob 21 false
Sarah 22 true
Lee 23 true

options.textHandlingStrategy

By default, tablemark attempts to detect and handle text containing characters like emoji, halfwidth and fullwidth characters, and ANSI escape codes (like terminal colors and styles).

Other options, which are only recommended for very specific use cases, are:

  • "auto" (default): Automatically determine the best text handling strategy based on the input data. This is the recommended option for most use cases.
  • "basic": Faster, but lacks support for properly wrapping text containing certain emojis, halfwidth and fullwidth characters, and ANSI styles.
  • "advanced": Forces proper handling of emoji, halfwidth and fullwidth characters, and ANSI styles, but is slow for large datasets.

To illustrate the difference, notice how the first example below doesn't accurately align the | characters of the row containing the CJK and doesn't apply the ANSI styles correctly while the second example does.

tablemark(
	[
		{
			name: "\u001B[4mThis text, containing emoji πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦, ANSI styles and CJK 叀, will not wrap or style properly\u001B[0m"
		}
	],
	{
		textHandlingStrategy: "basic",
		maxWidth: 17,
		wrapWithGutters: true
	}
);

// | Name              |
// | :---------------- |
// | This text,        |
// | containing emoji  |
// | πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦, ANSI          |
// | styles and CJK    |
// | 叀, will not align |
// | or style          |
// | properly          |

tablemark(
	[
		{
			name: "\u001B[4mThis text, containing emoji πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦, ANSI styles and CJK 叀, will wrap and style properly\u001B[0m"
		}
	],
	{
		// Note that this the default value and can be omitted
		textHandlingStrategy: "auto", // or "advanced"
		maxWidth: 17,
		wrapWithGutters: true
	}
);

// | Name              |
// | :---------------- |
// | This text,        |
// | containing emoji  |
// | πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦, ANSI styles   |
// | and CJK 叀, will  |
// | align and style   |
// | properly          |

Screenshot of the terminal output of the above demonstrating the difference between basic and advanced text handling strategies

options.toCellText

Transform the contents of a body cell. This function is called with an object containing the key of the column and the value of the cell and should return a string to be used as the cell's content.

const toCellText = ({ key, value }) => {
	if (value === true) {
		return "βœ”";
	}

	if (!value) {
		if (key === "studying") {
			return "X";
		}

		return "";
	}

	return value;
};

tablemark(
	[
		{ name: "Bob", pet_owner: true, studying: false },
		{ name: "Sarah", pet_owner: false, studying: true },
		{ name: "Sarah", pet_owner: true, studying: true }
	],
	{
		toCellText,
		columns: [{ align: "left" }, { align: "center" }, { align: "center" }]
	}
);

// | Name  | Pet owner | Studying |
// | :---- | :-------: | :------: |
// | Bob   |     βœ”οΈŽ     |    X     |
// | Sarah |           |    βœ”     |
// | Lee   |     βœ”     |    βœ”     |

TypeScript object key inference

If you define your toCellText function directly within the tablemark function call as shown above, the type of the key property may be automatically constrained to the keys of your input (if known).

tablemark([{ name: "Bob", role: "Admin" }], {
	toCellText: ({ key, value }) => {
		// `key` is of type `name | role`
		return value;
	}
});

If you want to extract that definition elsewhere, use tablemark's ToCellText type, and combine it with the GetDataKeys helper type for strong typing.

import { tablemark, type ToCellText, type GetDataKeys } from "tablemark";

const data = [{ name: "Bob", role: "Admin" }] as const;

const toCellText: ToCellText<GetDataKeys<typeof data>> = ({ key, value }) => {
	// `key` is of type `name | role`
	return value;
};

tablemark(data, {
	toCellText
});

options.toHeaderTitle

Transform your header titles. This function is called with an object containing the key of the column and the title of the header cell and should return a string to be used as the header cell's content.

This transformation is applied after headerCase, so this function will be called with both the original object key as well as the cased title.

const toHeaderTitle = ({ key, title }) => {
	if (key === "name") {
		// Output the title with bold styling, e.g., for command line display
		// (you could use a package like `chalk` to do this)
		return `\u001B[1m${title}\u001B[0m`;
	}

	return title;
};

tablemark(
	[
		{ name: "Bob", pet_owner: true, studying: false },
		{ name: "Sarah", pet_owner: false, studying: true },
		{ name: "Kisha", pet_owner: true, studying: true }
	],
	{
		toHeaderTitle,
		columns: [{ align: "left" }, { align: "center" }, { align: "center" }]
	}
);

TypeScript object key inference

If you define your toHeaderTitle function directly within the tablemark function call as shown above, the type of the key property can be automatically constrained to the keys of your input (if known).

tablemark([{ name: "Bob", role: "Admin" }], {
	toHeaderTitle: ({ key, title }) => {
		// `key` is of type `name | role`
		return title;
	}
});

If you want to extract that definition elsewhere, use tablemark's ToHeaderTitle type, and combine it with the GetDataKeys helper type for strong typing.

import { tablemark, type ToHeaderTitle, type GetDataKeys } from "tablemark";

const data = [{ name: "Bob", role: "Admin" }] as const;

const toHeaderTitle: ToHeaderTitle<GetDataKeys<typeof data>> = ({
	key,
	title
}) => {
	// `key` is of type `name | role`
	return title;
};

tablemark(data, {
	toHeaderTitle
});

options.unknownKeyStrategy

Control how unknown keys in objects are handled:

  • "ignore" (default): ignore unknown keys
  • "throw": throw an error if an unknown key is found
tablemark(
	[
		{ a: 1 },
		{ a: 2, b: 3 } // 'b' is unknown
	],
	{ unknownKeyStrategy: "throw" }
);
// Throws `RangeError`

options.wrapWithGutters

Enable wrapWithGutters to add pipes on all lines:

tablemark(
	[
		{ star: false, name: "Benjamin" },
		{ star: true, name: "Jet Li" }
	],
	{ maxWidth: 5, wrapWithGutters: true }
);

// | Star  | Name  |
// | :---- | :---- |
// | false | Benja |
// |       | min   |
// | true  | Jet   |
// |       | Li    |

See also

And several tools that power tablemark:

  • ansi-regex – regular expression for matching ANSI escape codes
  • change-case – convert strings between camelCase, PascalCase, Capital Case, snake_case and more
  • string-width – get the visual width of a string - the number of columns required to display it
  • wrap-ansi – wordwrap a string with ANSI escape codes

Contributing

Search the issues if you come across any trouble, open a new one if it hasn't been posted, or, if you're able, open a pull request. Contributions of any kind are welcome in this project.

The following people have already contributed their time and effort:

Thank you!

License

MIT Β© Bo Lingen / haltcase

About

Generate markdown tables from JSON data.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •