Source: dataTable.js

// Licensed under the MIT License
// https://github.com/craigahobbs/markdown-up/blob/main/LICENSE

/** @module lib/dataTable */

import {formatValue} from './dataUtil.js';
import {markdownElements} from 'markdown-model/lib/elements.js';
import {parseMarkdown} from 'markdown-model/lib/parser.js';
import {parseSchemaMarkdown} from 'schema-markdown/lib/parser.js';
import {validateType} from 'schema-markdown/lib/schema.js';
import {valueCompare} from 'bare-script/lib/value.js';


// The data table model's Schema Markdown
export const dataTableTypes = parseSchemaMarkdown(`\
group "Data Table"


# A data table model
struct DataTable

    # The table's fields
    optional string[len > 0] fields

    # The table's category fields
    optional string[len > 0] categories

    # The field formatting for "categories" and "fields"
    optional DataTableFieldFormat{len > 0} formats

    # The numeric formatting precision (default is 2)
    optional int(>= 0) precision

    # The datetime format
    optional DataTableDatetimeFormat datetime

    # If true, trim formatted values (default is true)
    optional bool trim


# A data table field formatting model
struct DataTableFieldFormat

    # The field alignment
    optional DataTableFieldAlignment align

    # If true, don't wrap text
    optional bool nowrap

    # If true, format the field as Markdown
    optional bool markdown


# A field alignment
enum DataTableFieldAlignment
    left
    right
    center


# A datetime format
enum DataTableDatetimeFormat

    # ISO datetime year format
    year

    # ISO datetime month format
    month

    # ISO datetime day format
    day
`);


/**
 * Validate a data table model
 *
 * @param {Object} dataTable - The
 *     [data table model]{@link https://craigahobbs.github.io/markdown-up/library/model.html#var.vName='DataTable'}
 * @returns {Object}
 * @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
 */
export function validateDataTable(dataTable) {
    return validateType(dataTableTypes, 'DataTable', dataTable);
}


/**
 * The data table options object
 *
 * @typedef {Object} DataTableOptions
 * @property {function} [urlFn] - The
 *     [Markdown URL modifier function]{@link https://craigahobbs.github.io/markdown-model/module-lib_elements.html#~URLFn}
 */


/**
 * Render a data table
 *
 * @param {Object[]} data - The data array
 * @param {?Object} [dataTable = null] - The
 *     [data table model]{@link https://craigahobbs.github.io/markdown-up/library/model.html#var.vName='DataTable'}
 * @param {?Object} [options = null] - The [data table options]{@link module:lib/dataTable~DataTableOptions}
 * @returns {Object} The data table [element model]{@link https://github.com/craigahobbs/element-model#readme}
 */
export function dataTableElements(data, dataTable = null, options = null) {
    // Compute the table's field names
    const categories = (dataTable !== null ? (dataTable.categories ?? []) : []);
    let tableFields = (dataTable !== null ? (dataTable.fields ?? null) : null);
    if (tableFields === null) {
        const allFields = new Set();
        for (const row of data) {
            for (const fieldName of Object.keys(row)) {
                allFields.add(fieldName);
            }
        }
        tableFields = Array.from(allFields.values());
        if (categories !== null) {
            tableFields = tableFields.filter((key) => categories.indexOf(key) === -1);
        }
    }

    // Generate the data table's element model
    const fieldFormats = (dataTable !== null ? (dataTable.formats ?? null) : null);
    const markdownOptions = (options !== null && 'urlFn' in options ? {'urlFn': options.urlFn} : null);
    const formatPrecision = (dataTable !== null ? (dataTable.precision ?? null) : null);
    const formatDatetime = (dataTable !== null ? (dataTable.datetime ?? null) : null);
    const formatTrim = (dataTable !== null ? (dataTable.trim ?? null) : null);
    return {
        'html': 'table',
        'elem': [
            // Table header
            {
                'html': 'tr',
                'elem': [
                    categories.map((field) => {
                        const fieldFormat = (fieldFormats !== null ? (fieldFormats[field] ?? null) : null);
                        return {
                            'html': 'th',
                            'attr': createFieldAttr(fieldFormat),
                            'elem': {'text': field}
                        };
                    }),
                    tableFields.map((field) => {
                        const fieldFormat = (fieldFormats !== null ? (fieldFormats[field] ?? null) : null);
                        return {
                            'html': 'th',
                            'attr': createFieldAttr(fieldFormat),
                            'elem': {'text': field}
                        };
                    })
                ]
            },

            // Table data
            data.map((row, ixRow) => {
                const rowPrev = ixRow > 0 ? data[ixRow - 1] : null;
                let skip = rowPrev !== null;
                return {
                    'html': 'tr',
                    'elem': [
                        categories.map((field) => {
                            const value = row[field] ?? null;

                            // Skip this value?
                            if (skip) {
                                skip = (valueCompare(value, rowPrev[field] ?? null) === 0);
                            }

                            const fieldFormat = (fieldFormats !== null ? (fieldFormats[field] ?? null) : null);
                            const fieldElements = fieldFormat !== null && fieldFormat.markdown
                                ? markdownElements(parseMarkdown(`${value}`), markdownOptions)
                                : {'text': formatValue(value, formatPrecision, formatDatetime, formatTrim)};
                            return {
                                'html': 'td',
                                'attr': createFieldAttr(fieldFormat),
                                'elem': (skip ? null : fieldElements)
                            };
                        }),
                        tableFields.map((field) => {
                            const value = row[field] ?? null;
                            const fieldFormat = (fieldFormats !== null ? (fieldFormats[field] ?? null) : null);
                            const fieldElements = fieldFormat !== null && fieldFormat.markdown
                                ? markdownElements(parseMarkdown(`${value}`), markdownOptions)
                                : {'text': formatValue(value, formatPrecision, formatDatetime, formatTrim)};
                            return {
                                'html': 'td',
                                'attr': createFieldAttr(fieldFormat),
                                'elem': fieldElements
                            };
                        })
                    ]
                };
            })
        ]
    };
}


function createFieldAttr(fieldFormat) {
    const fieldStyles = [];
    if (fieldFormat !== null && 'align' in fieldFormat) {
        fieldStyles.push(`text-align: ${fieldFormat.align}`);
    }
    if (fieldFormat !== null && fieldFormat.nowrap) {
        fieldStyles.push('white-space: nowrap');
    }
    return (fieldStyles.length ? {'style': fieldStyles.join('; ')} : null);
}