// Output formats. They all get the JSON result from the daemon; none talks to WhatsApp. // To add a format, add it to FORMATS as (data, opts) => string. import { toEml, toMbox } from './mail.js'; import { toText } from './text.js'; export const FORMATS = { json: (data) => JSON.stringify(data, null, 2), ndjson: (data) => (Array.isArray(data) ? data : [data]).map((d) => JSON.stringify(d)).join('\n'), table: (data, opts) => table(data, opts), csv: (data, opts) => csv(data, opts), yaml: (data) => yaml(data).replace(/\n$/, ''), text: (data, opts) => toText(data, opts) ?? table(data, opts), eml: (data) => toEml(data), mbox: (data) => toMbox(data), }; export function format(data, name, opts = {}) { const fn = FORMATS[name]; if (!fn) throw Object.assign(new Error(`formato desconocido: ${name} (${Object.keys(FORMATS).join(', ')})`), { code: 'EINVAL' }); return fn(data, opts); } // ---------------------------------------------------------------- helpers export function getPath(obj, key) { return key.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj); } function flatten(obj, prefix = '', out = {}) { for (const [k, v] of Object.entries(obj ?? {})) { const key = prefix ? `${prefix}.${k}` : k; if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out); else out[key] = v; } return out; } function cell(v) { if (v === null || v === undefined) return ''; if (Array.isArray(v)) return v.map((x) => (typeof x === 'object' ? JSON.stringify(x) : String(x))).join(', '); if (typeof v === 'object') return JSON.stringify(v); return String(v); } function rowsAndColumns(data, columns) { if (!Array.isArray(data)) { const flat = flatten(data); return { columns: ['key', 'value'], rows: Object.entries(flat).map(([key, value]) => ({ key, value })) }; } if (!columns) { const seen = new Set(); for (const r of data.slice(0, 50)) { for (const [k, v] of Object.entries(flatten(r))) if (typeof v !== 'object' || v === null) seen.add(k); } columns = [...seen]; } return { columns, rows: data }; } // Approximate terminal width (emoji and CJK take 2 columns). const WIDE = /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩\u{1f300}-\u{1faff}]/u; function width(s) { let w = 0; for (const ch of s) w += WIDE.test(ch) ? 2 : /\p{M}|‍|️/u.test(ch) ? 0 : 1; return w; } function truncate(s, max) { if (width(s) <= max) return s; let out = ''; let w = 0; for (const ch of s) { const cw = width(ch); if (w + cw > max - 1) break; out += ch; w += cw; } return out + '…'; } const pad = (s, n) => s + ' '.repeat(Math.max(0, n - width(s))); function table(data, { columns } = {}) { if (data === null || data === undefined) return ''; if (typeof data !== 'object') return String(data); const { columns: cols, rows } = rowsAndColumns(data, columns); if (!rows.length) return ''; const cells = rows.map((r) => cols.map((c) => cell(getPath(r, c)).replace(/\s*\n\s*/g, ' ⏎ '))); const widths = cols.map((c, i) => Math.max(width(c), ...cells.map((r) => width(r[i])))); // Fit the terminal width by shrinking the widest columns. const max = process.stdout.columns || 160; const sep = 2; while (widths.reduce((a, b) => a + b, 0) + sep * (cols.length - 1) > max) { const i = widths.indexOf(Math.max(...widths)); if (widths[i] <= 12) break; widths[i]--; } const line = (vals) => vals.map((v, i) => pad(truncate(v, widths[i]), widths[i])).join(' '.repeat(sep)).trimEnd(); return [line(cols.map((c) => c.toUpperCase())), ...cells.map(line)].join('\n'); } function csv(data, { columns } = {}) { if (data === null || data === undefined || typeof data !== 'object') return cell(data); const { columns: cols, rows } = rowsAndColumns(data, columns); const esc = (s) => (/[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s); return [cols.join(','), ...rows.map((r) => cols.map((c) => esc(cell(getPath(r, c)))).join(','))].join('\n'); } function yamlScalar(v) { if (v === null || v === undefined) return 'null'; if (typeof v === 'number' || typeof v === 'boolean') return String(v); const s = String(v); if (s === '' || /^[\s-?:,[\]{}#&*!|>'"%@`]|[:#]\s|\s$|\n/.test(s) || /^(true|false|null|yes|no|on|off|~|[-+]?\d[\d_.eE+-]*)$/i.test(s)) { return JSON.stringify(s); } return s; } function yaml(v, indent = '') { if (Array.isArray(v)) { if (!v.length) return `${indent}[]\n`; return v.map((item) => { if (item && typeof item === 'object' && Object.keys(item).length) { const inner = yaml(item, `${indent} `); return `${indent}- ${inner.slice(indent.length + 2)}`; } return `${indent}- ${item && typeof item === 'object' ? (Array.isArray(item) ? '[]' : '{}') : yamlScalar(item)}\n`; }).join(''); } if (v && typeof v === 'object') { const entries = Object.entries(v); if (!entries.length) return `${indent}{}\n`; return entries.map(([k, val]) => { const key = yamlScalar(k); if (val && typeof val === 'object' && (Array.isArray(val) ? val.length : Object.keys(val).length)) { return `${indent}${key}:\n${yaml(val, `${indent} `)}`; } const scalar = val && typeof val === 'object' ? (Array.isArray(val) ? '[]' : '{}') : yamlScalar(val); return `${indent}${key}: ${scalar}\n`; }).join(''); } return `${indent}${yamlScalar(v)}\n`; }