@tabular-json/tabular-json
Version:
Tabular-JSON: a superset of JSON with CSV-like tables
170 lines • 6.14 kB
JavaScript
import { getIn, isObject } from './objects.js';
import { collectFields, isTabular } from './tabular.js';
export function stringify(json, options) {
const globalIndentation = resolveIndentation(options?.indentation);
return stringifyValue(json, '', globalIndentation);
function stringifyValue(value, indent, indentation) {
// number
if (typeof value === 'number') {
if (isNaN(value)) {
return 'nan';
}
if (value === Infinity) {
return 'inf';
}
if (value === -Infinity) {
return '-inf';
}
return JSON.stringify(value);
}
// boolean, null
if (value === true || value === false || value === null) {
return JSON.stringify(value);
}
// string
if (typeof value === 'string') {
return stringifyStringValue(value);
}
// BigInt
if (typeof value === 'bigint') {
return value.toString();
}
// Table
if (isTabular(value)) {
return stringifyTable(value, indent, indentation);
}
// Array (test after Table!)
if (Array.isArray(value)) {
return stringifyArray(value, indent, indentation);
}
// Object
if (isObject(value)) {
return stringifyObject(value, indent, indentation);
}
return '';
}
function stringifyArray(array, indent, indentation) {
if (array.length === 0) {
return '[]';
}
const childIndent = indentation ? indent + indentation : indent;
let str = indentation ? '[\n' : '[';
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (indentation) {
str += childIndent;
}
if (typeof item !== 'undefined' && typeof item !== 'function') {
str += stringifyValue(item, childIndent, indentation);
}
else {
str += 'null';
}
if (i < array.length - 1) {
str += indentation ? ',\n' : ',';
}
else if (options?.trailingCommas) {
str += ',';
}
}
str += indentation ? '\n' + indent + ']' : ']';
return str;
}
function stringifyTable(array, indent, indentation) {
const isRoot = array === json;
const childIndent = indentation && indent ? indent + indentation : indent;
const colSeparator = indentation ? ', ' : ',';
const fields = getFields(array);
let str = isRoot ? '' : '---\n';
const header = fields.map((field) => field.name);
const rows = array.map((item) => fields.map((field) => stringifyValue(field.getValue(item), childIndent, undefined)));
if (indentation) {
const widths = calculateColumnWidths(header, rows);
str += childIndent + formatRow(header, widths);
rows.forEach((row) => (str += childIndent + formatRow(row, widths)));
}
else {
str += childIndent + header.join(colSeparator) + '\n';
rows.forEach((row) => (str += childIndent + row.join(colSeparator) + '\n'));
}
str += isRoot ? '' : indent + '---';
return str;
}
function formatRow(row, widths) {
return row
.map((field, f) => (f < widths.length - 1 ? (field + ',').padEnd(widths[f]) : field + '\n'))
.join('');
}
function stringifyObject(object, indent, indentation) {
if (typeof object.toJSON === 'function') {
return stringify(object.toJSON(), options);
}
const entries = Object.entries(object).filter(([_key, value]) => includeProperty(value));
if (entries.length === 0) {
return '{}';
}
const childIndent = indentation ? indent + indentation : indent;
let str = indentation ? '{\n' : '{';
entries.forEach(([key, value], index) => {
const keyStr = stringifyStringValue(key);
str += indentation ? childIndent + keyStr + ': ' : keyStr + ':';
str += stringifyValue(value, childIndent, indentation);
if (index < entries.length - 1) {
str += indentation ? ',\n' : ',';
}
else if (options?.trailingCommas) {
str += ',';
}
});
str += indentation ? '\n' + indent + '}' : '}';
return str;
}
/**
* Test whether to include a property in a stringified object or not.
*/
function includeProperty(value) {
return typeof value !== 'undefined' && typeof value !== 'function' && typeof value !== 'symbol';
}
}
/**
* Resolve a JSON stringify space:
* replace a number with a string containing that number of spaces
*/
function resolveIndentation(indentation) {
if (typeof indentation === 'number') {
return ' '.repeat(indentation);
}
if (typeof indentation === 'string' && indentation !== '') {
return indentation;
}
return undefined;
}
function getFields(records) {
return collectFields(records).map((path) => ({
name: stringifyField(path),
getValue: createGetValue(path)
}));
}
function createGetValue(path) {
if (path.length === 1) {
const key = path[0];
return (item) => item[key];
}
return (item) => getIn(item, path);
}
function stringifyStringValue(value) {
return JSON.stringify(value);
}
function stringifyField(path) {
return path.map((key) => stringifyStringValue(String(key))).join('.');
}
function calculateColumnWidths(header, rows) {
return rows
.reduce((widths, row) => {
return row.map((field, i) => Math.max(widths[i], field.length));
}, header.map((field) => field.length))
.map((width) => width + 2);
// Note: we add 1 space to account for the comma,
// and another to ensure there is at least 1 space between the columns
}
//# sourceMappingURL=stringify.js.map