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
pnpm add tablemark
# or
npm install tablemark
# or
yarn add tablemark
# or
bun add tablemark
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 |
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.
- Note that nested objects are not supported. Use
-
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
.
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 |
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 rootoverflowStrategy
.textHandlingStrategy
: How to handle text in this column. Defaults to the roottextHandlingStrategy
.width
: Fixed display width for the column, overriding both the root- and column-levelmaxWidth
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 |
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 |
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 |
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 |
Set the string used at the end of each line. The default is \n
(linefeed).
tablemark([{ name: "Bob" }], { lineEnding: "\r\n" });
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
).
Control how overflowing text in header cells is handled. The options are the same as overflowStrategy
. The default is "wrap"
.
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 |
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 |
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 |
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 | β | β |
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
});
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" }]
}
);
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
});
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`
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 |
tablemark-cli
β use this module from the command line
And several tools that power tablemark:
ansi-regex
β regular expression for matching ANSI escape codeschange-case
β convert strings between camelCase, PascalCase, Capital Case, snake_case and morestring-width
β get the visual width of a string - the number of columns required to display itwrap-ansi
β wordwrap a string with ANSI escape codes
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:
- Thomas Jensen (@tjconcept)
Thank you!
MIT Β© Bo Lingen / haltcase